Faq
Questions that open to their answers on <details>, with a search box that keeps only the ones matching.
Demo
What does it cost?
Nothing for one project and up to three people. Teams pay per seat, monthly or yearly.
Can I cancel at any time?
Yes, from Settings, Billing. You keep access until the end of the period you paid for.
Where is my data kept?
In the EU, on servers in Frankfurt, with nightly backups kept for 30 days.
Do you offer invoices for companies?
Yes: add a VAT number under Billing and every invoice carries it.
Is there a student discount?
Half price with a university email address; write to us from it.
How do I export everything?
Settings, Export makes a zip of your projects as JSON and files within a minute.
Type "invoice" or "cancel" to filter. Below, no search and one open at a time.
What does it cost?
Nothing for one project and up to three people. Teams pay per seat, monthly or yearly.
Can I cancel at any time?
Yes, from Settings, Billing. You keep access until the end of the period you paid for.
Where is my data kept?
In the EU, on servers in Frankfurt, with nightly backups kept for 30 days.
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/faq.qweimport ui/theme.qwe use theme_cssuse ui/faq.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
let faq_demo_items = [ { question: "What does it cost?", answer: "Nothing for one project and up to three people. Teams pay per seat, monthly or yearly.", open: true }, { question: "Can I cancel at any time?", answer: "Yes, from Settings, Billing. You keep access until the end of the period you paid for." }, { question: "Where is my data kept?", answer: "In the EU, on servers in Frankfurt, with nightly backups kept for 30 days." }, { question: "Do you offer invoices for companies?", answer: "Yes: add a VAT number under Billing and every invoice carries it." }, { question: "Is there a student discount?", answer: "Half price with a university email address; write to us from it." }, { question: "How do I export everything?", answer: "Settings, Export makes a zip of your projects as JSON and files within a minute." },]view FaqDemo() = <div.demo-stack> <Faq id="faq-demo" items={faq_demo_items}/> <p.demo-note>Type "invoice" or "cancel" to filter. Below, no search and one open at a time.</p> <Faq id="faq-demo-short" search={false} exclusive={true} items={take(faq_demo_items, 3)}/> </div>Source
// Faq: questions that open to show their answers, on the browser's own <details>,// with a search box above that keeps only the questions matching what is typed.//// <Faq id="faq" items={[// { question: "What does it cost?", answer: "Nothing for one project and three people." },// { question: "Can I cancel?", answer: "Any time, from Settings." },// ]}/>// <Faq id="help" items={questions} exclusive={true} search={false}/>//// id (required): the search box's id, unique on the page; the questions' ids// start with it. items: a list of { question, answer, open? }; answer is text,// or HTML (an `html` value, a <p> or two) which is shown as it is and not searched.// open: that question starts open. search: the box (true); label: what screen// readers call it ("Search questions"); placeholder: its hint ("Search…").// exclusive: opening one question closes the others. empty: what it says when// nothing matches ("No questions match."). The count of questions showing is// said to screen readers as it changes.import ui/theme.qwe use ui_clslet faq_css = css`.ui-faq { display: grid; gap: 14px; font: 15px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-faq-bar { display: flex; align-items: center; gap: 12px; }.ui-faq-search { position: relative; flex: 1; display: flex; align-items: center; }.ui-faq-search svg { position: absolute; left: 12px; width: 16px; height: 16px; color: var(--ui-muted); pointer-events: none; }.ui-faq-input { box-sizing: border-box; width: 100%; height: 40px; margin: 0; padding: 0 12px 0 38px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 15px/1 var(--ui-font); transition: border-color .15s, box-shadow .15s;}.ui-faq-input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-faq-input:hover { border-color: var(--ui-strong); }.ui-faq-input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-faq-count { flex: none; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-faq-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--ui-border); }.ui-faq-item { border-bottom: 1px solid var(--ui-border); }.ui-faq-item > summary { display: flex; align-items: center; gap: 14px; padding: 14px 4px; cursor: pointer; list-style: none; font-weight: 500; font-size: 16px; border-radius: var(--ui-radius-sm); }.ui-faq-item > summary::-webkit-details-marker { display: none; }.ui-faq-item > summary:hover { color: var(--ui-accent); }.ui-faq-item > summary:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-faq-q { flex: 1; }.ui-faq-plus { flex: none; position: relative; width: 16px; height: 16px; color: var(--ui-muted); }.ui-faq-plus::before, .ui-faq-plus::after { content: ""; position: absolute; top: 7px; left: 2px; width: 12px; height: 2px; border-radius: 1px; background: currentColor; transition: transform .2s; }.ui-faq-plus::after { transform: rotate(90deg); }.ui-faq-item[open] > summary .ui-faq-plus::after { transform: rotate(0); }.ui-faq-answer { padding: 0 4px 18px 34px; color: var(--ui-muted); }.ui-faq-answer > :first-child { margin-top: 0; }.ui-faq-answer > :last-child { margin-bottom: 0; }.ui-faq-empty { padding: 24px 4px; color: var(--ui-muted); text-align: center; }@media (prefers-reduced-motion: reduce) { .ui-faq-plus::before, .ui-faq-plus::after, .ui-faq-input { transition: none; } }`// Whether an item's question (or its answer, when it is text) has the words.fn faq_matches(item: map, needle: str) -> bool { if needle == "" { return true } let answer = type_of(item.answer) == "str" ? item.answer : "" contains(lower(item.question + " " + answer), needle)}component Faq(id: str, items: list = [], search: bool = true, label: str = "Search questions", placeholder: str = "Search…", exclusive: bool = false, empty: str = "No questions match.", class: str? = null) { style global faq_css state query = "" let needle = lower(trim(query)) let shown = items | filter((it) => faq_matches(it, needle)) let total = len(items) let count = len(shown) <div class={ui_cls(["ui-faq", class])}> {search && <div.ui-faq-bar> <div.ui-faq-search> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg> <input.ui-faq-input type="search" id={id} value={query} placeholder={placeholder} aria-label={label} aria-controls={`${id}-list`} autocomplete="off" @input={(e) => { query = e.value }}/> </div> <span.ui-faq-count role="status" aria-live="polite"> {needle == "" ? `${total} question${total == 1 ? "" : "s"}` : `${count} of ${total}`} </span> </div>} <ul.ui-faq-list id={`${id}-list`}> {for [i, it] in enumerate(shown) => <li key={it.question}> <details.ui-faq-item id={`${id}-q-${i}`} name={exclusive ? `${id}-group` : null} open={it.open == true}> <summary> <span.ui-faq-q>{it.question}</span> <span.ui-faq-plus aria-hidden="true"></span> </summary> <div.ui-faq-answer>{type_of(it.answer) == "str" ? <p>{it.answer}</p> : it.answer}</div> </details> </li>} </ul> {count == 0 && <p.ui-faq-empty>{empty}</p>} </div>}Related
Thread 0 posts
On Faq: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.