Nº 111 of 520
FAQ two-column search
A Kestrel docs FAQ: eight accordion rows in two columns, a 44px search field that filters them, and an empty state when nothing hits.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "FAQ two-column search" (piece faq-two-column-search) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 111 · "FAQ two-column search" · www.designlounge.live -->
# FAQ two-column search
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A docs FAQ for **Kestrel**, an HTTP API product. Eight numbered questions sit in two columns under a 44px search field. Typing filters rows by question and answer text (case-insensitive). A live “**N** of 8 topics” count updates on every keystroke. If the filter matches nothing, both columns hide their rows and an empty state appears: “No topics match that filter” plus a Clear filter button. Each row is an accordion that opens with `grid-template-rows: 0fr → 1fr` over 280ms; several may be open at once. This is a swiss index, not a paper one-column FAQ.
## Reference behaviour
1. Initial state: 52px nav (8px rotated-square mark + Kestrel + Guides / API / Changelog in Geist Mono + “docs 2.4.1”). Lead: kicker “DOCS · FAQ”, 32px heading “Questions the API desk hears”, search on the right with placeholder “Filter by topic — auth, webhook, region…”. Count reads **8** of 8 topics. Eight rows in two columns (odd numbers left, even right). Question 01 is open (`aria-expanded="true"`), its plus icon rotated 45°, text navy. The other seven are closed.
2. Click a closed question: that row opens over 280ms; its plus rotates 45° to a close mark; question text turns `--navy`. Other open rows stay open.
3. Click an open question: it closes on the same clock; icon returns to +; colour returns to `--ink`.
4. Type `auth` in the search field: rows whose haystack contains “auth” stay; the rest get `hidden`. Count becomes **2 of 8** (01 rotate key, 06 signed requests). Open/closed state of survivors is unchanged.
5. Type a string with no hit (e.g. `banana`): count **0 of 8**, `#empty` gets `.show`, the two columns look vacant. Empty copy offers `auth`, `webhook`, `region` as examples.
6. Click **Clear filter**: input value is emptied, all eight rows unhide, count returns to 8, focus returns to the search field.
7. Clearing the field with the input’s native clear control (type=search) also runs the same `input` handler.
8. Reduced motion: panel and icon transitions become 1ms. Filter still works.
## Structure
```
1280 × 800
+----------------------------------------------------------------------+
| * Kestrel Guides API Changelog docs 2.4.1 | 52
| DOCS · FAQ [ Filter by topic -- auth... ] |
| Questions the API desk hears 8 of 8 topics | 44
|----------------------------------------------------------------------| ink rule
| 01 How do I rotate an API key... + | 02 Why is my webhook... + |
| (answer open on 01) | 04 What does the 429... + |
| 03 Which regions can a project...+ | 06 How do signed requests... + |
| 05 Can I replay a failed event...+ | 08 Where do I file a... + |
| 07 Is the sandbox key billed? + | |
+----------------------------------------------------------------------+
pad 56 columns 1fr 1fr, gap 48 row pad 14 0
```
- `<nav>` 52px.
… 147 more lines. Copy or download for the full brief. Loading the source…Brief
187 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A docs FAQ for Kestrel, an HTTP API product. Eight numbered questions sit in two columns under a 44px search field. Typing filters rows by question and answer text (case-insensitive). A live “N of 8 topics” count updates on every keystroke. If the filter matches nothing, both columns hide their rows and an empty state appears: “No topics match that filter” plus a Clear filter button. Each row is an accordion that opens with grid-template-rows: 0fr → 1fr over 280ms; several may be open at once. This is a swiss index, not a paper one-column FAQ.
Reference behaviour
- Initial state: 52px nav (8px rotated-square mark + Kestrel + Guides / API / Changelog in Geist Mono + “docs 2.4.1”). Lead: kicker “DOCS · FAQ”, 32px heading “Questions the API desk hears”, search on the right with placeholder “Filter by topic — auth, webhook, region…”. Count reads 8 of 8 topics. Eight rows in two columns (odd numbers left, even right). Question 01 is open (
aria-expanded="true"), its plus icon rotated 45°, text navy. The other seven are closed. - Click a closed question: that row opens over 280ms; its plus rotates 45° to a close mark; question text turns
--navy. Other open rows stay open. - Click an open question: it closes on the same clock; icon returns to +; colour returns to
--ink. - Type
authin the search field: rows whose haystack contains “auth” stay; the rest gethidden. Count becomes 2 of 8 (01 rotate key, 06 signed requests). Open/closed state of survivors is unchanged. - Type a string with no hit (e.g.
banana): count 0 of 8,#emptygets.show, the two columns look vacant. Empty copy offersauth,webhook,regionas examples. - Click Clear filter: input value is emptied, all eight rows unhide, count returns to 8, focus returns to the search field.
- Clearing the field with the input’s native clear control (type=search) also runs the same
inputhandler. - Reduced motion: panel and icon transitions become 1ms. Filter still works.
Structure
1280 × 800
+----------------------------------------------------------------------+
| * Kestrel Guides API Changelog docs 2.4.1 | 52
| DOCS · FAQ [ Filter by topic -- auth... ] |
| Questions the API desk hears 8 of 8 topics | 44
|----------------------------------------------------------------------| ink rule
| 01 How do I rotate an API key... + | 02 Why is my webhook... + |
| (answer open on 01) | 04 What does the 429... + |
| 03 Which regions can a project...+ | 06 How do signed requests... + |
| 05 Can I replay a failed event...+ | 08 Where do I file a... + |
| 07 Is the sandbox key billed? + | |
+----------------------------------------------------------------------+
pad 56 columns 1fr 1fr, gap 48 row pad 14 0
<nav>52px.<section aria-label="Frequently asked questions">:.lead(heading +.search) and#grid.#gridis a 2-column grid containing#col-a,#col-b, and#empty(spans both columns when shown).- Each
.item:<h3><button class="q" aria-expanded aria-controls>(.n+.t+ plus SVG) and.panelrole="region". - Search: icon SVG, visually hidden label,
input type="search",.count.
Questions, exact (odd → left, even → right):
| # | Question | Answer facts |
|---|---|---|
| 01 | How do I rotate an API key without dropping traffic? | Second key, 15-minute overlap, then revoke. Old key → 401 kestrel.auth.stale. |
| 02 | Why is my webhook retrying every 30 seconds? | Retries on non-2xx for 2 hours: 30s, 60s, then 5 min. Return 204. 10-second budget. |
| 03 | Which regions can a project live in? | eu-central, us-east, ap-south. Move is a copy; old region readable 30 days. |
| 04 | What does the 429 burst window actually count? | 120 / 10 s per key. X-Kestrel-Remain. Daily cap 250,000 is a separate 429 kestrel.quota.day. |
| 05 | Can I replay a failed event from the dashboard? | 14 days. Adds x-kestrel-replay: 1. Replays do not increment daily quota. |
| 06 | How do signed requests expire? | X-Kestrel-Ts within 300 seconds. Else 401 kestrel.auth.skew. |
| 07 | Is the sandbox key billed? | No. 1,000 calls/day. Promote copies routes and drops the flag; new key. |
| 08 | Where do I file a region outage? | status.kestrel.dev. Ticket with X-Kestrel-Id. Do not rotate keys first. |
Tokens
:root {
--bg: #f6f6f4;
--ink: #111111;
--ink-2: #5a5a56;
--ink-3: #8a8a84;
--line: #e2e2dc;
--line-2: #c8c8c2;
--navy: #1f4e79; /* kicker, open question, focus, code */
--navy-soft: #e6eef4; /* code chip */
--sans: "Geist", system-ui, sans-serif;
--mono: "Geist Mono", ui-monospace, monospace;
--nav-h: 52px;
--pad: 56px;
--search-h: 44px;
--gap-cols: 48px;
--t-fast: 160ms;
--t-open: 280ms;
--ease: cubic-bezier(.2, .7, .2, 1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Body / answers | Geist | 14px | 400 | 1.45 | 0 | sentence |
| Brand | Geist | 14px | 600 | 1 | −0.02em | sentence |
| Nav links | Geist Mono | 12px | 400 | 1 | 0 | sentence |
| Version / count | Geist Mono | 11px | 400 / 500 | 1 | 0 | sentence |
| Kicker | Geist Mono | 11px | 500 | 1 | +0.12em | UPPERCASE |
| Heading | Geist | 32px | 600 | 1.1 | −0.03em | sentence |
| Search input | Geist | 14px | 400 | 1 | 0 | as typed |
| Question | Geist | 15px | 500 | 1.3 | −0.01em | sentence |
| Number | Geist Mono | 11px | 400 | 1.3 | 0 | 01–08 |
| Code | Geist Mono | 12px | 400 | 1 | 0 | as written |
| Empty title | Geist | 22px | 600 | 1.2 | −0.02em | sentence |
| Clear button | Geist Mono | 12px | 500 | 1 | 0 | sentence |
Motion
| Element | Trigger | Property | From → To | Duration | Easing |
|---|---|---|---|---|---|
.panel | open / close | grid-template-rows | 0fr ↔ 1fr | 280ms | --ease |
.q svg | open / close | rotate | 0 ↔ 45° | 280ms | --ease |
| Filter show/hide | input | hidden | — | 0 | instant |
Do not animate rows in and out of the filter. Reduced motion: 1ms on .panel and the icon.
States
- Question rest:
--ink, plus at 0°. - Question hover: colour
--navy. - Question open: colour
--navy, plus at 45°, panel1fr. - Question focus-visible: 2px navy outline, 3px offset (global).
- Search rest: 44px, white fill, 1px
--line-2, 36px left padding for the icon. - Search focus: border
--navy. - Empty hidden:
#emptydisplay:none. - Empty shown:
.show→display:block, grid-column 1 / -1. - Clear hover: invert to
#111fill,#F6F6F4text. - Filtered-out row:
[hidden]/display:none. Do not useopacity:0— hidden rows must leave the columns.
Accessibility
- Section:
aria-label="Frequently asked questions". - Search has a visually hidden
<label for="q">“Filter questions”. Usetype="search". - Each question is a
<button>witharia-expandedandaria-controlspointing at the panel id. Panel isrole="region"+aria-labelledbythe button. - Count (
#shown) updates in visible text; it does not need a live region on every keystroke (that would chatter). The empty heading is enough when the list collapses. - Keyboard: Tab through nav → search → each visible question button → Clear (when shown). Enter/Space toggles the focused question (native button).
- Contrast:
#111on#F6F6F416:1;#5A5A56on#F6F6F45.8:1;#1F4E79on#F6F6F47.4:1; navy on#E6EEF4≥ 7:1. - Hit targets: search 44px; question rows ≥ 44px (14px padding + 15px/1.3 type); Clear 36px × auto with 12px padding.
Responsive rules
- ≥ 1280: two columns, 48px gutter, lead is heading + 340px search, pad 56px.
- 1024–1279: pad 28px, lead stacks (search under heading), gutter 24px. Two columns remain.
- 768–1023: one column; nav links hide; heading 26px. Filter still applies to the single stack (odds then evens, or keep source order by walking both columns).
- < 640: same single column. Search stays 44px full width.
Acceptance checklist
- First frame shows eight numbered questions in two columns; 01 is open; search is empty; count is 8 of 8.
- Typing
authleaves two rows and the count reads 2 of 8. - A non-matching filter shows the empty state and hides every row.
- Clear filter restores eight rows and focuses the search field.
- Accordion height animates with
grid-template-rows0fr → 1fr over 280ms; several rows may be open together. - Open question and plus icon are navy; plus rotates 45°.
- Answers use Geist Mono chips on
#E6EEF4for codes such as401 kestrel.auth.stale. - Type pairing is Geist + Geist Mono only. Ground is
#F6F6F4. Navy is#1F4E79, not a purple-to-blue gradient. - Focus rings are 2px navy, 3px offset, on search, questions, and Clear.
-
prefers-reduced-motion: reducekeeps filter and toggle, instant height. - Demo fills 1280×800 and starts with the piece header comment.
Implementation notes
Split columns in source, filter in place. Build eight items, append odds to column A and evens to column B, then toggle hidden — do not rebuild the DOM on each keystroke or the open state is lost:
el.hidden = t && !el.dataset.hay.includes(t);
Store a plain-text haystack (question + answer, tags stripped) on data-hay at build time.
Height without scrollHeight:
.panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 280ms var(--ease); }
.item.open .panel { grid-template-rows: 1fr; }
.panel > div { overflow: hidden; min-height: 0; }
Empty state is a grid child, not a sibling covering the grid:
.empty { display: none; grid-column: 1 / -1; }
.empty.show { display: block; }
Common mistakes: a single-column accordion (that is a different piece); filtering only the question title so “429” misses the burst answer; display:none on the panel for close (no animation, and hidden on the item already covers filter); loading Inter instead of Geist; a magnifying-glass PNG.
Details
- Palette
- Type
- Geist · Geist Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
Don't Board MeA service with a next step. The headline can shout. The booking stays plain.
All sourcesOn the shelves