BottomNav
A fixed bar of icon tabs along the bottom of a phone's screen, with counts and the current one marked.
Demo
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/bottom_nav.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/bottom_nav.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/bottom_nav.qwe
let bottom_nav_demo_items = [ { label: "Home", href: "#home", icon: "M3 11 12 3l9 8M5 9.5V21h14V9.5" }, { label: "Search", href: "#search", icon: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM16 16l5 5" }, { label: "Inbox", href: "#bottom_nav", icon: "M4 4h16v16H4zM4 13h5l1.5 2h3L15 13h5", badge: 3 }, { label: "Profile", href: "#profile", icon: "M16 19v-1a4 4 0 0 0-8 0v1M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6" }]component BottomNavDemoLive() { state tab = "Feed" <div.demo-stack> <div style="width: min(360px, 100%); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); overflow: hidden"> <div style="height: 80px; padding: 14px"><p.demo-note>Showing: {tab}</p></div> <BottomNav id="bottom-nav-demo-live" fixed={false} labels={false} current={tab} items={[ { label: "Feed", icon: "📰" }, { label: "Explore", icon: "🧭" }, { label: "Saved", icon: "🔖", badge: 7 } ]} on_select={(it) => { tab = it.label }}/> </div> </div>}view BottomNavDemo() = <div.demo-stack> <div style="width: min(360px, 100%); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); overflow: hidden"> <div style="height: 80px; padding: 14px"><p.demo-note>A phone's screen; the bar stays at its bottom.</p></div> <BottomNav id="bottom-nav-demo" fixed={false} items={bottom_nav_demo_items} current="#bottom_nav"/> </div> <BottomNavDemoLive/> </div>Source
ui/bottom_nav.qwe
// BottomNav: a bar of icon tabs along the bottom of the screen, for phones: three to// five places in the app, the current one marked. It stays fixed at the bottom, above// the home indicator, and is hidden on wide screens unless asked to stay.//// <BottomNav current={request.path} items={[// { label: "Home", href: "/", icon: "M3 11 12 3l9 8M5 9.5V21h14V9.5" },// { label: "Search", href: "/search", icon: "🔍" },// { label: "Inbox", href: "/inbox", icon: "M4 4h16v16H4zM4 13h5l1.5 2h3L15 13h5", badge: 3 },// { label: "Profile", href: "/me", icon: "M16 19v-1a4 4 0 0 0-8 0v1M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6" }// ]}/>//// items: { label, href?, icon, badge? }. icon: a character or an emoji, or an SVG path// (24x24, stroked) when it starts with "M". badge: a count on the icon's corner. An// item without an href is a button that, inside one of your components, calls// on_select(item). current: the href of the page showing (aria-current="page").// fixed: at the bottom of the screen (true); false puts it in the page where it is// written, for a frame or a demo. wide: keep it on wide screens too (it hides past// 720px by default, where a Navbar takes over). labels: show the words under the// icons (true); without them each icon's label is its tooltip and screen-reader name.// label: what screen readers call it ("Main").import ui/theme.qwe use ui_clslet bottom_nav_css = css`.ui-bottomnav { display: flex; justify-content: space-around; align-items: stretch; width: 100%; min-height: 56px; padding: 4px 8px max(4px, env(safe-area-inset-bottom)); border-top: 1px solid var(--ui-border); background: var(--ui-bg); color: var(--ui-fg); font: 11px/1.2 var(--ui-font);}.ui-bottomnav-fixed { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; box-shadow: var(--ui-shadow); }.ui-bottomnav-item { position: relative; display: flex; flex: 1 1 0; flex-direction: column; align-items: center; justify-content: center; gap: 3px; max-width: 120px; min-width: 48px; padding: 6px 4px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); font: inherit; font-weight: 500; text-decoration: none; cursor: pointer; transition: color .12s;}.ui-bottomnav-item:hover { color: var(--ui-fg); }.ui-bottomnav-item:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bottomnav-item[aria-current="page"] { color: var(--ui-accent); }.ui-bottomnav-icon { position: relative; display: inline-grid; place-items: center; width: 28px; height: 28px; font-size: 20px; line-height: 1; border-radius: 999px; transition: background .12s; }.ui-bottomnav-icon svg { width: 24px; height: 24px; }.ui-bottomnav-item[aria-current="page"] .ui-bottomnav-icon { background: var(--ui-accent-soft); }.ui-bottomnav-badge { position: absolute; top: -4px; left: 100%; margin-left: -10px; min-width: 16px; padding: 0 4px; border-radius: 999px; background: var(--ui-danger); color: #fff; font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;}.ui-bottomnav-text { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-bottomnav-bare .ui-bottomnav-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;}@media (min-width: 720px) { .ui-bottomnav-fixed:not(.ui-bottomnav-wide) { display: none; } }@media (prefers-reduced-motion: reduce) { .ui-bottomnav-item, .ui-bottomnav-icon { transition: none; } }`fn bottom_nav_body(it, tips: bool) = [ <span.ui-bottomnav-icon aria-hidden="true"> {starts_with(it.icon, "M") && len(it.icon) > 2 ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={it.icon}/></svg> : it.icon} {get(it, "badge") != null && <span.ui-bottomnav-badge>{it.badge}</span>} </span>, <span.ui-bottomnav-text>{it.label}{get(it, "badge") != null && <span.ui-sr>, {it.badge} new</span>}</span>,]view BottomNav(items: list = [], current: str? = null, label: str = "Main", fixed: bool = true, wide: bool = false, labels: bool = true, id: str? = null, on_select = null, class: str? = null) { style global bottom_nav_css <nav class={ui_cls(["ui-bottomnav", fixed && "ui-bottomnav-fixed", wide && "ui-bottomnav-wide", !labels && "ui-bottomnav-bare", class])} id={id} aria-label={label}> {for it in items => get(it, "href") != null ? <a.ui-bottomnav-item href={it.href} title={labels ? null : it.label} aria-current={current != null && it.href == current ? "page" : null}>{bottom_nav_body(it, !labels)}</a> : <button.ui-bottomnav-item type="button" title={labels ? null : it.label} aria-current={current != null && it.label == current ? "true" : null} @click={() => { on_select && on_select(it) }}>{bottom_nav_body(it, !labels)}</button>} </nav>}Related
IconTabsTabs with an icon above each label and a count on its corner, the arrow keys moving between them.5 views · 6 downloadsScrollTabsA row of tabs that scrolls sideways when there are more than fit, with arrows at the ends and the keyboard.5 views · 8 downloadsFilterTabsTabs with counts that filter a list, one selected, moved between with the arrow keys; a hidden input holds the key.5 views · 6 downloadsMobileNavA menu button that opens the site's navigation over the whole screen, as a dialog: groups, the current page, actions at the bottom.5 views · 6 downloadsNotificationBellA bell with the unread count on its corner: a link, or a button that opens any panel.5 views · 8 downloadsSubnavA second row of links under the main navigation, for one section's pages, with counts and the current one marked.5 views · 5 downloads
Thread
Thread 0 posts
On BottomNav: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.