Elevation
How Edmi expresses depth: four levels, role-based defaults, a layered mode for a whole page, and the nesting rules. Philosophy first.
Edmi is calm and editorial by default. Depth is the one tool it adds on top of the plain shadcn look, and it is used to say something: what is an action, what receives input, what sits above the page. This page explains the thinking first, then the levels, the roles, the layered mode and the rules that keep it tidy.
Philosophy
Section titled “Philosophy”Flat is the default. Every component renders the plain shadcn look: a solid fill and a 1px border. That is what
keeps dense app UI quiet and readable, and it means add @edmi-ui/<name> --overwrite over a stock project never
surprises anyone. Depth is opt-in, either per component or for a whole scope.
Depth expresses role, not decoration. A global skin that puts a shadow on everything makes everything shout. Edmi decides depth by what a thing does:
- the controls that do something rise (a filled button, a card you act on, a switch thumb you drag);
- the controls that receive input sink, so a field reads as a well;
- the things that sit above the page float (popovers, menus, dialogs, the chat composer);
- everything else, the frame and the quiet parts, stays flat.
Levels are relative to the parent. A child is never more than one step above what holds it. A card inside a raised card drops back to flat and keeps its border: no bevel on bevel. Buttons and fields are exempt, because they are what you reach for inside any surface.
One step only, and only the part that is active. Tabs, a segmented control, pagination, a calendar, a switch and a slider never bevel their track, list or grid. Only the active tab, the selected day, the checked box and the thumb rise. Alerts, toasts, tooltips and brand badges stay soft fill with a tinted border; floating is simply the natural level for toasts.
A bevel, not a lip. Earlier versions used a hard lip under raised things. It read as a stack of slabs and made nesting awkward. The bevel is one quiet language everywhere: a top highlight, an inner rim and a dark hairline. Floating adds exactly one soft drop on top of the same bevel. Sunken is a soft inset with no hard dark edge, and when a sunken field takes focus the ring replaces its edge. Pressed sinks 1px. Elevation never changes a component’s size, so control heights stay 36 / 32 / 42.
Layered mode applies the policy to a page. You can raise things one by one, or wrap a subtree in
ElevationProvider mode="layered" and let every role take the level it is meant to have. Same markup, two modes. An
explicit elevation prop on a component always wins, so you can still opt one call-to-action in or out.
The four levels
Section titled “The four levels”elevation is one prop with five values, available on every component that can take depth. Default "auto".
| Value | Level | Look | Tokens |
|---|---|---|---|
sunken |
-1 | soft inset well | bg-sk-bg border-sk-bd shadow-sunken |
flat |
0 | fill and 1px border | none |
raised |
+1 | bevel: rim, top highlight, hairline | shadow-raised, buttons shadow-btn-raised-* |
floating |
+2 | the bevel plus one soft drop | shadow-floating, buttons shadow-btn-float-* |
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Nested: a card inside a raised card resolves flat
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Nested: a card inside a raised card resolves flat
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Nested: a card inside a raised card resolves flat
import { Button } from "@/components/ui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-6"> <div className="flex flex-wrap items-start gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Card elevation={value} size="sm" className="w-64"> <CardHeader> <CardTitle>Join MAG4</CardTitle> <CardDescription>Magnificent Four, 4 tokens</CardDescription> </CardHeader> <CardContent className="font-mono text-sm"> Amount 1,000 USDC </CardContent> <CardFooter> <Button size="sm">Join index</Button> </CardFooter> </Card> </div> ))} </div> <div className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground"> Nested: a card inside a raised card resolves flat </p> <Card elevation="raised" size="sm" className="w-80"> <CardHeader> <CardTitle>Portfolio</CardTitle> </CardHeader> <CardContent> <Card size="sm"> <CardHeader> <CardTitle>Nested card</CardTitle> <CardDescription> Border only, no bevel on bevel. </CardDescription> </CardHeader> </Card> </CardContent> </Card> </div> </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<template> <div class="flex flex-col gap-6"> <div class="flex flex-wrap items-start gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <Card :elevation="level.value" size="sm" class="w-64"> <CardHeader> <CardTitle>Join MAG4</CardTitle> <CardDescription>Magnificent Four, 4 tokens</CardDescription> </CardHeader> <CardContent class="font-mono text-sm">Amount 1,000 USDC</CardContent> <CardFooter> <Button size="sm">Join index</Button> </CardFooter> </Card> </div> </div> <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">Nested: a card inside a raised card resolves flat</p> <Card elevation="raised" size="sm" class="w-80"> <CardHeader> <CardTitle>Portfolio</CardTitle> </CardHeader> <CardContent> <Card size="sm"> <CardHeader> <CardTitle>Nested card</CardTitle> <CardDescription>Border only, no bevel on bevel.</CardDescription> </CardHeader> </Card> </CardContent> </Card> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import * as Card from "$lib/components/ui/card";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-6"> <div class="flex flex-wrap items-start gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <Card.Root elevation={level.value} size="sm" class="w-64"> <Card.Header> <Card.Title>Join MAG4</Card.Title> <Card.Description>Magnificent Four, 4 tokens</Card.Description> </Card.Header> <Card.Content class="font-mono text-sm">Amount 1,000 USDC</Card.Content> <Card.Footer> <Button size="sm">Join index</Button> </Card.Footer> </Card.Root> </div> {/each} </div> <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">Nested: a card inside a raised card resolves flat</p> <Card.Root elevation="raised" size="sm" class="w-80"> <Card.Header> <Card.Title>Portfolio</Card.Title> </Card.Header> <Card.Content> <Card.Root size="sm"> <Card.Header> <Card.Title>Nested card</Card.Title> <Card.Description>Border only, no bevel on bevel.</Card.Description> </Card.Header> </Card.Root> </Card.Content> </Card.Root> </div></div>auto resolves in this order: the component’s own elevation prop, the nearest ElevationProvider, then flat.
Roles and layered defaults
Section titled “Roles and layered defaults”In a layered scope each component has a role, and each role has a default level. An explicit prop always wins.
| Role | Components | Layered level |
|---|---|---|
filled action (button-filled) |
Button default, secondary, destructive, brand | +1 raised |
quiet action (button-quiet) |
Button outline, ghost, link | 0 flat |
| field | Input, Textarea, Select trigger, Input OTP, Input Group | -1 sunken |
| control | Checkbox, Radio, Tabs, Segmented, Pagination, Badge | 0 flat (the active part rises when raised) |
| handle | Switch thumb, Slider thumb, calendar selected day, Kbd, Empty media | +1 raised |
| surface | Card, node, list-row card, Inset panel body | +1 raised |
| container | Inset panel shell, nested card, Alert, Toast, Tooltip | 0 flat |
| overlay | Popover, Dropdown, Select menu, Dialog, chat composer | +2 floating |
The full list of components that take elevation, and what rises on each, is on the component pages under
Elevation ✦.
Flat or layered?
Section titled “Flat or layered?”| Use flat (default) when | Use layered when |
|---|---|
| the screen is dense: forms, tables, settings, admin | the page should feel tactile: dashboards, marketing, product tours |
| you want the calm, plain shadcn look | you want roles to read at a glance: actions up, fields down, overlays above |
| you raise one or two things yourself | most of the page should follow the role policy |
Pick one per page. Do not mix a layered page with hand-raised random controls; override the few that need it.
field raised · button raised · surface flat · overlay raised
field raised · button raised · surface raised · overlay raised
field raised · button raised · surface raised · overlay raised
import { Button } from "@/components/ui/button";import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";import { ElevationProvider, SurfaceProvider, useElevation,} from "@/components/ui/elevation";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover";
/* What each role resolves to inside the scope (the same call every component makes). */function Resolved() { const field = useElevation(undefined, "field"); const button = useElevation(undefined, "button-filled"); const surface = useElevation(undefined, "surface"); const overlay = useElevation(undefined, "overlay"); return ( <p className="font-mono text-[11.5px] text-muted-foreground"> field {field} · button {button} · surface {surface} · overlay {overlay} </p> );}
function Scope({ title, children,}: { title: string; children: React.ReactNode;}) { return ( <div className="flex flex-col gap-3"> <div className="text-xs font-semibold text-muted-foreground">{title}</div> {children} </div> );}
function Sample() { return ( <Card className="w-72"> <CardHeader> <CardTitle>Rebalance limits</CardTitle> </CardHeader> <CardContent className="flex flex-col gap-3"> <Input defaultValue="5%" /> <div className="flex gap-2"> <Button>Save</Button> <Popover> <PopoverTrigger render={<Button variant="outline" />}> More </PopoverTrigger> <PopoverContent className="w-48 text-[13px]"> Applied to the next keeper run. </PopoverContent> </Popover> </div> <Resolved /> </CardContent> </Card> );}
/* Token check: the v4 shadow utilities and bevel / sunken variables, straight from the theme map. */function Swatches() { return ( <div className="flex flex-wrap items-center gap-4"> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs"> flat </div> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised"> shadow-raised </div> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating"> shadow-floating </div> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken"> shadow-sunken </div> </div> );}
export default function Demo() { return ( <div className="flex flex-col gap-6"> <div className="flex flex-wrap gap-8"> <Scope title="Flat (default)"> <ElevationProvider> <Sample /> </ElevationProvider> </Scope> <Scope title='mode="layered"'> <ElevationProvider mode="layered"> <Sample /> </ElevationProvider> </Scope> <Scope title='level="raised" (forced)'> <ElevationProvider level="raised"> <SurfaceProvider level="raised"> <Resolved /> </SurfaceProvider> </ElevationProvider> </Scope> </div> <Swatches /> </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";import { ElevationProvider, useElevation } from "@/components/ui/elevation";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";import { defineComponent, h } from "vue";
// What each role resolves to inside the scope (the same call every component makes).const Resolved = defineComponent(() => { const field = useElevation(() => undefined, "field"); const button = useElevation(() => undefined, "button-filled"); const surface = useElevation(() => undefined, "surface"); const overlay = useElevation(() => undefined, "overlay"); return () => h( "p", { class: "font-mono text-[11.5px] text-muted-foreground" }, `field ${field.value} · button ${button.value} · surface ${surface.value} · overlay ${overlay.value}`, );});</script>
<template> <div class="flex flex-col gap-6"> <div class="flex flex-wrap gap-8"> <div v-for="scope in [{ title: 'Flat (default)', mode: undefined }, { title: 'mode="layered"', mode: 'layered' as const }]" :key="scope.title" class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">{{ scope.title }}</div> <ElevationProvider :mode="scope.mode"> <Card class="w-72"> <CardHeader> <CardTitle>Rebalance limits</CardTitle> </CardHeader> <CardContent class="flex flex-col gap-3"> <Input default-value="5%" /> <div class="flex gap-2"> <Button>Save</Button> <Popover> <PopoverTrigger as-child> <Button variant="outline">More</Button> </PopoverTrigger> <PopoverContent class="w-48 text-[13px]">Applied to the next keeper run.</PopoverContent> </Popover> </div> <Resolved /> </CardContent> </Card> </ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">level="raised" (forced)</div> <ElevationProvider level="raised"> <Resolved /> </ElevationProvider> </div> </div> <div class="flex flex-wrap items-center gap-4"> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs">flat</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised">shadow-raised</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating">shadow-floating</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken">shadow-sunken</div> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import * as Card from "$lib/components/ui/card"; import { ElevationProvider } from "$lib/components/ui/elevation"; import { Input } from "$lib/components/ui/input"; import * as Popover from "$lib/components/ui/popover"; import Resolved from "./_shared/elevation-resolved.svelte";</script>
{#snippet sample()} <Card.Root class="w-72"> <Card.Header> <Card.Title>Rebalance limits</Card.Title> </Card.Header> <Card.Content class="flex flex-col gap-3"> <Input value="5%" /> <div class="flex gap-2"> <Button>Save</Button> <Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>More</Button> {/snippet} </Popover.Trigger> <Popover.Content class="w-48 text-[13px]">Applied to the next keeper run.</Popover.Content> </Popover.Root> </div> <Resolved /> </Card.Content> </Card.Root>{/snippet}
<div class="flex flex-col gap-6"> <div class="flex flex-wrap gap-8"> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">Flat (default)</div> <ElevationProvider>{@render sample()}</ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">mode="layered"</div> <ElevationProvider mode="layered">{@render sample()}</ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">level="raised" (forced)</div> <ElevationProvider level="raised"><Resolved /></ElevationProvider> </div> </div> <div class="flex flex-wrap items-center gap-4"> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs">flat</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised">shadow-raised</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating">shadow-floating</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken">shadow-sunken</div> </div></div>Using the provider
Section titled “Using the provider”Install it with any component that takes elevation (it is a dependency), or on its own with
add @edmi-ui/elevation. Wrap the app, or any subtree, in ElevationProvider.
import { ElevationProvider } from "@/components/ui/elevation";
export function App() { return ( <ElevationProvider mode="layered"> <Dashboard /> </ElevationProvider> );}<script setup lang="ts">import { ElevationProvider } from "@/components/ui/elevation";</script>
<template> <ElevationProvider mode="layered"> <Dashboard /> </ElevationProvider></template><script lang="ts"> import { ElevationProvider } from "$lib/components/ui/elevation"; let { children } = $props();</script>
<ElevationProvider mode="layered"> {@render children()}</ElevationProvider>mode="layered"gives each role its default level.mode="flat"is the default.level="raised"(orsunken,flat,floating) forces one level for the whole subtree and wins overmode.- The provider is layout-neutral (
display: contents) unless you pass a class, and it rendersdata-elevationfor CSS. - Scopes nest: a child provider can set its own mode.
Override one component
Section titled “Override one component”Props beat the scope in both directions.
<ElevationProvider mode="layered"> <Button elevation="floating">Create index</Button> {/* the one hero action */} <Button elevation="flat" variant="secondary">Cancel</Button></ElevationProvider>
<Card elevation="raised">…</Card> {/* flat page, one raised card */}<Input elevation="sunken" /><ElevationProvider mode="layered"> <Button elevation="floating">Create index</Button> <Button elevation="flat" variant="secondary">Cancel</Button></ElevationProvider>
<Card elevation="raised">…</Card><Input elevation="sunken" /><ElevationProvider mode="layered"> <Button elevation="floating">Create index</Button> <Button elevation="flat" variant="secondary">Cancel</Button></ElevationProvider>
<Card.Root elevation="raised">…</Card.Root><Input elevation="sunken" />Nesting
Section titled “Nesting”Levels are relative to the parent. A surface (a card, a panel body) inside a raised or floating surface drops to flat
and keeps its border, so you never get bevel on bevel. Buttons and fields are exempt and keep their role level inside
any surface. An explicit elevation still wins, but a card at +1 on a card at +1 is almost always a mistake.
The card demo above ends with a nested card: it resolves flat inside the raised one. An Inset panel follows the same idea: the shell stays flat as the frame, the body is the raised plate inside it (inset 2px so the depth reads), and a card inside that body drops to flat.
Only the active part rises
Section titled “Only the active part rises”Components made of a track and an active item never bevel the track.
| Component | What rises with raised |
|---|---|
| Tabs | the active trigger (line never) |
| Toggle group, segmented | the ON item |
| Pagination | the active link |
| Calendar, date picker | the selected day; elevation sits on the shell (popover or card), never the day grid |
| Switch, Slider | the thumb or thumbs |
| Checkbox | the checked box |
| Toggle (default) | shows depth only when ON |
Flat (0)
Raised (+1): only the active tab rises
Flat (0)
Raised (+1): only the active tab rises
Flat (0)
Raised (+1): only the active tab rises
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1): only the active tab rises" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="flex flex-wrap items-start gap-6"> {(["default", "pills"] as const).map((variant) => ( <Tabs key={variant} defaultValue="overview" className="w-[360px]"> <TabsList variant={variant} elevation={value}> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="holdings">Holdings</TabsTrigger> <TabsTrigger value="activity">Activity</TabsTrigger> </TabsList> <TabsContent value="overview">{variant} tabs.</TabsContent> <TabsContent value="holdings"> Four tokens, weighted by AUM. </TabsContent> <TabsContent value="activity">No recent trades.</TabsContent> </Tabs> ))} </div> </div> ))} </div> );}<script setup lang="ts">import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1): only the active tab rises" }] as const;const variants = ["default", "pills"] as const;</script>
<template> <div class="flex flex-col gap-6"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="flex flex-wrap items-start gap-6"> <Tabs v-for="variant in variants" :key="variant" default-value="overview"> <TabsList :variant="variant" :elevation="level.value"> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="holdings">Holdings</TabsTrigger> <TabsTrigger value="activity">Activity</TabsTrigger> </TabsList> </Tabs> </div> </div> </div></template><script lang="ts"> import * as Tabs from "$lib/components/ui/tabs";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1): only the active tab rises" }] as const; const variants = ["default", "pills"] as const;</script>
<div class="flex flex-col gap-6"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="flex flex-wrap items-start gap-6"> {#each variants as variant (variant)} <Tabs.Root value="overview"> <Tabs.List {variant} elevation={level.value}> <Tabs.Trigger value="overview">Overview</Tabs.Trigger> <Tabs.Trigger value="holdings">Holdings</Tabs.Trigger> <Tabs.Trigger value="activity">Activity</Tabs.Trigger> </Tabs.List> </Tabs.Root> {/each} </div> </div> {/each}</div>Button groups
Section titled “Button groups”On a ButtonGroup, raised raises each item. floating floats the whole group as one plate while the items stay
raised, so you never get a drop shadow per item.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="flex flex-wrap items-center gap-4"> <ButtonGroup elevation={value}> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup> <ButtonGroup elevation={value}> <Button variant="secondary">Buy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Sell</Button> </ButtonGroup> </div> </div> ))} </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="flex flex-wrap items-center gap-4"> <ButtonGroup :elevation="level.value"> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup> <ButtonGroup :elevation="level.value"> <Button variant="secondary">Buy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Sell</Button> </ButtonGroup> </div> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { ButtonGroup, ButtonGroupSeparator } from "$lib/components/ui/button-group";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="flex flex-wrap items-center gap-4"> <ButtonGroup elevation={level.value}> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup> <ButtonGroup elevation={level.value}> <Button variant="secondary">Buy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Sell</Button> </ButtonGroup> </div> </div> {/each}</div>Buttons: three depths
Section titled “Buttons: three depths”Flat, raised (+1) and floating (+2). Use floating for the one hero action in a view. Filled buttons keep their colour
when sunken (8% darker). ghost follows the neutral faces and link never gets depth. Pressed sinks 1px.
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
import { Button } from "@/components/ui/button";
const variants = [ "default", "secondary", "outline", "destructive", "brand", "ghost",] as const;
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="flex flex-wrap items-center gap-3"> {variants.map((variant) => ( <Button key={variant} variant={variant} elevation={value}> {variant} </Button> ))} <Button variant="link" elevation={value}> link </Button> </div> </div> ))} </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";
const variants = ["default", "secondary", "outline", "destructive", "brand", "ghost"] as const;
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="flex flex-wrap items-center gap-3"> <Button v-for="variant in variants" :key="variant" :variant="variant" :elevation="level.value">{{ variant }}</Button> <Button variant="link" :elevation="level.value">link</Button> </div> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button";
const variants = ["default", "secondary", "outline", "destructive", "brand", "ghost"] as const;
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="flex flex-wrap items-center gap-3"> {#each variants as variant (variant)} <Button {variant} elevation={level.value}>{variant}</Button> {/each} <Button variant="link" elevation={level.value}>link</Button> </div> </div> {/each}</div>Do and don’t
Section titled “Do and don’t”Do
- Start flat. Add layered mode to pages that benefit from it.
- Let roles decide: actions rise, fields sink, overlays float.
- Use one floating action per view.
- Override a single component with the prop when the page needs an exception.
Don’t
- Raise everything. If every control has depth, none of them says “primary”.
- Stack bevels: no raised card inside a raised card, no floating inside floating.
- Hand-write shadows, gradients or lips to imitate it. Use the prop or the tokens.
- Add transparency to surfaces for effect. Floating layers are always solid.
Dark mode
Section titled “Dark mode”Depth in dark comes from the bevel hairline and a faint top highlight, never from black blocks. The stone dark palette uses a clearer ladder so each step is visible: background, card, popover, secondary, border, input. See Theming for the tokens.
See also
Section titled “See also”- Rules: the design decisions, including the bevel model.
- Elevation component:
ElevationProvider,useElevationand the API. - Upgrading: moving from
raisedtoelevation.