<!-- https://getkonjo.com/design/studio/overlays · source: docs/design/studio/overlays.md -->

# Overlays

Studio's floating surfaces: the drawer, the menu, the dropdown, the transient confirmation.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them. Studio-specific — see
> [the dialect](dialect.md).

An overlay is a surface that sits **above** the page rather than in it. That is a claim about
depth, and this design language has no depth: page content is flat and
[shadows are banned](../foundations/elevation.md). So an overlay has to earn its plane some
other way, and the way it earns it is the whole subject of this chapter.

**These four are Studio's, not the app's.** The mobile equivalent is one shape — the bottom
sheet in [sheet-picker](../patterns/sheet-picker.md) — and it works because a phone gives it
the full width, a scrim over everything behind it, and a thumb that arrived deliberately. A
desktop dropdown has none of those: it is 200px wide, it covers almost nothing, and it opens
under a pointer that may already be moving somewhere else. Copying the sheet's recipe onto it
is how the recipe gets blamed for a result it was never asked for.

## When an overlay

- The thing is **transient and secondary** — a menu of five actions, a list of options, a
  confirmation of something just done.
- Losing it costs nothing. Escape, a click elsewhere, or a scroll all dismiss it, and the
  person is back where they were with nothing to undo.

Otherwise it is a page or a panel. **An overlay must never be the only route to a fact**:
anything the reader might want to return to, screenshot, or send to somebody has a URL. A lead
opens in a drawer *and* is addressable; if it were only a drawer, "send me that lead" would be
an instruction nobody can follow.

**Never put a form longer than three fields in an overlay.** *(System-wide — this holds for
the mobile sheet too, and [form](../patterns/form.md) states the same limit from the other
side.)* An overlay cannot show validation errors above the fold, cannot be scrolled to
comfortably, and evaporates on a mis-click, taking the typing with it.

## Separation — the arithmetic

This is the part the rest of the chapter rests on, and it is the part where the phone's answer
gives the wrong result on a desk.

The spine's recipe for a floating surface is **a scrim plus a hairline**. On a bottom sheet
that works: the scrim darkens the entire page, so the sheet is separated by the wholesale
change behind it, and the hairline only has to crisp the edge.

A desktop **dropdown has no scrim.** Scrimming the page to open a five-item select would be
absurd — the reader is mid-task and the page behind is the context they are choosing against.
So the boundary is doing **all** the separation work, alone.

And `border.hairline` cannot do that work:

| Pair | Ratio | |
|---|---|---|
| `border.hairline` on `surface.card` | **1.22:1** | under 3:1 — an optical edge, not a boundary |
| `border.hairline` on `surface.page` | **1.09:1** | under 3:1 — effectively invisible |
| `surface.card` on `surface.page` | **1.11:1** | under 3:1 — a card is not separated by its fill either |
| `border.control` on `surface.card` | **3.53:1** | clears 3:1 |
| `border.control` on `surface.page` | **3.15:1** | clears 3:1 |
| `border.control` on `surface.muted` | **3.38:1** | clears 3:1 |

So: **a floating surface's boundary is `border.control`, not `border.hairline`.**
*(Shape-local. It overrides the hairline in the spine's floating-surface recipe, and only for
surfaces that float. A hairline stays a hairline everywhere else — between rows, around a card,
under a header.)*

The reasoning generalises even though the rule does not. A hairline is a divider **between
things on the same plane**; it says "these are two rows", not "this is a different surface".
Where a plane change has to be legible on its own, WCAG 1.4.11 governs it at 3:1, and only
`border.control` clears that on all three Studio surfaces.

