Styling

Theme Cloudlet with tokens and class names, and try them in the Style editor.

On this page

Cloudlet uses one external stylesheet and CSS tokens scoped to .cloudlet-host. The editor below lets you try them on a live Toaster and copy the CSS; every setting is a Toaster prop listed in Toaster props.

Style editor

Light theme
Colors

Light theme colors. Switch the site theme to edit the dark ones.

Shape
Type
notifications.css
/* Nothing changed yet */

The stylesheet

Import @cloudlet/toast/styles.css once, from your global CSS entry or root layout (see Installation). Rules and resets are scoped to .cloudlet-host.

Keep it external under a strict Content Security Policy; Content Security Policy has the rest of the setup.

Put overrides in a stylesheet imported after Cloudlet's. Use tokens and class names to change the look; the renderer controls width, height, position, transform and clip path.

Tokens

Set tokens on .cloudlet-host. Text uses --cloudlet-font-family (system-ui, sans-serif) and --cloudlet-font-size (.875rem); descriptions and actions are smaller.

TokenDefaultDescription
--cloudlet-pill-height44pxMinimum height of the collapsed pill.
--cloudlet-pill-radius9999pxCorner radius of the pill and its tails.
--cloudlet-pill-max-width22remThe pill grows with its title up to this width.
--cloudlet-island-radius24pxCorner radius of the open Island.
--cloudlet-island-max-width26remWidth of the open Island, within the viewport.
--cloudlet-tail-offset4pxHow far each tail peeks out from behind the one in front.
--cloudlet-tail-scale.9Scale of the first tail relative to the pill; the second is scaled again.
--cloudlet-control-min-size44pxTouch target of buttons and links.
--cloudlet-viewport-gutter16pxGap to the viewport edge; see Offsets.
--cloudlet-z-index1000Stacking order of the fixed host.

The safe-area insets are tokens too: --cloudlet-safe-top, --cloudlet-safe-right, --cloudlet-safe-bottom and --cloudlet-safe-left default to the browser's env(safe-area-inset-*).

Colours, in the light and the dark theme:

TokenLightDark
--cloudlet-background#ffffff#1c1c1e
--cloudlet-foreground#111113#f5f5f7
--cloudlet-muted#6e6e76#a1a1aa
--cloudlet-focus#245cdb#93b4ff
--cloudlet-shadowa soft two-layer shadowa deeper single shadow
--cloudlet-tail-backgroundthe backgroundthe foreground mixed into the background
--cloudlet-tail-borderthe borderthe foreground at 18%
--cloudlet-borderthe foreground at 10%the same mix
--cloudlet-action-backgroundthe foreground at 8%the same mix
--cloudlet-successoklch(0.723 0.219 142.136)the same
--cloudlet-erroroklch(0.637 0.237 25.331)the same
--cloudlet-infooklch(0.685 0.169 237.323)the same
--cloudlet-warningoklch(0.795 0.184 86.047)the same
--cloudlet-loadingoklch(0.556 0 0)the same
--cloudlet-badge#d70015the same
--cloudlet-badge-foreground#fffthe same

Overriding tokens

Set theme-independent tokens and status colours on .cloudlet-host:

notifications.css
.cloudlet-host {
  --cloudlet-font-family: 'Inter', system-ui, sans-serif;
  --cloudlet-pill-max-width: 24rem;
  --cloudlet-island-radius: 20px;
  --cloudlet-success: oklch(0.7 0.17 160);
}

Dark surfaces use a more specific selector, so .cloudlet-host alone changes light surfaces. To map both themes to your app's colour tokens, use the guarded rule below:

notifications.css
/* Your --surface/--text/--ring are defined on :root and on your .dark root. */
@media (forced-colors: none) {
  .cloudlet-host[data-theme] {
    --cloudlet-background: var(--surface);
    --cloudlet-foreground: var(--text);
    --cloudlet-muted: var(--text-muted);
    --cloudlet-focus: var(--ring);
  }
}

The tokens must reach the portal: define them on :root or on an ancestor of the container, not on a wrapper around your React tree, which does not contain the default portal on document.body. This is the approach of the shadcn registry item's toaster.css, which maps --popover, --popover-foreground, --muted-foreground, --border, --ring, --accent and --font-sans.

Theme

<Toaster theme> is system by default:

  • system follows the user's prefers-color-scheme.
  • light and dark force a theme, whatever the operating system prefers.

Pass your app's resolved theme to keep Cloudlet's surface and native color-scheme in sync with the page. The host's data-theme keeps the prop value; the selector below reflects the active theme after mount:

notifications.css
.cloudlet-host[data-resolved-theme="dark"] .app-toast-action { font-weight: 500; }

The built-in dark surface uses prefers-color-scheme and works before scripts run.

In forced-colours mode (Windows High Contrast), every theme uses system colours: Canvas, CanvasText and Highlight, with visible borders on controls.

Direction

<Toaster dir> is ltr or rtl. When it is omitted, the Toaster follows dir="rtl" on the <html> element. In right-to-left mode, text aligns to the start, the badge and the slider are mirrored, and the arrow keys move between notifications in the reading direction. Use the RTL toggle in the Playground to see it.

Class names

classNames adds your classes to the parts of the renderer. They are added next to Cloudlet's own cloudlet-* classes and never replace them, so the stylesheet keeps working:

notifications.tsx
import { CloudletProvider, createCloudlet, Toaster } from '@cloudlet/toast';
import './notifications.css';

const cloudlet = createCloudlet();

export function Notifications() {
  return (
    <CloudletProvider controller={cloudlet}>
      <Toaster
        classNames={{
          island: 'app-island',
          title: 'app-toast-title',
          action: 'app-toast-action',
        }}
      />
    </CloudletProvider>
  );
}

The parts are host, stack, pill, tail, badge, island, header, icon, title, close, body, description, actions, action, indicator, clear and overflow; classNames says which element each one is.

Cloudlet's own rules use two classes, such as .cloudlet-host .cloudlet-action, so a selector with a single class of yours loses to them on the properties they set. Write rules at least as specific, or style through the tokens:

notifications.css
.cloudlet-host .app-toast-title { letter-spacing: -0.01em; }
.cloudlet-host .app-toast-action { font-weight: 600; }

Utility classes that live in a cascade layer, as Tailwind's do, lose to Cloudlet's unlayered stylesheet for every property it sets. They still apply to properties Cloudlet leaves alone.

To let utilities win, import the stylesheet into a layer of your own, ordered before utilities, instead of importing it from JavaScript:

globals.css
@layer theme, base, cloudlet, components, utilities;
@import "tailwindcss";
@import "@cloudlet/toast/styles.css" layer(cloudlet);

The stylesheet is unlayered on purpose: then a page's own resets, such as button { all: unset }, cannot break the notifications. Inside a layer, every unlayered rule of the page wins over Cloudlet, so check your global styles after the switch.

The host also exposes its state as data attributes you can select on: data-state (stack or island), data-island-reason, data-position, data-edge (top or bottom), data-theme, data-resolved-theme, data-reduced-motion and data-dragging. The current notification carries data-kind and data-current.

Next steps