Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

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

    Layer renders 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 renders Layer itself.

  • Scrim

    Scrim covers the window behind a layer's content: dim darkens it, as behind a dialog, and blur softens it, as behind a menu that opens over busy content. Put it first inside the Layer, so it sits under what it sets apart.

  • Sticky

    Sticky keeps 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 with FloatingLanes.Item; for toasts, call toast() from @butteryui/react.

FloatingLanes.Item takes the lane as lane, "bottom-center" by default:

LaneWhat goes thereBand
bottom-centerToaststoast, above dialogs
top-endNoticestoast, above dialogs
bottom-endGuidance, such as a setup guidefloating, 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

PropTypeDefaultDescription
band

required

LayerBandThe z band to render in, such as "dropdown", "modal" or "toast".
childrenReactNodeWhat the layer shows. Its own elements take clicks; the rest of the viewport lets them through.

Portal

PropTypeDefaultDescription
containerHTMLElement | nullWhere to render. Defaults to the current layer's container, or document.body on the page.
childrenReactNodeWhat to render there.

Also exported

ExportDescription
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 | nullThe 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.

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.

Halfmoon Roastery

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

PropTypeDefaultDescription
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.

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.

Step 1 of 4: Grind

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.

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.

Recent orders

0 pinned

Variants and states

Stacked stickies

A sub nav sticks under the nav; the panel publishes their total as --bt-sticky-top.

Nav
Sub nav
Row 1Row 2Row 3Row 4Row 5Row 6Row 7Row 8Row 9Row 10Row 11Row 12Row 13Row 14Row 15Row 16Row 17Row 18Row 19Row 20Row 21Row 22Row 23Row 24

Aside

A sidebar sticks under the nav without adding to --bt-sticky-top, so anchors and lanes only clear the nav.

Nav
On this page

Setup

Export

Paragraph 1Paragraph 2Paragraph 3Paragraph 4Paragraph 5Paragraph 6Paragraph 7Paragraph 8Paragraph 9Paragraph 10Paragraph 11Paragraph 12Paragraph 13Paragraph 14Paragraph 15Paragraph 16Paragraph 17Paragraph 18Paragraph 19Paragraph 20Paragraph 21Paragraph 22Paragraph 23Paragraph 24

Props

Sticky

PropTypeDefaultDescription
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.
asChildbooleanRender 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.

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

PropTypeDefaultDescription
laneLaneName"bottom-center"Which lane to stack in. Defaults to "bottom-center", where toasts go.
childrenReactNodeThe item, such as a toast. Menus and tooltips it opens stack above the lane.

Also exported

ExportDescription
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.
laneNamesThe places free-floating UI can live: toasts at the bottom, notices at the top end.
notifyObstacleMoved(): voidTells 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.

  • Elevation

    Surface levels, from the app's background to dialogs, and the shadows that set them apart.

  • Menu

    A list of actions that opens from a trigger, on the floating surface in the dropdown layer.

  • Dialog

    A modal dialog that dims the page, traps focus and hands it back when it closes.