Foundations

Elevation

A surface earns a shadow when it leaves the page. Borders and surface tints carry hierarchy in flow; shadow is reserved for UI that floats.

Documentedby Derek Fidler

Overview

In editorial UIs, depth is something you spend, not something you decorate with. Most surfaces in Flatpay are flat. They sit on the page; a hairline border and a subtle background tint tell you where one ends and another begins.

Shadow shows up when a surface leaves the page — when it is summoned for a moment, when it captures focus, when it has to read as a layer above the rest. A page full of equally-shadowed cards looks templated. A page where exactly one thing is shadowed reads as deliberate.

The first principle

Borders before shadows. If a surface lives in the page's normal flow, give it a 1-pixel border and stop. Reach for shadow-* only when the surface is genuinely floating above the rest of the page.

Surfaces first

The same card, two ways. The bordered version belongs on the page. The shadowed version pretends it's floating, and the lie compounds the moment you put two of them next to each other.

Demo-readyLucky GardenOnboarding flow for new merchants.

In-flow surface

Preferred

Border + tint

A card embedded in the page. The 1-pixel border and a small bg-tint shift do the work. No shadow needed — the page stays flat and orderly.

Demo-readyLucky GardenOnboarding flow for new merchants.

In-flow surface

Avoid

Shadow as the only cue

Shadow without a border reads as floating UI on the page itself. Tempting, then noisy at scale — a dashboard of these looks templated, not designed.

The shadow scale

We use Tailwind's default shadow scale verbatim — seven rungs from flat to fully detached. Each rung implies a different relationship to the page beneath it. Pick the lowest rung that gets the job done.

TokenTailwindPreviewUse
  • shadow.noneshadow-none

    The default. Cards, list rows, table cells, sections. Flat surfaces hold the page together.

  • shadow.xsshadow-xs

    Hairline lift — hovered list rows, pressed buttons leaving the surface for a beat.

  • shadow.smshadow-sm

    Hover cards, inline popovers, autocomplete suggestions sitting on the input.

  • shadow.mdshadow-md

    Dropdown menus, select listboxes, date pickers, tooltips with context.

  • shadow.lgshadow-lg

    Side sheets, command palettes, popover dialogs anchored to a trigger.

  • shadow.xlshadow-xl

    Modal dialogs sitting on a dimmed scrim. The page behind goes quiet; the dialog gets the lift.

  • shadow.2xlshadow-2xl

    Reserved. Toast stacks, app-level overlays, anything that must read as a layer above the modal layer itself.

Floating UI

The four canonical floating surfaces: hover cards (the lightest), dropdowns (anchored to a trigger), popovers (anchored, larger, may contain controls), and dialogs (modal, demand a decision). Each gets the lowest shadow that still reads as detached.

Filter by status
  • All prototypes
  • Demo-ready
  • Building
  • Archived

Dropdown menu · shadow-md

Anchored to a trigger, lives one layer above the page. The shadow says "temporary," not "structural."

Lucky Garden

Lucky Garden

Demo-ready · Low risk

Lab

Onboarding flow for new merchants. Last synced 3h ago.

Hover card · shadow-sm

The lightest lift. Lives on the surface it's anchored to; doesn't claim its own page region.

Archive Lucky Garden?

The repo stays — the prototype is hidden from the catalog.

CancelArchive

Dialog · shadow-xl + scrim

The page is dimmed; the dialog gets the highest practical lift. Reserve this treatment for decisions the user must answer.

Backdrops belong to dialogs only

A scrim — the dimmed sheet behind a dialog — signals that everything else is paused. Don't add it to popovers, dropdowns, or hover cards. Those surfaces co-exist with the page; the dialog interrupts it.

Stacking order

Five layers cover every screen we ship. They climb in steps of 10 so a future layer can slot in between without renumbering everything else.

z-indexLayerExamples
  • z-0page

    Cards, list rows, content sections — everything in normal flow.

  • z-10sticky

    In-page sticky elements — table headers, secondary tabs.

  • z-20sidebar

    Left navigation; lifts above scroll content but sits under chrome.

  • z-30header

    Top app bar, page hero pin. Above the sidebar so menus extend cleanly.

  • z-40overlay

    Dropdowns, popovers, hover cards, autocomplete listboxes.

  • z-50dialog

    Modal dialogs, side sheets, command palettes — UI that captures focus.

  • z-50*toast

    Same layer as dialog; rendered last so notifications survive a dialog open.

* Toasts and dialogs share the dialog layer; render order, not z-index, decides which stacks on top.

Interactive elevation

Interactive surfaces don't just change color on hover — they shift relative to the page. The lift is small, the press is smaller, and both finish in under 150ms. The motion is what tells the user the surface responded; the shadow is just the byproduct.

Resting

Default state — flat against the page.

Hover

Cursor on the surface. Border tightens, an xs shadow hints the lift.

Pressed

Pointer down. Returns to the page; the shadow drops away.

In code

Tailwind utilities, applied in line. The shadow class is the token; no extra alias layer.

tsx

// Default card — flat, bordered
<article className="rounded-lg border bg-card p-5">
  …
</article>

// Dropdown menu — anchored, floating
<div role="menu" className="rounded-lg border bg-background p-2 shadow-md">
  …
</div>

// Hover card — light lift
<aside className="rounded-lg border bg-background p-3 shadow-sm">
  …
</aside>

// Modal dialog — highest practical lift, with scrim
<div className="fixed inset-0 z-50">
  <div aria-hidden className="absolute inset-0 bg-foreground/35" />
  <div role="dialog" className="absolute inset-x-6 top-1/2 -translate-y-1/2 rounded-xl border bg-background p-6 shadow-xl">
    …
  </div>
</div>

// Interactive lift — restful → hover → pressed
<button
  className="rounded-md border bg-background px-3 py-2 transition
             hover:border-foreground/40 hover:shadow-xs
             active:shadow-none"
>
  Open prototype
</button>

Don'ts

  • Don't shadow in-flow surfaces. Cards, list rows, sections, and table cells are flat. A border carries the edge.
  • Don't skip rungs. If a dropdown wants shadow-md, don't reach for shadow-xl to make it "more visible." Bigger shadow doesn't mean bigger importance — it means further from the page.
  • Don't stack shadows. A shadowed card inside a shadowed surface inside a dialog reads as a layered cake. Pick the layer that owns the lift; everything inside it stays flat.
  • Don't hard-code z-index. Use the named layers (z-0 through z-50). Numbers like z-[9999] are a symptom of an unowned stacking order.
  • Don't replace shadow with glow. No coloured drop shadows, no neon halos, no "brand-tinted" glows. Lift is described in greyscale; the brand is described elsewhere.