Skip to content

Elevation

Four levels of depth. Surfaces on the page are drawn by their border; only what floats above the page casts a shadow. Depth tells people what belongs to what, and what is temporary.

Elevation tells people how things relate. Content sits on the page; menus float above the thing that opened them; dialogs interrupt everything. prfct has four levels and components choose one by role.

flat

0. Flat

Page content, inline elements.

raised

1. Raised

Cards, panels, controls. A 1px border, never a border and a shadow.

floating

2. Floating

Menus, popovers, tooltips, select lists.

modal

3. Modal

Dialogs, sheets, command palette.

Raised · card

A 1px border, no shadow.

Floating · menu

Lifted above content it belongs to.

Modal · dialog

Above everything; the page waits.

import { Button } from "@/components/ui/button"

export default function ElevationStack() {
  return (
    // Wide containers overlap the layers in place; narrow ones cascade them
    // down the page, each one still overlapping the last.
    <div className="@container w-full max-w-xl">
      <div className="relative flex flex-col @md:block @md:h-72">
        <div className="rounded-xl border bg-card p-5 pb-10 shadow-raised @md:absolute @md:inset-x-0 @md:top-0 @md:h-56">
          <p className="text-heading-sm">Raised · card</p>
          <p className="text-sm text-muted-foreground">
            A 1px border, no shadow.
          </p>
        </div>
        <div className="-mt-6 ml-4 w-56 max-w-[calc(100%-1rem)] rounded-xl bg-popover p-4 shadow-floating ring-1 ring-edge @md:absolute @md:top-20 @md:left-10 @md:mt-0 @md:ml-0">
          <p className="text-sm font-semibold">Floating · menu</p>
          <p className="text-xs text-muted-foreground">
            Lifted above content it belongs to.
          </p>
        </div>
        <div className="-mt-4 ml-auto w-64 max-w-[calc(100%-2rem)] rounded-2xl bg-popover p-5 shadow-modal ring-1 ring-edge @md:absolute @md:top-32 @md:right-4 @md:mt-0">
          <p className="text-sm font-semibold">Modal · dialog</p>
          <p className="mb-3 text-xs text-muted-foreground">
            Above everything; the page waits.
          </p>
          <div className="flex justify-end">
            <Button size="xs">Confirm</Button>
          </div>
        </div>
      </div>
    </div>
  )
}

Levels

LevelTokenUsed by
0 · Flat—Page content, table rows, inline elements
1 · Raisedshadow-raised (none)Cards, outline buttons, inputs, selectable items
2 · Floatingshadow-floatingMenus, popovers, selects, tooltips, toasts
3 · Modalshadow-modalDialogs, sheets, drawers, command palette

Raised surfaces are drawn by their 1px border alone: an edge or a shadow, never both. shadow-raised resolves to no shadow. It stays a token on cards, outlined items and the floating sidebar, so a theme can give those a shadow in one place by setting --prfct-shadow-raised; inputs and outline buttons don't read it.

Floating and modal surfaces carry a shadow and a hairline ring-edge: a translucent outline that defines the edge on any background, where a shadow alone can disappear.

Shadows

The two shadows components use, floating and modal, are each built from three layers, from a tight contact shadow to a soft ambient one; the smallest steps of the scale below have one or two. In light mode shadows take the hue of the neutral scale, so a tinted gray set in lib/tokens/palette.ts casts a matching shadow. With the default neutral they are plain dark gray, and in dark mode they are black.

shadow-xs
shadow-sm
shadow-md
shadow-lg
shadow-xl
shadow-2xl
shadow-xs
shadow-sm
shadow-md
shadow-lg
shadow-xl
shadow-2xl

Dark mode

Shadows are nearly invisible on dark backgrounds, so dark mode expresses elevation with lighter surfaces and edges instead: cards use gray-2 on a gray-1 page, and menus, popovers and dialogs step up to gray-3 with a brighter hairline ring.

Guidelines

  • Elevation is not emphasis. Don't add shadows to make something look important; use hierarchy and color for that.
  • Don't stack levels arbitrarily. A floating surface belongs to its trigger; a modal belongs to the page. Only tooltips float above a toast.
  • Prefer borders at level 1. A 1px border does more for structure than a shadow, and works in every mode.