Mobile

StickyBuyBar

Price and Buy stuck above the safe area once the page's button scrolls away, hiding near the on-screen keyboard.

#mobile#interactive#commerce#sticky#buy

Demo

Preview

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

Thread 0 posts

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

No posts yet. The first one opens the thread.