Fab
A round floating action button fixed to a corner of the screen, with an icon and an optional label.
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/fab.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/fab.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/fab.qwe
component FabDemoLive() { state calls = 0 <div.demo-stack> <div style="position: relative; height: 120px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius)"> <Fab id="fab-demo-call" label="Call us" icon="phone" tone="neutral" inline={true} on_click={() => { calls += 1 }}/> </div> <p.demo-note>{calls == 0 ? "Not called yet." : `Called ${calls} ${calls == 1 ? "time" : "times"}.`}</p> </div>}view FabDemo() = <div.demo-stack> <div style="position: relative; height: 150px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius)"> <Fab id="fab-demo-new" label="New message" icon="edit" href="#fab" inline={true}/> <Fab id="fab-demo-add" label="Add" extended={true} corner="bottom-left" inline={true}/> <Fab label="Search" icon="search" size="sm" corner="top-right" inline={true}/> <Fab label="Delete" icon="x" tone="danger" size="lg" corner="top-left" inline={true}/> </div> <p.demo-note>These are inline, in the corners of the box above; without inline they sit in the corners of the screen.</p> <FabDemoLive/> </div>Source
ui/fab.qwe
// Fab: a floating action button, round, fixed to a corner of the screen, for the one// thing a page is mostly for: write, add, call. An icon, and a label beside it when// extended; the label is always its name for screen readers and its tooltip.//// <Fab label="New message" icon="edit" href="/messages/new"/>// <Fab label="Add" extended={true} corner="bottom-left"/>// <Fab label="Call us" icon="phone" tone="neutral" on_click={() => { calling = true }}/>//// label (required): its name; shown beside the icon when extended.// icon: "plus" (default), "edit", "message", "search", "phone", "mail", "share", "heart",// "up", "check" or "x"; or icon={null} and your own <svg> as children.// href makes it a link; else on_click(), inside one of your components.// corner: "bottom-right" (default), "bottom-left", "top-right" or "top-left".// inline: not fixed to the screen but to the corner of its nearest positioned ancestor// (a panel with `position: relative`), as in the gallery.// tone: "accent" (default), "neutral" or "danger". size: "sm", "md" (default) or "lg".// id: the element's id, for a test or a link to it.import ui/theme.qwe use ui_clslet fab_css = css`.ui-fab { position: fixed; z-index: 40; display: inline-flex; align-items: center; justify-content: center; gap: 8px; width: 56px; height: 56px; padding: 0; border: 0; border-radius: 999px; background: var(--b); color: var(--bf); box-shadow: var(--ui-shadow-lg); font: 600 15px/1 var(--ui-font); text-decoration: none; white-space: nowrap; cursor: pointer; transition: transform .15s, box-shadow .15s, background .15s; --b: var(--ui-accent); --bf: var(--ui-accent-fg);}.ui-fab-bottom-right { right: max(20px, env(safe-area-inset-right)); bottom: max(20px, env(safe-area-inset-bottom)); }.ui-fab-bottom-left { left: max(20px, env(safe-area-inset-left)); bottom: max(20px, env(safe-area-inset-bottom)); }.ui-fab-top-right { right: max(20px, env(safe-area-inset-right)); top: max(20px, env(safe-area-inset-top)); }.ui-fab-top-left { left: max(20px, env(safe-area-inset-left)); top: max(20px, env(safe-area-inset-top)); }.ui-fab-inline { position: absolute; z-index: 2; }.ui-fab-inline.ui-fab-bottom-right { right: 16px; bottom: 16px; }.ui-fab-inline.ui-fab-bottom-left { left: 16px; bottom: 16px; }.ui-fab-inline.ui-fab-top-right { right: 16px; top: 16px; }.ui-fab-inline.ui-fab-top-left { left: 16px; top: 16px; }.ui-fab-neutral { --b: var(--ui-fg); --bf: var(--ui-bg); }.ui-fab-danger { --b: var(--ui-danger); --bf: #fff; }.ui-fab:hover { background: color-mix(in srgb, var(--b) 88%, var(--ui-fg)); transform: translateY(-1px); }.ui-fab:active { transform: none; }.ui-fab:focus-visible { outline: none; box-shadow: var(--ui-shadow-lg), var(--ui-focus); }.ui-fab[aria-disabled="true"] { opacity: .55; cursor: not-allowed; pointer-events: none; }.ui-fab svg { flex: none; width: 24px; height: 24px; }.ui-fab-extended { width: auto; padding: 0 20px 0 16px; }.ui-fab-sm { width: 44px; height: 44px; font-size: 14px; }.ui-fab-sm svg { width: 20px; height: 20px; }.ui-fab-sm.ui-fab-extended { padding: 0 16px 0 12px; }.ui-fab-lg { width: 68px; height: 68px; font-size: 16px; }.ui-fab-lg svg { width: 28px; height: 28px; }.ui-fab-lg.ui-fab-extended { padding: 0 26px 0 20px; }@media (prefers-reduced-motion: reduce) { .ui-fab { transition: none; } .ui-fab:hover { transform: none; } }`// The path of a built-in icon (24 by 24, drawn with a 2px line), or "" for a name it does not know.fn fab_path(name: str) = match name { "plus" => "M12 5v14M5 12h14" "edit" => "M12 20h9M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" "message" => "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" "search" => "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM21 21l-4.3-4.3" "phone" => "M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.8.7a2 2 0 0 1 1.7 2Z" "mail" => "M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2ZM22 7l-10 7L2 7" "share" => "M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13" "heart" => "M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8Z" "up" => "M12 19V5M5 12l7-7 7 7" "check" => "M20 6 9 17l-5-5" "x" => "M18 6 6 18M6 6l12 12" _ => "" }view Fab(label: str, icon: str? = "plus", href: str? = null, extended: bool = false, corner: str = "bottom-right", inline: bool = false, tone: str = "accent", size: str = "md", disabled: bool = false, id: str? = null, on_click = null, class: str? = null, children) { style global fab_css let cls = ui_cls(["ui-fab", `ui-fab-${corner}`, inline && "ui-fab-inline", extended && "ui-fab-extended", tone != "accent" && `ui-fab-${tone}`, size != "md" && `ui-fab-${size}`, class]) let body = [ icon != null ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={fab_path(icon)}/></svg> : children, extended && <span>{label}</span>, ] href != null ? <a class={cls} id={id} href={href} aria-label={extended ? null : label} title={label} aria-disabled={disabled ? "true" : null}>{body}</a> : <button class={cls} id={id} type="button" aria-label={extended ? null : label} title={label} disabled={disabled} @click={() => { on_click && on_click() }}>{body}</button>}Related
BookmarkButtonA save-for-later toggle whose bookmark fills when pressed, with or without its label.8 views · 8 downloadsButtonA button, or a link that looks like one: four variants, three tones, three sizes, loading.7 views · 7 downloadsButtonGroupButtons joined edge to edge into one control, or spaced in a row; across or upright.6 views · 6 downloadsFollowButtonFollow becomes Following once pressed, and says Unfollow on hover, as the social sites do.5 views · 7 downloadsIconButtonA square button with an icon, labelled for screen readers and on hover; built-in icons or your own.6 views · 9 downloadsLikeButtonA heart that toggles with a count that follows it, a real toggle for screen readers and forms.5 views · 4 downloads
Thread
Thread 0 posts
On Fab: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.