Skip to content

CSS

.sheet {
background: white;
border-radius: 16px 16px 0 0;
box-shadow: 0 -8px 40px rgb(0 0 0 / 0.16);
}

scrollsheet ships mechanics only (position, transform, scroll-snap, backdrop, focus containment) in scrollsheet/styles.css; import it once next to your components, the same convention vaul uses. The className on Sheet.Content lands on the panel, so style it like any card.

Open it and step through menu, key, and remove. One className, no default sheet look.

No bundler CSS handling, Shadow DOM, or a strict CSP? Import from scrollsheet/auto instead: the same components with the stylesheet embedded, injected the first time a sheet opens. Sheets only render after JS runs (a native dialog is display:none until opened), so runtime injection cannot flash unstyled content.

import { Sheet } from "scrollsheet/auto";

The embed costs 21.2 kB gzip for Sheet against the default entry’s 16.9. Toasts on the default entry need both files: scrollsheet/styles.css and scrollsheet/toast.css. Never mix the two entries in one app; you would bundle every component twice.

Every number above is measured on a production build. Dev-only warning strings sit behind a development/production export condition, the same one Vite, webpack 5+, and Next.js all set automatically, so they never reach a production bundle. A bundler that doesn’t set that condition just serves the production build in dev too: no warnings, nothing broken.

themeColorDimming goes further: its controller loads from a separate, lazily-fetched chunk, so an app that never sets the prop never ships it.

Light is the default palette everywhere. An app with no theming of its own never flips dark just because the visitor’s OS is set to dark: the panel, handle, and close button all keep their light-mode colors regardless of prefers-color-scheme.

Opt into dark by setting data-scrollsheet-theme="dark" on an ancestor, typically <html>:

<html data-scrollsheet-theme="dark">

To make the panel track the OS scheme automatically, use "system" instead:

<html data-scrollsheet-theme="system">

Any other value, or no attribute at all, renders light. The attribute is page-level: any ancestor carrying it switches the built-in defaults, and a nested data-scrollsheet-theme="light" does not switch a subtree back. To theme one sheet differently from the rest of the page, set the tokens on that sheet instead.

Your own --scrollsheet-bg, --scrollsheet-fg, --scrollsheet-handle, --scrollsheet-handle-hover, --scrollsheet-close-bg, --scrollsheet-close-color, --scrollsheet-close-bg-hover, and --scrollsheet-close-color-hover overrides win in every mode: the attribute only changes which built-in fallback color those variables fall back to.

Toaster renders each card as .scrollsheet-toast (icon: .scrollsheet-toast-icon, body: .scrollsheet-toast-body, title: .scrollsheet-toast-title, description: .scrollsheet-toast-description, actions: .scrollsheet-toast-action / .scrollsheet-toast-cancel / .scrollsheet-toast-close) and reads its layout knobs from --scrollsheet-toast-* custom properties: --scrollsheet-toast-gap, --scrollsheet-toast-offset, --scrollsheet-toast-mobile-offset. Override either the same way you’d style any card:

.scrollsheet-toast {
border-radius: 12px;
}

Arrived with an existing Sonner integration? Its import { toast, Toaster } from 'scrollsheet' compat layer also stamps the matching sonner-* class and data-sonner-* attribute on every element, so your existing .sonner-toast {} CSS keeps matching unmodified. See Migrate from Sonner. Starting fresh, with no Sonner CSS to keep matching? <Toaster sonnerCompat={false}> drops the mirrored names and renders the neutral .scrollsheet-* classes and attributes only.