Navigation

BottomNav

A fixed bar of icon tabs along the bottom of a phone's screen, with counts and the current one marked.

#navigation#mobile#tabs#icon#no-js

Demo

Preview

A phone's screen; the bar stays at its bottom.

Showing: Feed

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.qwe
main.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>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On BottomNav: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.