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.
0. Flat
Page content, inline elements.
1. Raised
Cards, panels, controls. A 1px border, never a border and a shadow.
2. Floating
Menus, popovers, tooltips, select lists.
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
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.
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.