Feedback

CookieConsent

A cookie banner with accept, reject and settings by category, remembering the choice in localStorage.

#feedback#notification#settings#security#interactive

Demo

Preview

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

Thread 0 posts

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

No posts yet. The first one opens the thread.