Layers
The z bands menus, dialogs, toasts and tooltips open into, and the primitives that put things in them.
A layer is a band of the page's stacking order, such as dropdown, modal or toast, with a container that covers the window. Use the layer primitives for anything that floats over the page, rather than a z-index of your own, so a menu inside a dialog still opens above the dialog and a toast stays above both.
The bands
Each band is a token, --bt-z-<band>, listed here from the lowest. buttery's stylelint config fails a z-index that isn't one of them.
- --bt-z-behind
- -1
- --bt-z-base
- 0
- --bt-z-raised
- 10
- --bt-z-scrim-nav
- 15
- --bt-z-sticky
- 20
- --bt-z-floating
- 30
- --bt-z-dropdown
- 40
- --bt-z-overlay
- 50
- --bt-z-modal
- 60
- --bt-z-toast
- 70
- --bt-z-tooltip
- 80
- --bt-z-dev
- 90
The layer primitives
Four primitives put things over the page or keep them in view while it scrolls. Each has its own section below, with its examples and props.
Layer
Layerrenders its children above the page in the band you name, in a container that covers the window but lets clicks through. A layer opened from inside another one stacks above it. Menus, popovers, dialogs and tooltips open their own layers, so most code never rendersLayeritself.Scrim
Scrimcovers the window behind a layer's content:dimdarkens it, as behind a dialog, andblursoftens it, as behind a menu that opens over busy content. Put it first inside theLayer, so it sits under what it sets apart.Sticky
Stickykeeps a bar at the top or bottom of its scroll container while the rest scrolls, and stacks with the other sticky bars there instead of covering them.variant="aside"makes a sticky side column, such as this site's sidebar.FloatingLanes
Toasts, notices and guides that float over the page go in a lane, so they stack instead of covering each other. Lanes move out of the way of sticky bars and anything registered with
useFloatingObstacle. Render into one withFloatingLanes.Item; for toasts, calltoast()from@butteryui/react.
FloatingLanes.Item takes the lane as lane, "bottom-center" by default:
| Lane | What goes there | Band |
|---|---|---|
bottom-center | Toasts | toast, above dialogs |
top-end | Notices | toast, above dialogs |
bottom-end | Guidance, such as a setup guide | floating, under dialogs |
Layer
When to use it
Use it for
- Building an overlay that isn't covered by an existing one: a modal panel, a drawer, a blocking progress panel
Not for
- Menus, popovers and floating panels: use Popover
- Toasts and notices: use FloatingLanes.Item
- Moving content into a layer that already exists: use Portal
Examples
Publish progress
A publish queue: while new coffees go up in the shop, a panel in the modal layer holds the page and counts them.
Guji
Draft
Nyeri
Draft
Huehuetenango
Draft
Not published yet
Saving overlay
A "Saving" panel in the modal layer, over a dim scrim, that holds the page until the save is done.
Not saved yet
Variants and states
Nested layers
A dropdown opened inside a modal mounts in the modal's container; a tooltip would go to the root.
Props
Layer
| Prop | Type | Default | Description |
|---|---|---|---|
bandrequired | LayerBand | The z band to render in, such as "dropdown", "modal" or "toast". | |
children | ReactNode | What the layer shows. Its own elements take clicks; the rest of the viewport lets them through. |
Portal
| Prop | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | null | Where to render. Defaults to the current layer's container, or document.body on the page. | |
children | ReactNode | What to render there. |
Also exported
| Export | Description |
|---|---|
LayerContext: Context<LayerInfo | null> | The layer a subtree renders in: Layer provides it, useCurrentLayer reads it, and a lane host passes its own on. |
useCurrentLayer(): LayerInfo | null | The layer the calling component renders inside, or null on the page. |
Accessibility
No role of its own. The layer's content keeps its own semantics; focus management belongs to the component inside (Popover, Dialog).
It takes no keyboard input of its own.
Motion
It doesn't move.
With reduced motion: Nothing changes.
Layout
Its container covers the window at its band's z-index, --bt-z-<band>, and lets presses through, so only the elements inside it take them; it has no size or hit area of its own. A layer opened inside another whose band is no higher mounts in that layer's container and stacks above it by order alone.
Related
See it in the Deckhand demo.
Scrim
When to use it
Use it for
- Behind a modal dialog (dim)
- Behind a menu that opens over page content (blur)
Not for
- Hiding content: unmount it instead
- Behind popovers that don't block the page
Examples
Blur behind menu
A top bar's menu that opens over the page on a blurred scrim; choosing a page or pressing the scrim closes it.
Browsing Coffee
Dim behind panel
A dim scrim first inside a modal layer, under a panel that holds the page while a price list applies.
8 cafés on this price list
Dim or blur
Both variants, drawn by Popover's scrim prop: dim darkens the page, blur softens it under a light wash.
Variants and states
Variants
Dim behind modals; blur under a light wash behind menus over content.
Props
Scrim
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "dim" | "blur" | "dim" | "dim" (the default) darkens the page behind a modal; "blur" blurs it under a light wash, for menus
that open over content. |
It also takes Omit<ComponentPropsWithRef<"div">, "children">.
Accessibility
Hidden from assistive technology (aria-hidden). The component inside the layer (a Radix Dialog, or Popover with its scrim prop) moves focus in, closes on Escape and treats a click on the scrim as a click outside.
It takes no keyboard input of its own.
Motion
It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.
With reduced motion: It has no motion of its own: a dialog, sheet, nav bar or popover fades it with its layer, over 100ms with reduced motion, and a popover with an instant entrance shows and drops it at once.
Layout
It covers the whole window inside its layer and draws only a fill: --bt-overlay for dim, or --bt-wash over a 10px backdrop blur (--bt-blur-scrim) for blur. The whole scrim takes presses, which reach the component it sits under as a press outside.
Related
See it in the Deckhand demo.
Sticky
When to use it
Use it for
- Sticky nav bars, sub navs and panel footers
- Anything anchors and floating lanes need to stay clear of
- Sidebars and tables of contents that follow the scroll: variant="aside"
Not for
- Free-floating UI over the page: use a Layer
- Inspector footers inside a flex column: lay them out in flow
Examples
Guide with aside
Each variant in one scrolling guide: a bar at the top edge, a step list as an aside, a bar at the bottom edge.
Grind
Weigh 20g of beans and grind them medium-fine, like coarse sand.
Grind just before you brew; ground coffee goes flat within minutes.
Bloom
Pour 40g of water just off the boil and wait 30 seconds while the grounds rise.
Fresh beans bloom high; older ones barely move.
Pour
Pour in slow circles until the scale reads 320g, keeping the bed level.
Aim to finish pouring by the two-minute mark.
Serve
Let it drain, swirl the carafe and pour while it's still hot.
The whole brew takes about three and a half minutes.
Panel footer
A settings panel whose Save stays at the bottom while the list scrolls, and waits until something changes.
No unsaved changes
Scroll header
A header that stays at the top of its scrolling panel, counting the orders pinned below it.
0 pinned
Variants and states
Stacked stickies
A sub nav sticks under the nav; the panel publishes their total as --bt-sticky-top.
Aside
A sidebar sticks under the nav without adding to --bt-sticky-top, so anchors and lanes only clear the nav.
Setup
Export
Props
Sticky
| Prop | Type | Default | Description |
|---|---|---|---|
edge | "top" | "bottom" | "top" | The edge of the scroll container to hold to. Defaults to "top". |
variant | "bar" | "aside" | "bar" | "bar" (the default) runs across the scroll container, such as a nav bar or a panel footer, and
counts toward the published height. "aside" is a column beside the content, such as a sidebar or a
table of contents: it sits under the bars, doesn't count, and fits the space between the bars,
scrolling inside when its content is taller. Start it right under the bars (a sticky header above it)
so its whole box is on screen from the first frame. |
asChild | boolean | Render the single child element instead of a div, with Sticky's props merged onto it. |
It also takes ComponentPropsWithRef<"div">.
Accessibility
No role of its own; keep sticky content in source order so it's read where it appears.
It takes no keyboard input of its own.
Motion
It doesn't move.
With reduced motion: Nothing changes.
Layout
A bar takes its parent's width and its content's height; bars on one edge stack, and the scroll container publishes their total as --bt-sticky-top or --bt-sticky-bottom. An aside sticks under the bars without counting toward them and fits between them, scrolling inside when its content is taller. It adds no hit area of its own.
Related
FloatingLanes
When to use it
Use it for
- Notices that float over the app, such as an update that's ready or an export that's running (bottom-end, top-end)
- Custom free-floating UI that has to keep clear of sticky bars and toolbars
Not for
- Toasts: call toast() from @butteryui/react, which uses the bottom-center lane and keeps three at most
- UI anchored to an element: use Popover
- Inline status messages: put them in flow
Examples
Export progress
A notice in the bottom-end lane that stays while an export runs and can cancel it, beside a status line in the page.
Nothing exporting
Update notice
A notice in the top-end lane that the user can put away, with a status line in the page that says the same.
No update waiting
Variants and states
Lanes
Items stack 8px apart and stay 24px from the edges and clear of sticky bars. A lane has room for three, so a fourth here lets the oldest go.
Props
FloatingLanes.Item
| Prop | Type | Default | Description |
|---|---|---|---|
lane | LaneName | "bottom-center" | Which lane to stack in. Defaults to "bottom-center", where toasts go. |
children | ReactNode | The item, such as a toast. Menus and tooltips it opens stack above the lane. |
Also exported
| Export | Description |
|---|---|
useFloatingObstacle(): RefCallback<HTMLElement> | Registers an element that floating lanes must stay clear of, such as a toolbar anchored under a crop
box. Lane items move above or below it instead of covering it. Returns a callback ref for the element,
which useMergedRefs combines with another ref. The lanes measure it again when it changes size; call
notifyObstacleMoved() when it moves without changing size. |
laneNames | The places free-floating UI can live: toasts at the bottom, notices at the top end. |
notifyObstacleMoved(): void | Tells the floating lanes that an obstacle moved without changing size, such as a toolbar following its anchor, so they keep clear of where it is now. |
Accessibility
Lanes are plain containers and items bring their own semantics. To announce an item, keep a live region (role="status") mounted outside the lane and change its text: a region that mounts along with its content often goes unannounced.
It takes no keyboard input of its own.
Motion
It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.
With reduced motion: The lanes place items without animating; an item that arrives with a presence, such as a toast, fades in and out over 100ms without rising, and the items around it take their new places at once.
Layout
Each lane stacks its items in a column 8px apart, 24px in from the window's edges and clear of sticky bars, registered obstacles and a docked side panel. An item is never wider than its lane, the window less 24px a side, so its text wraps or truncates inside it; the lanes let the pointer through and only the items take it.