Marketing

Faq

Questions that open to their answers on <details>, with a search box that keeps only the ones matching.

#marketing#search#filter#list#interactive

Demo

Preview
6 questions
  • 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.qwe
main.qwe
import 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.

demos/faq.qwe
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

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

Thread 0 posts

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

No posts yet. The first one opens the thread.