Design Lounge
Install

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

Open full size
1280 × 800 · 9.8 KB · 157 lines

Use it

Free to use · credit appreciated

1 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.

Brief

187 lines

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.
  • <section aria-label="Frequently asked questions">: .lead (heading + .search) and #grid.
  • #grid is 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 .panel role="region".
  • Search: icon SVG, visually hidden label, input type="search", .count.

Questions, exact (odd → left, even → right):

#QuestionAnswer facts
01How do I rotate an API key without dropping traffic?Second key, 15-minute overlap, then revoke. Old key → 401 kestrel.auth.stale.
02Why is my webhook retrying every 30 seconds?Retries on non-2xx for 2 hours: 30s, 60s, then 5 min. Return 204. 10-second budget.
03Which regions can a project live in?eu-central, us-east, ap-south. Move is a copy; old region readable 30 days.
04What 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.
05Can I replay a failed event from the dashboard?14 days. Adds x-kestrel-replay: 1. Replays do not increment daily quota.
06How do signed requests expire?X-Kestrel-Ts within 300 seconds. Else 401 kestrel.auth.skew.
07Is the sandbox key billed?No. 1,000 calls/day. Promote copies routes and drops the flag; new key.
08Where 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

RoleFamilySizeWeightLine-heightTrackingCase
Body / answersGeist14px4001.450sentence
BrandGeist14px6001−0.02emsentence
Nav linksGeist Mono12px40010sentence
Version / countGeist Mono11px400 / 50010sentence
KickerGeist Mono11px5001+0.12emUPPERCASE
HeadingGeist32px6001.1−0.03emsentence
Search inputGeist14px40010as typed
QuestionGeist15px5001.3−0.01emsentence
NumberGeist Mono11px4001.3001–08
CodeGeist Mono12px40010as written
Empty titleGeist22px6001.2−0.02emsentence
Clear buttonGeist Mono12px50010sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasing
.panelopen / closegrid-template-rows0fr ↔ 1fr280ms--ease
.q svgopen / closerotate0 ↔ 45°280ms--ease
Filter show/hideinputhidden—0instant

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°, panel 1fr.
  • 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: #empty display:none.
  • Empty shown: .show → display:block, grid-column 1 / -1.
  • Clear hover: invert to #111 fill, #F6F6F4 text.
  • Filtered-out row: [hidden] / display:none. Do not use opacity:0 — hidden rows must leave the columns.

Accessibility

  • Section: aria-label="Frequently asked questions".
  • Search has a visually hidden <label for="q"> “Filter questions”. Use type="search".
  • Each question is a <button> with aria-expanded and aria-controls pointing at the panel id. Panel is role="region" + aria-labelledby the 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: #111 on #F6F6F4 16:1; #5A5A56 on #F6F6F4 5.8:1; #1F4E79 on #F6F6F4 7.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 auth leaves 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-rows 0fr → 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 #E6EEF4 for codes such as 401 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: reduce keeps 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
Designed bySusan AcharyaKathmandu

Studied

Don't Board Me

A service with a next step. The headline can shout. The booking stays plain.

All sources