CookieConsent
A cookie banner with accept, reject and settings by category, remembering the choice in localStorage.
Demo
No choice made yet.
Positioned across the bottom of the window by default; inline here so you can see it. The choice is kept in localStorage: these two forget it when the page loads, so they show again.
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/cookie_consent.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/cookie_consent.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/cookie_consent.qwe
component CookieConsentDemoLive() { state choice = null <div.demo-stack> <CookieConsent name="demo-live" position="inline" policy="#cookie_consent" on_change={(c) => { choice = c }}/> <p.demo-note>{choice == null ? "No choice made yet." : `Analytics ${choice.analytics ? "on" : "off"}, marketing ${choice.marketing ? "on" : "off"}.`}</p> </div>}view CookieConsentDemo() = <div.demo-stack> <CookieConsentDemoLive/> <CookieConsent name="demo-short" position="inline" title={null} accept="OK" reject="No thanks" settings="Choose" text="This site uses a few cookies to work, and one to count visits." categories={[{ key: "necessary", label: "Necessary", required: true }, { key: "stats", label: "Statistics", text: "Page views, counted anonymously." }]}/> <p.demo-note>Positioned across the bottom of the window by default; inline here so you can see it. The choice is kept in localStorage: these two forget it when the page loads, so they show again.</p> </div>Source
ui/cookie_consent.qwe
// CookieConsent: the cookie banner: accept all, reject all, or open the settings// and pick by category. The choice is kept in localStorage, so once made the// banner stays away, on every page that shows it.//// <CookieConsent policy="/privacy"/>// <CookieConsent name="v2" title="We use cookies" text="A few to sign you in, and some to see how the site is used."// categories={[{ key: "analytics", label: "Analytics", text: "How the site is used, anonymously." },// { key: "marketing", label: "Marketing", text: "Ads that match your interests." }]}// on_change={(choices) => { if choices.analytics { start_analytics() } }}/>//// Then anywhere on the page, cookie_consent_allowed("analytics") says whether// that kind was allowed (false until a choice is made).//// name: a name for the choice ("default"); change it to ask again (a new// policy). title; text (a sentence by default); children: more HTML under the// text. policy: a link's href ("Cookie policy"). categories: maps of { key, label, text?, required? };// by default Necessary (required), Analytics and Marketing. Necessary ones are// always on. accept, reject, settings, save: the buttons' words. position:// "bottom" (default, fixed across the window's bottom), "bottom-left" (a card in// the corner) or "inline" (in the flow of the page). It is a region named// label ("Cookie consent"). on_change(choices): inside one of your components,// called with a map of each key to true or false when a choice is made.// It shows until a choice is kept; on the page's next load it does not show at// all, so the choice works for every visit.import ui/theme.qwe use ui_clslet cookie_consent_css = css`.ui-cookie-host { display: contents; }.ui-cookie { display: grid; gap: 12px; padding: 16px 20px; border: 1px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); box-shadow: var(--ui-shadow-lg); }.ui-cookie-bottom { position: fixed; left: 0; right: 0; bottom: 0; z-index: 60; border-width: 1px 0 0; }.ui-cookie-bottom-left { position: fixed; left: 16px; bottom: 16px; z-index: 60; width: min(420px, calc(100vw - 32px)); border-radius: var(--ui-radius); }.ui-cookie-inline { border-radius: var(--ui-radius); box-shadow: none; }.ui-cookie-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 24px; }.ui-cookie-bottom .ui-cookie-row { max-width: 1100px; margin: 0 auto; width: 100%; }.ui-cookie-body { flex: 1 1 320px; min-width: 0; }.ui-cookie-title { margin: 0 0 2px; font-weight: 600; }.ui-cookie-text { margin: 0; color: var(--ui-muted); }.ui-cookie-policy { margin-left: 4px; }.ui-cookie-text a { color: var(--ui-fg); font-weight: 500; text-underline-offset: 2px; }.ui-cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; }.ui-cookie-btn { display: inline-flex; align-items: center; height: 36px; padding: 0 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 14px/1 var(--ui-font); cursor: pointer; white-space: nowrap; }.ui-cookie-btn:hover { background: var(--ui-subtle); }.ui-cookie-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-cookie-main { background: var(--ui-accent); border-color: transparent; color: var(--ui-accent-fg); }.ui-cookie-main:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-cookie-settings { display: grid; gap: 10px; padding-top: 12px; border-top: 1px solid var(--ui-border); }.ui-cookie-bottom .ui-cookie-settings { max-width: 1100px; width: 100%; margin: 0 auto; }.ui-cookie-cat { display: flex; align-items: flex-start; gap: 10px; }.ui-cookie-cat input { margin: 3px 0 0; accent-color: var(--ui-accent); }.ui-cookie-cat-label { font-weight: 500; }.ui-cookie-cat-text { color: var(--ui-muted); font-size: 13px; }.ui-cookie-req { margin-left: 6px; color: var(--ui-muted); font-size: 12px; font-weight: 400; }`let cookie_consent_default = [ { key: "necessary", label: "Necessary", text: "Signing in, your basket, and what you have chosen here.", required: true }, { key: "analytics", label: "Analytics", text: "How the site is used, counted without knowing who you are." }, { key: "marketing", label: "Marketing", text: "Ads and offers that match what you looked at." },]// The choices kept for `name`, as a map, or null when none was made yet.fn cookie_consent_choices(name: str = "default") -> map? { let saved = js("localStorage.getItem", `ui-cookies:${name}`) saved != null ? (json.parse(saved) ?? null) : null}// Whether the kind of cookie `key` was allowed (false until a choice is made).fn cookie_consent_allowed(key: str, name: str = "default") -> bool = (cookie_consent_choices(name) ?? {})[key] == truecomponent CookieConsent(name: str = "default", title: str? = "Cookies", text: str = "We use cookies to keep you signed in and to see how the site is used.", policy: str? = null, categories: list? = null, accept: str = "Accept all", reject: str = "Only necessary", settings: str = "Settings", save: str = "Save choices", position: str = "bottom", label: str = "Cookie consent", on_change = null, class: str? = null, children) { style global cookie_consent_css let cats = categories ?? cookie_consent_default state shown = true state open = false state picked = cats | filter((c) => c.required == true) | map((c) => c.key) let keep = (choices: map) => { js("localStorage.setItem", `ui-cookies:${name}`, json.stringify(choices)) shown = false on_change && on_change(choices) } let decide = (all_on: bool) => { keep(reduce(cats, {}, (m, c) => set(m, c.key, all_on || c.required == true))) } <div class="ui-cookie-host" @mount={(_el) => { if cookie_consent_choices(name) != null { shown = false } }}> {shown && <div class={ui_cls(["ui-cookie", `ui-cookie-${position}`, class])} role="region" aria-label={label}> <div.ui-cookie-row> <div.ui-cookie-body> {title != null && <p.ui-cookie-title>{title}</p>} <p.ui-cookie-text> {text} {policy != null && <a.ui-cookie-policy href={policy}>Cookie policy</a>} </p> {children} </div> <div.ui-cookie-actions> <button.ui-cookie-btn type="button" aria-expanded={open ? "true" : "false"} @click={() => { open = !open }}>{settings}</button> <button.ui-cookie-btn type="button" @click={() => { decide(false) }}>{reject}</button> <button.ui-cookie-btn.ui-cookie-main type="button" @click={() => { decide(true) }}>{accept}</button> </div> </div> {open && <div.ui-cookie-settings> {for c in cats => <label.ui-cookie-cat key={c.key}> <input type="checkbox" name={`cookies_${c.key}`} checked={c.required == true || contains(picked, c.key)} disabled={c.required == true} @change={(e) => { picked = e.checked ? unique(push(picked, c.key)) : filter(picked, (k) => k != c.key) }}/> <span> <span.ui-cookie-cat-label>{c.label}{c.required == true && <span.ui-cookie-req>Always on</span>}</span> {c.text != null && <span.ui-cookie-cat-text><br/>{c.text}</span>} </span> </label>} <div.ui-cookie-actions> <button.ui-cookie-btn.ui-cookie-main type="button" @click={() => { keep(reduce(cats, {}, (m, c) => set(m, c.key, c.required == true || contains(picked, c.key)))) }}>{save}</button> </div> </div>} </div>} </div>}Related
BannerAn 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 downloadsConnectionStatusSays 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 downloadsSnackbarA short message along the bottom of the window with one action, shown from anywhere by calling snackbar_show().6 views · 6 downloadsPasswordStrengthA new-password field with a meter that fills as it gets stronger and a checklist of rules ticked as they are met.5 views · 4 downloads
Thread
Thread 0 posts
On CookieConsent: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.