StickyBuyBar
Price and Buy stuck above the safe area once the page's button scrolls away, hiding near the on-screen keyboard.
Demo
Trail boots
Waterproof leather with a grippy sole, made to last. Bought 0 so far.
Scroll down: when the button above is gone, the bar rises.
Details, sizing and reviews go here.
Trail boots, waterproof leather$129.00was $149.00
Get it
It needs theme.qwe, as every component does, and price.qwe; 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/price.qwe$ curl -O https://quillolang.org/components/ui/sticky_buy_bar.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/sticky_buy_bar.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/sticky_buy_bar.qwe
component StickyBuyBarDemoLive() { state bought = 0 <div.demo-tablet> <div id="sticky-demo-page" style="height: 100%; overflow: auto; padding: 16px; box-sizing: border-box; overscroll-behavior: contain"> <h3 style="margin: 0 0 4px">Trail boots</h3> <p.demo-note>Waterproof leather with a grippy sole, made to last. Bought {bought} so far.</p> <p style="margin: 12px 0"><Button id="sticky-demo-buy" size="lg" on_click={() => { bought += 1 }}>Buy now</Button></p> <div style="height: 760px; display: grid; align-content: space-between; color: var(--ui-muted)"> <p>Scroll down: when the button above is gone, the bar rises.</p> <p>Details, sizing and reviews go here.</p> </div> <label for="sticky-demo-note" style="display: grid; gap: 4px">Gift note <input id="sticky-demo-note" style="height: 46px; font-size: 16px; box-sizing: border-box" placeholder="Touch to type"/> </label> <div style="height: 320px"></div> </div> <StickyBuyBar watch="sticky-demo-buy" name="Trail boots, waterproof leather" price={dec("129.00")} old={dec("149.00")} on_buy={() => { bought += 1 }}/> </div>}view StickyBuyBarDemo() = <StickyBuyBarDemoLive/>Source
ui/sticky_buy_bar.qwe
// StickyBuyBar: the price and a Buy button, stuck to the bottom of the screen above the// safe area, which come up once the page's own Buy button has scrolled away. They go// again when that button is back, and while someone is typing, so the bar is not left// hanging over the on-screen keyboard. On a wide screen it also names the product.//// import ui/sticky_buy_bar.qwe use StickyBuyBar//// <Button id="buy">Buy now</Button>// ...// <StickyBuyBar watch="buy" name="Trail boots" price={dec("129.00")} old={dec("149.00")} href="/cart/add/12"/>// <StickyBuyBar watch="buy" price={total} on_buy={() => { add() }} above={56}/>//// watch: the id of the page's own button. The bar shows only after that has gone off the// top: not while it is still to come. With no watch, or an id not on the page, the bar is// always up. price, old (the price before): a decimal, formatted for the currency and// locale, or text, shown as it is. currency ("USD"), locale.// name: the product, shown from 600 px across. label: the button's words ("Buy now").// href: the button is this link; else on_buy() is called. disabled: the button is off.// above: pixels to stay above something fixed, such as a tab bar (0).//// Without a script it stays hidden, and the page's own button does the work.import ui/theme.qwe use ui_clsimport ui/button.qwe use Buttonimport ui/price.qwe use Pricelet sticky_buy_bar_css = css`.ui-sbb { position: fixed; inset: auto 0 var(--ui-sbb-above, 0px) 0; z-index: 40; box-sizing: border-box; container-type: inline-size; padding: 8px max(16px, env(safe-area-inset-right, 0px)) calc(8px + env(safe-area-inset-bottom, 0px)) max(16px, env(safe-area-inset-left, 0px)); border-top: 1px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 15px/1.3 var(--ui-font); transform: translateY(calc(100% + 12px)); visibility: hidden; transition: transform .22s cubic-bezier(.2, .8, .2, 1), visibility .22s;}.ui-sbb-up { transform: none; visibility: visible; }.ui-sbb-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; max-width: 760px; margin: 0 auto; }.ui-sbb-what { display: grid; min-width: 0; }.ui-sbb-name { display: none; overflow: hidden; color: var(--ui-muted); font-size: 13px; white-space: nowrap; text-overflow: ellipsis; }.ui-sbb-buy { flex: 1 1 auto; max-width: 240px; }@container (min-width: 600px) { .ui-sbb-name { display: block; } }@media (prefers-reduced-motion: reduce) { .ui-sbb { transition: none; } }`// Whether the keyboard is up: the visual viewport is at least 120 px less than the tallest// the page has been. The tallest is needed because Android's web view may shrink the whole// page, innerHeight with it; the scale, because a pinch shrinks the viewport too.fn sticky_buy_bar_keyboard(tallest, height, scale) -> bool = tallest - height * scale >= 120// Whether an element takes typing, so a keyboard is coming: iOS tells nothing until it is up.fn sticky_buy_bar_typing(tag, kind, editable) -> bool { if editable == true || tag == "TEXTAREA" { return true } tag == "INPUT" && !contains(["button", "checkbox", "radio", "range", "color", "file", "submit", "reset", "image"], lower(str(kind ?? "text")))}// Whether the page's button has gone past the top: out of view, and above the bar.fn sticky_buy_bar_gone(in_view: bool, bottom, bar_top) -> bool = !in_view && bottom <= bar_topcomponent StickyBuyBar(watch: str? = null, name: str? = null, price = null, old = null, currency: str = "USD", locale: str? = null, label: str = "Buy now", href: str? = null, disabled: bool = false, above: int = 0, on_buy = null) { style global sticky_buy_bar_css state past = false // the page's button has gone off the top state typing = false // the on-screen keyboard is up, or about to be state tallest = 0 state wide = 0 let up = past && !typing <div class={ui_cls(["ui-sbb", up && "ui-sbb-up"])} inert={!up} role="region" aria-label="Buy" style={above > 0 ? `--ui-sbb-above: ${above}px` : null} @mount={(el) => { let w = js("window") let d = js("document") let target = watch != null ? d.getElementById(watch) : null if target == null { past = true } let seen = js_new("IntersectionObserver", (es) => { let e = es[len(es) - 1] past = sticky_buy_bar_gone(e.isIntersecting, e.boundingClientRect.bottom, el.getBoundingClientRect().top) }, { threshold: [0] }) if target != null { seen.observe(target) } let vv = w.visualViewport tallest = w.innerHeight wide = w.innerWidth let look = (_e = null) => { // A turn of the phone is a new height, not a keyboard. if w.innerWidth != wide { wide = w.innerWidth tallest = w.innerHeight } tallest = max([tallest, w.innerHeight]) let f = d.activeElement let field = f != null && sticky_buy_bar_typing(f.tagName, f.getAttribute("type"), f.isContentEditable) typing = field || (vv != null && sticky_buy_bar_keyboard(tallest, vv.height, vv.scale)) } d.addEventListener("focusin", look) d.addEventListener("focusout", look) if vv != null { vv.addEventListener("resize", look) } w.addEventListener("resize", look) () => { seen.disconnect() d.removeEventListener("focusin", look) d.removeEventListener("focusout", look) if vv != null { vv.removeEventListener("resize", look) } w.removeEventListener("resize", look) } }}> <div.ui-sbb-row> <div.ui-sbb-what> {name != null && <span.ui-sbb-name>{name}</span>} {price != null && <Price amount={price} old={old} currency={currency} locale={locale} size="lg" discount={false}/>} </div> <Button class="ui-sbb-buy" size="lg" href={href} disabled={disabled} on_click={() => { on_buy && on_buy() }}>{label}</Button> </div> </div>}Related
CartSheetA cart in a sheet with quantities, swipe to remove with undo, live totals in decimals, a free-shipping note and a checkout button.2 views · 9 downloadsCheckoutSheetA one-screen checkout in a sheet with address, delivery, payment and a live total, every part validated, and a slide to pay.2 views · 6 downloadsFilterSheetFacets in a sheet, with result counts updating live, clear, apply, the count on the button, and a state kept for next time.3 views · 5 downloadsOrderStatusCardThe stages of an order with a progress bar, an estimate from data, and what to do when it is late.3 views · 6 downloadsPayCodeA QR or code to pay with, refreshing on a timer, screen brightness up while shown, locked until confirmed.2 views · 6 downloadsPosRegisterA tablet till: a product grid, ticket, quantity and discount, cash and card, in decimals, keeping the sale through a reload.2 views · 9 downloads
Thread
Thread 0 posts
On StickyBuyBar: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.