**This does not reopen the shadow ban.** The [calibration record](../2026-08-22-design-calibration.md)
pre-authorises one elevation token for floating surfaces if the existing set proves
insufficient — and it does not. The existing set had a rule missing, not a value. That is
governance step one working exactly as [written](../governance.md#adding-a-token).

**A drawer keeps its scrim.** It covers most of the page and takes over the reader's attention;
without one, the page behind stays live and clickable, and a click meant for the drawer lands
in the roster underneath.

## The drawer

Right-side, full-height, for one record's detail while the list stays put.

```
                              ┌──── scrim over the page ─────┐
┌─────────────────────────────┼──────────────────────────────┐
│ Leads                       │  Marcus Webb            [ × ] │  1px border.control on the
│ ─────────────────────────── │  Contacted · 3 days           │  scrim-facing edge only
│ ▢ Ana Torres                │  ───────────────────────────  │
│ ▢ Marcus Webb   ← still     │                               │  480px, or 40% — whichever
│ ▢ Priya Nair      visible   │  [ Call ] [ Text ] [ Email ]  │  is narrower
│                             │                               │
│                             │  ─────────────────────────    │  actions pinned to the
│                             │  [ Convert to member ]        │  bottom, always reachable
└─────────────────────────────┴───────────────────────────────┘
```

- **The list behind stays visible and stays put.** That is the entire reason a drawer beats a
  page here: the reader is working *through* a list and has not left it.
- **480px or 40% of the viewport, whichever is narrower.** Below ~900px it goes full-width,
  because a drawer that leaves 100px of list is showing neither.
- **Escape closes. The scrim closes.** Both, always.
- **It has a URL.** Opening a drawer pushes a route; closing it pops. Back does what it looks
  like it does, and a drawer is linkable.

## The menu, and the popover

A menu is a short list of actions. A popover is a short piece of read-only detail. They are the
same surface with different contents, and both are anchored to the control that opened them.

```
[ ⋯ ]                        ← the trigger keeps aria-expanded
 └─┬──────────────────────┐
   │ Edit                 │     1px border.control all round
   │ Duplicate            │     radius.card, surface.card
   │ ──────────────────── │     ← hairline BETWEEN items: same plane
   │ Delete               │       (the outer boundary is not)
   └──────────────────────┘
```

- **No scrim.** It covers almost nothing and the page behind is still the context.
- **The boundary is `border.control` at 3.53:1** on a card. Inside it, a divider between two
  items is `border.hairline` — those items share a plane, so the hairline is right there and
  wrong on the outside edge. One surface, two different jobs, two different tokens.
- **Anchored, and it flips rather than clips.** Not enough room below, it opens upward; not
  enough room right, it right-aligns. It never opens off-screen and it never scrolls the page
  to fit itself.
- **At most about seven items.** More than that is a search field or a page, not a menu.
- **A destructive item is last, after a divider**, and still obeys
  [destructive](../patterns/destructive.md) — the menu is not a confirmation.

## The dropdown

A select's listbox: the same surface as a menu, but choosing one value rather than firing an
action, and it carries the current selection.

```
Rank        [ Brown belt              ▾ ]   ← the trigger; the value lives HERE
             └─────────────────────────┐
               │ ✓ Brown belt          │    the selected row is marked with a check,
               │   Brown stripe 2      │    NOT with colour alone
               │   Brown stripe 1      │
               │   Green belt          │    max-height ~40% viewport, then it scrolls
               └───────────────────────┘    itself — never the page
```

- **The trigger shows the current value, always.** A dropdown reading "Select…" next to a
  record that already has a rank is lying about the record.
- **The selected row carries a check.** Colour alone fails anyone who cannot distinguish it,
  and a selected row that is only tinted is indistinguishable from a hovered one.
- **An unavailable option states why, in the row.** *(System-wide — this is
  [sheet-picker](../patterns/sheet-picker.md)'s rule and it holds for every picker on either
  platform.)* "Brown belt — needs head-instructor approval", never a greyed row with no
  explanation.
- **Typing jumps.** A listbox open with focus on it takes letter keys and moves the highlight.
  For anything past ~10 options this is not optional; it is the only fast path a keyboard has.
- **The listbox scrolls, the page does not.**

## The transient confirmation

The line that appears after something worked, and leaves on its own.

```
                    ┌─────────────────────────────────────┐
                    │ Moved to Contacted.        [ Undo ] │
                    └─────────────────────────────────────┘
                       bottom-centre · surface.hero ·
                       1px border.control · stays 8s when
                       it carries an Undo, 4s when it does not
```

- **It never carries the only copy of anything.** *(System-wide.)* If the reader misses it
  entirely, they must lose nothing but the convenience — the change is already on the page
  behind it.
- **An Undo makes it stay longer, and pause on hover.** Eight seconds, and the clock stops
  while the pointer is over it. A four-second Undo is decoration.
- **It never covers an action.** Bottom-centre, above anything pinned there.
- **One at a time.** A second replaces the first rather than stacking; a stack is a log, and a
  log is a page.
- **Never for an error.** An error that needs reading needs somewhere to stay —
  [states](../patterns/states.md) owns that. A toast that disappears with the only account of
  what went wrong is worse than no toast.

## Focus and the keyboard

*(System-wide. Every rule in this section applies to the mobile sheet too; nothing here is a
desk-only concern, and getting it wrong is what makes an overlay unusable rather than merely
awkward.)*

| | |
|---|---|
| **On open** | Focus moves into the overlay — its first focusable control, or the overlay itself if it has none. |
| **While open** | Tab is trapped inside it. Tab past the last control returns to the first. |
| **Escape** | Closes. Always, on all four. No exceptions, including a drawer with unsaved input — which is a reason to warn, not a reason to trap. |
| **On close** | Focus returns to **the control that opened it**. Losing focus to the top of the document is how a keyboard user gets lost, and it is the single most common overlay defect. |
| **The trigger** | Carries `aria-expanded`, and `aria-controls` pointing at the overlay. |
| **The focus ring** | `border.focus` at **16.48:1** on a card. Never removed, never replaced with a colour change. |

A menu, popover or dropdown **does not trap focus the way a drawer does** — it closes on blur
instead. Trapping a five-item menu makes it a modal, which it is not.

## Announcements

- A drawer opening announces its **title**, not "dialog opened".
- A transient confirmation is `role="status"` and **polite**: it must not interrupt what the
  reader is doing, because by definition the thing it describes has already succeeded.
- An error uses `assertive` — but per the rule above, an error does not belong in a toast.
- A dropdown's selection change announces the **new value**, not the fact that a change
  occurred.

## Never

- A shadow, a blur, or a `backdrop-filter`. The boundary does the work.
- An overlay opened from an overlay. If a menu needs a submenu, the menu is a page.
- A scrim on a menu, popover or dropdown.
- A drawer with no URL.
- A form of more than three fields.
- A toast carrying an error, or the only copy of anything.
- `border.hairline` as an overlay's outer boundary — 1.22:1 on a card, and the reader cannot
  see where the surface ends.
- Removing the focus ring to make an overlay look tidier.

## The self-check

1. Close it with Escape. Did focus return to the control that opened it?
2. Tab all the way round. Did you ever land behind it?
3. Screenshot it and cover the content. Can you still see where the surface ends?
4. Does the thing inside it have a URL, or did the drawer become the only way to reach it?
5. Read the toast's words with the page hidden. Do they still say what happened?
