IconTabs
Tabs with an icon above each label and a count on its corner, the arrow keys moving between them.
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/icon_tabs.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/icon_tabs.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/icon_tabs.qwe
let icon_tabs_demo_tabs = [ { label: "Photos", icon: "M4 5h16v14H4zM4 15l5-5 4 4 3-3 4 4" }, { label: "Albums", icon: "M4 6h16v14H4zM8 3h8M8 10h8", badge: 3 }, { label: "Shared", icon: "M16 19v-1a4 4 0 0 0-8 0v1M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6M20 19v-1a3 3 0 0 0-2-2.8" }, { label: "Trash", icon: "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13", disabled: true }]component IconTabsDemoLive() { state tab = 0 <div.demo-stack> <IconTabs id="icon-tabs-demo" label="Library" tabs={icon_tabs_demo_tabs} selected={tab} on_change={(i) => { tab = i }}> <p.demo-note>{icon_tabs_demo_tabs[tab].label}: the panel for tab {tab + 1} of 4, from state.</p> </IconTabs> </div>}component IconTabsDemoFull() { state tab = 1 <div style="width: min(360px, 100%)"> <IconTabs id="icon-tabs-demo-full" full={true} size="sm" label="Emoji" selected={tab} on_change={(i) => { tab = i }} tabs={[{ label: "Feed", icon: "📰" }, { label: "Explore", icon: "🧭" }, { label: "Saved", icon: "🔖", badge: 12 }]}> <p.demo-note>Tab {tab + 1} of 3, the tabs sharing the width.</p> </IconTabs> </div>}view IconTabsDemo() = <div.demo-stack style="gap: 28px"> <IconTabsDemoLive/> <IconTabsDemoFull/> </div>Source
ui/icon_tabs.qwe
// IconTabs: a row of tabs with an icon above each label, for the main areas of an// app or a phone-like switcher; the selected one is marked, and the arrow keys move// between them. Real ARIA tabs driven by your component's state, with one panel:// the children.//// <IconTabs id="areas" selected={tab} on_change={(i) => { tab = i }} tabs={[// { label: "Photos", icon: "M4 5h16v14H4zM4 15l5-5 4 4 3-3 4 4" },// { label: "Albums", icon: "📚", badge: 3 },// { label: "Shared", icon: "👥" }// ]}>// {tab == 0 ? <Photos/> : tab == 1 ? <Albums/> : <Shared/>}// </IconTabs>//// id (required): a prefix for the tabs' ids, unique on the page. tabs: { label, icon,// badge?, disabled? }. 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. selected: the index// showing first. children: the panel of the selected tab. full: the tabs share the// width. size: "sm" or "md" (default). label: what screen readers call the row.// on_change(index): called with the tab picked.import ui/theme.qwe use ui_clslet icon_tabs_css = css`.ui-icontabs { font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-icontabs-list { display: flex; gap: 4px; margin: 0; padding: 0; border-bottom: 1px solid var(--ui-border); }.ui-icontabs-full .ui-icontabs-list .ui-icontabs-tab { flex: 1 1 0; }.ui-icontabs-tab { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; min-width: 72px; padding: 10px 12px 9px; margin-bottom: -1px; border: 0; border-bottom: 2px solid transparent; border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0; background: transparent; color: var(--ui-muted); font: 500 13px/1 var(--ui-font); cursor: pointer; transition: color .12s, background .12s, border-color .12s;}.ui-icontabs-tab:hover { color: var(--ui-fg); background: var(--ui-subtle); }.ui-icontabs-tab:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-icontabs-tab[aria-selected="true"] { color: var(--ui-accent); border-bottom-color: var(--ui-accent); }.ui-icontabs-tab:disabled { opacity: .5; cursor: not-allowed; }.ui-icontabs-icon { position: relative; display: inline-grid; place-items: center; width: 28px; height: 28px; font-size: 22px; line-height: 1; }.ui-icontabs-icon svg { width: 24px; height: 24px; }.ui-icontabs-badge { position: absolute; top: -5px; 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-icontabs-sm .ui-icontabs-tab { min-width: 56px; padding: 7px 8px 6px; font-size: 12px; }.ui-icontabs-sm .ui-icontabs-icon { width: 22px; height: 22px; font-size: 18px; }.ui-icontabs-sm .ui-icontabs-icon svg { width: 20px; height: 20px; }.ui-icontabs-panel { padding-top: 16px; }.ui-icontabs-panel > :first-child { margin-top: 0; }.ui-icontabs-panel > :last-child { margin-bottom: 0; }@media (prefers-reduced-motion: reduce) { .ui-icontabs-tab { transition: none; } }`component IconTabs(id: str, tabs: list = [], selected: int = 0, full: bool = false, size: str = "md", label: str? = null, on_change = null, class: str? = null, children) { style global icon_tabs_css state at = selected let live = range(0, len(tabs)) | filter((i) => get(tabs[i], "disabled") != true) let pick = (i: int) => { at = i on_change && on_change(i) } <div class={ui_cls(["ui-icontabs", full && "ui-icontabs-full", size == "sm" && "ui-icontabs-sm", class])}> <div.ui-icontabs-list role="tablist" aria-label={label} @keydown={(e) => { // Taken first: pick may wait, as it calls on_change, and after a wait the event has no currentTarget. let bar = e.currentTarget let n = len(live) let k = index_of(live, at) ?? 0 let to = match e.key { "ArrowRight" => live[(k + 1) % n] "ArrowLeft" => live[(k + n - 1) % n] "Home" => live[0] "End" => live[n - 1] _ => null } if to != null && n > 0 { e.preventDefault() pick(to) bar.querySelector(`[data-i="${to}"]`).focus() } }}> {for [i, t] in enumerate(tabs) => <button.ui-icontabs-tab type="button" role="tab" id={`${id}-tab-${i}`} data-i={i} aria-selected={i == at ? "true" : "false"} aria-controls={`${id}-panel`} tabindex={i == at ? "0" : "-1"} disabled={get(t, "disabled") == true} @click={() => { pick(i) }}> <span.ui-icontabs-icon aria-hidden="true"> {starts_with(t.icon, "M") && len(t.icon) > 2 ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={t.icon}/></svg> : t.icon} {get(t, "badge") != null && <span.ui-icontabs-badge>{t.badge}</span>} </span> <span>{t.label}{get(t, "badge") != null && <span.ui-sr>, {t.badge} new</span>}</span> </button>} </div> <div.ui-icontabs-panel id={`${id}-panel`} role="tabpanel" aria-labelledby={`${id}-tab-${at}`} tabindex="0">{children}</div> </div>}Related
BottomNavA fixed bar of icon tabs along the bottom of a phone's screen, with counts and the current one marked.6 views · 7 downloadsFilterTabsTabs with counts that filter a list, one selected, moved between with the arrow keys; a hidden input holds the key.5 views · 6 downloadsNotificationBellA bell with the unread count on its corner: a link, or a button that opens any panel.5 views · 8 downloadsScrollTabsA row of tabs that scrolls sideways when there are more than fit, with arrows at the ends and the keyboard.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 downloadsTabsPanels behind a row of tabs, from radio buttons and CSS: no JavaScript, any HTML from the server.5 views · 7 downloads
Thread
Thread 0 posts
On IconTabs: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.