Snackbar
A short message along the bottom of the window with one action, shown from anywhere by calling snackbar_show().
Demo
Archived 0, restored 0. Press the buttons quickly: each message waits its turn.
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/snackbar.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/snackbar.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/snackbar.qwe
component SnackbarDemoLive() { state archived = 0 state restored = 0 <div.demo-stack> <div.demo-row> <Button id="snackbar-demo-archive" variant="outline" on_click={() => { archived += 1 snackbar_show("Message archived", { action: "Undo", on_action: () => { restored += 1 } }) }}>Archive a message</Button> <Button id="snackbar-demo-saved" variant="outline" on_click={() => { snackbar_show("Draft saved", { tone: "success", seconds: 2 }) }}>Save a draft</Button> <Button id="snackbar-demo-failed" variant="outline" tone="danger" on_click={() => { snackbar_show("Could not send. Check the connection and try again.", { tone: "danger", seconds: 0 }) }}>Fail to send</Button> </div> <p.demo-note>Archived {archived}, restored {restored}. Press the buttons quickly: each message waits its turn.</p> </div>}view SnackbarDemo() = <div.demo-stack> <Snackbar/> <SnackbarDemoLive/> </div>Source
ui/snackbar.qwe
// Snackbar: a short message along the bottom of the window with one action// ("Undo"), shown by calling a function from anywhere on the page. Put one// <Snackbar/> on the page; then, in any component's handler://// <Snackbar/>// snackbar_show("Message archived", { action: "Undo", on_action: () => { restore(id) } })// snackbar_show("Could not send. Try again.", { tone: "danger", seconds: 0 })//// snackbar_show(message, options): message is one short line. options: action// (the button's text, none by default), on_action() (called with nothing when the// button is pressed; the snackbar then closes), seconds (how long it stays: 5; 0// keeps it until closed) and tone ("neutral" (default), "success", "warning",// "danger" or "info"). A message shown while one is up waits its turn.// Snackbar: position "bottom" (default, centred), "bottom-left" or "bottom-right";// seconds: the default stay (5); dismissable: a close button (true); label: what// screen readers call the region ("Notifications"). Each message is announced// (role="status"), and Escape closes the one showing while it has the focus.import ui/theme.qwe use ui_clslet snackbar_css = css`.ui-snackbar-host { position: fixed; z-index: 70; display: grid; justify-items: center; pointer-events: none; left: 16px; right: 16px; bottom: max(16px, env(safe-area-inset-bottom)); font: 14px/1.45 var(--ui-font); }.ui-snackbar-bottom-left { justify-items: start; }.ui-snackbar-bottom-right { justify-items: end; }.ui-snackbar { display: flex; align-items: center; gap: 12px; width: max-content; max-width: min(560px, 100%); min-height: 48px; padding: 10px 8px 10px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-fg); color: var(--ui-bg); box-shadow: var(--ui-shadow-lg); pointer-events: auto; animation: ui-snackbar-in .2s ease-out; --t: var(--ui-bg);}.ui-snackbar-success { --t: var(--ui-success); }.ui-snackbar-warning { --t: var(--ui-warning); }.ui-snackbar-danger { --t: var(--ui-danger); }.ui-snackbar-info { --t: var(--ui-info); }.ui-snackbar:not(.ui-snackbar-neutral) { box-shadow: var(--ui-shadow-lg), inset 4px 0 0 var(--t); }.ui-snackbar-text { flex: 1; min-width: 0; margin: 0; }.ui-snackbar-action { flex: none; height: 32px; padding: 0 12px; border: 0; border-radius: var(--ui-radius-sm); background: color-mix(in srgb, var(--ui-bg) 14%, transparent); color: inherit; font: 600 13px/1 var(--ui-font); cursor: pointer;}.ui-snackbar:not(.ui-snackbar-neutral) .ui-snackbar-action { color: color-mix(in srgb, var(--t) 70%, var(--ui-bg)); }.ui-snackbar-action:hover { background: color-mix(in srgb, var(--ui-bg) 24%, transparent); }.ui-snackbar-x { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: inherit; font-size: 20px; line-height: 1; opacity: .7; cursor: pointer;}.ui-snackbar-x:hover { opacity: 1; background: color-mix(in srgb, var(--ui-bg) 14%, transparent); }.ui-snackbar-action:focus-visible, .ui-snackbar-x:focus-visible { outline: 2px solid var(--ui-bg); outline-offset: 1px; }@keyframes ui-snackbar-in { from { opacity: 0; transform: translateY(12px); } }@media (prefers-reduced-motion: reduce) { .ui-snackbar { animation: none; } }@media (max-width: 480px) { .ui-snackbar { width: 100%; } }`// Shows a snackbar: the Snackbar on the page hears this and shows the message.fn snackbar_show(message: str, options: map = {}) { js("document.dispatchEvent", js_new("CustomEvent", "ui-snackbar", { detail: { message, ...options } }))}component Snackbar(position: str = "bottom", seconds: int = 5, dismissable: bool = true, label: str = "Notifications", class: str? = null) { style global snackbar_css state queue = [] state current = null state made = 0 let stay = current != null ? (current.seconds ?? seconds) : 0 let close = () => { let next = first(queue) if next != null { queue = drop(queue, 1) } current = next } if current != null && stay > 0 { every(stay * 1000, close) } <div class={ui_cls(["ui-snackbar-host", `ui-snackbar-${position}`, class])} role="status" aria-label={label} @mount={(el) => { let hear = (e) => { let d = e.detail made += 1 let item = { n: made, message: str(d.message ?? ""), action: d.action, on_action: d.on_action, seconds: d.seconds, tone: d.tone ?? "neutral" } if current == null { current = item } else { queue = push(queue, item) } } js("document.addEventListener", "ui-snackbar", hear) () => js("document.removeEventListener", "ui-snackbar", hear) }}> {current != null && <div class={ui_cls(["ui-snackbar", `ui-snackbar-${current.tone}`])} key={current.n} @keydown={(e) => { if e.key == "Escape" { close() } }}> <p.ui-snackbar-text>{current.message}</p> {current.action != null && <button.ui-snackbar-action type="button" @click={() => { if current.on_action != null { current.on_action() } close() }}>{current.action}</button>} {dismissable && <button.ui-snackbar-x type="button" aria-label="Dismiss" @click={close}>×</button>} </div>} </div>}Related
ConnectionStatusSays when the browser goes offline, as a bar across the top or a pill, and that it is back online for a moment.6 views · 7 downloadsNotificationDotA small dot or a count on the corner of anything, in tones and four positions, with a label for screen readers.5 views · 7 downloadsAlertA message in the page in six tones and three variants, with an icon, announced to screen readers, and dismissable.5 views · 9 downloadsBadgeA small pill for a status or a count: six tones, soft, solid or outline, a status dot, and 99+.8 views · 6 downloadsBannerAn announcement across the top of the page in six tones, with a link, closed for the session once dismissed.7 views · 6 downloadsCoachmarkA small callout pointing at an element by id, explaining it once, dismissed with Got it; several make a tour.6 views · 8 downloads
Thread
Thread 0 posts
On Snackbar: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.