Browse all of Kata

Overlays

Source docs/design/studio/overlays.mdMarkdown

On this page

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

Part of the Konjo design language. The laws in the spine apply here unless this chapter contradicts them. Studio-specific — see the dialect.

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

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 — 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'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 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?