Surface and nesting
How surfaces nest, with concentric corners and even insets, and the Surface primitive that does it.
A surface is a region with a background, an edge and a radius, such as a panel, a section well or a card. Nest surfaces with Surface, and each inner one takes its corners from the one around it, so a screen of nested regions reads as one shape.
The nesting recipe
Panel, section, card
buttery's screens nest a few levels deep: a
panelon the app background, asectionwell inside it, andcards inside the well, each with rows inside.Corners are concentric
A child's radius is its parent's radius less the parent's padding, which
radius="auto"works out. The one deliberate exception is a section, which is sized from the card inside it.Insets are even
A nested surface sits evenly inside its parent, with the same inset on every open side.
Titles line up
A section's title starts on the same edge as the rows inside it.
In code, each level is a Surface, and the card takes radius="auto":
import type { ReactElement } from "react";
import { Stack, Surface, Text } from "@butteryui/core";
export function Settings(): ReactElement {
return (
<Surface level="panel">
<Surface level="section">
<Stack gap={2}>
<Text variant="control">Notifications</Text>
<Surface level="card" radius="auto">
<Text variant="label">Email me when an order ships</Text>
</Surface>
</Stack>
</Surface>
</Surface>
);
}Here is the recipe at full size, in an editor's inspector:
Your canvas is empty
Start a fresh card, or open one from your library.
Surface
When to use it
Use it for
- Panels, sections, cards, floating menus and dialogs
- Nesting surfaces with concentric corners (radius="auto")
Not for
- Drawing a box with custom shadows or radii: pick the level that fits
Examples
Floating menu
The floating level used by menus and popovers, holding a short list.
Nested section
A panel holding a section whose card nests with concentric corners.
Variants and states
Levels
Background, edge, shadow and radius for each level.
Concentric nesting
Each inner surface uses radius="auto": the parent's radius minus its padding.
Never less than 4px.
Props
Surface
| Prop | Type | Default | Description |
|---|---|---|---|
level | SurfaceLevel | "card" | Background, edge, shadow, corner radius and padding, by level. Defaults to "card". |
radius | "auto" | RadiusToken | Corner radius. "auto" nests concentrically inside the surface around it: the parent's radius minus
the parent's padding, never less than 4px. Defaults to the level's own radius. | |
pad | SpaceValue | Padding on every side, on the spacing scale. Replaces the level's padding. | |
padX | SpaceValue | Left and right padding, on the spacing scale. | |
padY | SpaceValue | Top and bottom padding, on the spacing scale. | |
asChild | boolean | Render the single child element instead of a div, with Surface's props merged onto it. |
It also takes ComponentPropsWithRef<"div">.
Accessibility
No role of its own; add a landmark role or aria-label when a surface is a region the user navigates to.
It takes no keyboard input of its own.
Motion
It doesn't move.
With reduced motion: Nothing changes.
Layout
It's as wide as its parent and as tall as its content and padding. The padding comes from its level (8px for panel and dialog, 6px for floating, a 4px well on three sides for section, none for the rest) unless pad, padX or padY replace it. With radius set to auto, its corners take the parent surface's radius less that surface's padding, never under 4px. It has no hit area of its own: to make one pressable, put a Pressable inside it with asChild.