npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bubble-design-system/ui

v1.2.2

Published

Bubble Design System — soft + teal floating-pill UI on Base UI, with token-driven theming and a single shipped stylesheet (no Tailwind required).

Readme

@bubble-design-system/ui

Bubble — a neutral, composable, token-driven UI foundation built on Base UI, shipped as a single plain CSS file.

Bubble's signature is the soft tone with a teal brand: a soft-gray page (#ECEDEF) on which white pill-shaped surfaces float via layered shadows + an inset white top-highlight, accented by teal (#00CEC8) and a pink→magenta→violet gradient blob mark. The canonical identity is tone=soft · brand=teal · gray=slate · radius=default · density=default · font=roboto · light.

Every visual decision — tone, color, brand, gray family, radius, density, typography, motion — is driven by CSS custom properties. Toggle a single data-* attribute on <html> and the whole app re-skins live, with no rebuild.

<html
  data-theme="light"
  data-tone="soft"
  data-brand="teal"
  data-gray="slate"
  data-radius="default"
  data-density="default"
  data-font="roboto"
>
  • 25 components — Button, Input, Textarea, Checkbox, Radio, Switch, Select, Badge, Avatar, Divider, Modal, Toast, Tooltip, Tabs, Alert, DropdownMenu, Skeleton, Card, StatusPill, Segmented, Popover, DataTable, CommandPalette, Chat (plus Container + Grid layout primitives). Each wraps an @base-ui/react primitive where one exists — accessible by construction, styled with a single shipped stylesheet.
  • A 3-layer, multi-theme token system spanning color (light/dark · 3 gray families · 6 brand palettes including teal), 3 tones (vivid · pastel · soft — soft is the signature look), radius (4 scales), density (3 scales), typography (2 font pairs), layered shadows, and motion.
  • Live theme switching via seven data-* attributes on any ancestor. Every CSS rule reads var(--…) at use-site, so swapping data-tone="vivid" for data-tone="soft" reflows the UI without re-rendering or rebuilding.
  • No build dependency in consumer apps. One CSS import. No PostCSS, no Tailwind, no preprocessor required.
  • Dual ESM + CJS with per-format .d.ts / .d.cts. sideEffects is scoped to the CSS so unused components tree-shake.
  • Composable, not opinionated — components take children, spread ...props, forward refs, expose compound sub-parts (Card.Header, StatusPill.Indicator, Segmented.Item), and emit stable BEM class names that you can target with plain CSS to override defaults.

Design principles

The five rules every decision in Bubble traces back to:

  1. Restraint over decoration. Few colors, light shadows, moderate radius. Every visual element must justify itself.
  2. Composable, not opinionated. No business logic baked into components, so the next person can remix freely (a Card never forces a header).
  3. Accessible by default. Contrast, focus state, keyboard nav pass WCAG AA without extra thought.
  4. Token-driven. Nothing is hardcoded in a component — every value references a token, so a new theme re-skins the whole system instantly.
  5. One way to do things. If there are two ways to do the same thing, pick one.

Common UI Patterns

Before writing custom CSS against var(--color-bg-*), var(--radius-*), or var(--shadow-*) to build a surface, pill, menu, or banner — check this table. There is very likely already a component for it.

| You're building... | Use | Not... | | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | A floating card / panel / content surface | Card (variant="elevated" or "muted") | a <div> styled with --color-bg-secondary + --radius-lg + --shadow-md | | A chat message thread (bubbles, avatars, timestamps, read receipts, reactions, compose bar) | ChatChatThread / ChatMessage / ChatCompose | a custom flex layout styled from raw tokens, with hand-rolled grouping and scroll logic | | A notification / toast / status banner | Toast + useToast() | a custom toast-container + setTimeout store | | A status indicator (online, connected, error dot) | StatusPill + StatusPill.Indicator | a ::after pseudo-element colored with --color-bg-success-strong | | A small tag, count, or label pill | Badge | a custom pill <div> with --radius-full | | A dropdown / context menu / select-from-list | DropdownMenu | an absolutely-positioned custom <div> list | | A command palette / "/" slash-command menu / ⌘K launcher | CommandPalette + useCommandPalette() | a custom filtered dropdown with arrow-key handling | | A popover anchored to a trigger (info panel, form, menu) | Popover | a custom absolutely-positioned <div> + manual outside-click handling | | A hover tooltip | Tooltip | the native title attribute or a custom hover <div> | | A loading placeholder | Skeleton | a custom @keyframes pulse <div> | | A view switcher / segmented toggle | Segmented | a custom button group with manual active-state CSS | | A confirm dialog / modal | Modal | a custom backdrop + centered <div> | | A responsive page section / 12-col grid | Container + Grid | custom max-width + flex/grid CSS | | A tabbed interface | Tabs | a custom button row + conditional render | | A sortable / searchable / paginated table | DataTable | a custom <table> + manual sort/filter/page state | | A horizontal or vertical rule | Divider | border-top: 1px solid var(--color-border-*) | | A user/avatar icon with fallback initials | Avatar | a circular <div> + <img> with manual error handling | | Form fields (text, multiline, checkbox, radio, switch, select) | Input / Textarea / Checkbox / Radio / RadioGroup / Switch / Select | native elements styled from scratch |

If nothing in this table fits, that's a real gap — reach for tokens directly, and consider opening an issue so the pattern can become component #26.


Table of contents


Tech stack

| Concern | Tool | | ----------------- | --------------------------------------------------------------------------------- | | Framework | React 19 (works with ≥ 18.2) | | Primitives | @base-ui/react ≥ 1.0 (the post-rename successor to @base-ui-components/react) | | Styling | Plain CSS — one shipped stylesheet, hand-authored per component | | Class composition | clsx (re-exported as cn()) | | Build tool | tsup (ESM + CJS + dual .d.ts) + a 50-line Node script for CSS bundling | | Language | TypeScript 6 | | Package manager | [email protected] | | Node | ≥ 20 |


Installation

# npm
npm install @bubble-design-system/ui

# pnpm
pnpm add @bubble-design-system/ui

# yarn
yarn add @bubble-design-system/ui

Then install the peer dependencies your app must have:

npm install react react-dom @base-ui/react

| Peer dependency | Required version | | ---------------- | ---------------- | | react | ≥ 18.2 | | react-dom | ≥ 18.2 | | @base-ui/react | ≥ 1.0.0 |


Setup

1. Import the stylesheet

The shipped CSS contains the design tokens, a minimal reset, and every component rule. One import wires everything up — no PostCSS plugin, no Tailwind config, no preprocessor.

/* app/globals.css — or wherever your global styles live */
@import "@bubble-design-system/ui/styles.css";

Or, in a TS/JS entry file (Vite, Next.js App Router, Webpack, Parcel, …):

import "@bubble-design-system/ui/styles.css";

If you only want the raw CSS custom properties (no component rules), import the tokens file directly:

@import "@bubble-design-system/ui/tokens.css";

2. Set the theme attributes on your root element

// app/layout.tsx (Next.js App Router example)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html
      lang="en"
      data-theme="light"
      data-tone="soft"
      data-brand="teal"
      data-gray="slate"
      data-radius="default"
      data-density="default"
      data-font="roboto"
    >
      <body>{children}</body>
    </html>
  );
}

Every attribute has a sensible default if omitted, but spelling them out makes the design surface explicit.

3. Use components

import { Button, Modal, Divider } from "@bubble-design-system/ui";

export function Example() {
  return (
    <div>
      <Button variant="primary">Save</Button>
      <Divider />
      <Modal.Root>
        <Modal.Trigger render={<Button variant="secondary" />}>
          Open
        </Modal.Trigger>
        <Modal.Content>
          <Modal.Title>Confirm</Modal.Title>
          <Modal.Description>Are you sure?</Modal.Description>
        </Modal.Content>
      </Modal.Root>
    </div>
  );
}

Runtime theming

Seven data-* attributes on any ancestor element (typically <html> or <body>) re-skin every descendant at runtime, with no rebuild.

| Attribute | Values | Default | What it controls | | -------------- | ---------------------------------------------------------- | --------- | -------------------------------------------------------------------------------- | | data-theme | light · dark | light | Semantic color mapping (background, text, border, shadow). | | data-tone | vivid · pastel · soft | soft | Surface model, palette saturation, control radius. soft is the signature look. | | data-gray | slate · neutral · stone | slate | The gray family used for surfaces and text. | | data-brand | blue · violet · emerald · orange · mono · teal | teal | The brand palette (--brand-50 through --brand-950). | | data-radius | default · sharp · soft · pill | default | The corner radius scale (--radius-xs through --radius-2xl). | | data-density | default · compact · comfortable | default | Control heights and padding (--control-h-*, --control-px-*). | | data-font | roboto · system | roboto | The font pair (--font-sans / --font-mono). |

Toggle them with any approach you like — setAttribute, React state, a media-query listener, a server cookie:

document.documentElement.setAttribute("data-theme", "dark");
document.documentElement.setAttribute("data-brand", "violet");

A working live-switcher implementation (with localStorage persistence and an SSR-safe bootstrap script) lives in the docs app — see apps/docs/app/ThemeBar.tsx and apps/docs/app/layout.tsx in the repository.


Components

Every component is exported from the package root:

import {
  Alert,
  Avatar,
  Badge,
  Button,
  Card,
  ChatCompose,
  ChatDateDivider,
  ChatMessage,
  ChatThread,
  Checkbox,
  CommandPalette,
  Container,
  DataTable,
  Divider,
  DropdownMenu,
  Grid,
  Input,
  Modal,
  Popover,
  Radio,
  RadioGroup,
  Segmented,
  Select,
  Skeleton,
  StatusPill,
  Switch,
  Tabs,
  Textarea,
  Toast,
  Tooltip,
  useCommandPalette,
  useToast,
  cn,
} from "@bubble-design-system/ui";

Conventions shared by all components:

  • They forwardRef to the underlying Base UI primitive.
  • Native HTML attributes are spread via ...propsonClick, aria-*, id, style all just work.
  • Each component emits stable BEM class names (pds-btn, pds-btn--primary, pds-card__header, …). Your className is appended last in the final class string.
  • Variants and sizes are string-literal enums with documented defaults.
  • Both :disabled and [data-disabled] are styled (Base UI uses the data-attr form).

Alert

A static informational banner with a variant-specific icon, title, and body.

| Prop | Type | Default | Description | | ----------- | ------------------------------------------------ | ---------------- | ------------------------------------------------------ | | variant | "info" \| "success" \| "warning" \| "danger" | "info" | Visual tone and default icon. | | icon | ReactNode \| false | variant-specific | Override the default icon, or pass false to hide it. | | title | ReactNode | — | Optional bold header line. | | children | ReactNode | — | The body copy, rendered in the secondary text color. | | className | string | — | Extra classes (appended after the library's). | | ...props | HTMLAttributes<HTMLDivElement> (minus title) | — | All native div attributes. |

<Alert variant="success" title="Saved">Your changes are live.</Alert>
<Alert variant="danger" title="Couldn't save" icon={false}>
  Try again in a moment.
</Alert>

Avatar

Compound component built on @base-ui/react/avatar — handles image load failure with a fallback.

| Sub-component | Description | | ----------------- | --------------------------------------------------- | | Avatar (root) | Sized circular surface. | | Avatar.Image | The image element. | | Avatar.Fallback | Shown while the image is loading or after it fails. |

Avatar props:

| Prop | Type | Default | Description | | ----------- | ------------------------------ | ------- | --------------------- | | size | "sm" \| "md" \| "lg" \| "xl" | "md" | 24 / 32 / 40 / 48 px. | | className | string | — | Extra classes. | | ...props | Base UI Avatar.Root props | — | Spread to the root. |

Avatar.Image and Avatar.Fallback accept their Base UI props plus className.

<Avatar size="lg">
  <Avatar.Image src="/me.jpg" alt="Ada" />
  <Avatar.Fallback>AL</Avatar.Fallback>
</Avatar>

Badge

Small inline pill for status, counts, or labels.

| Prop | Type | Default | Description | | ----------- | ------------------------------------------------------------ | ----------- | -------------------------- | | variant | "neutral" \| "brand" \| "success" \| "warning" \| "danger" | "neutral" | Background and text color. | | size | "sm" \| "md" \| "lg" | "md" | Pill height and padding. | | className | string | — | Extra classes. | | ...props | HTMLAttributes<HTMLSpanElement> | — | Native span attributes. |

<Badge variant="success">Active</Badge>
<Badge variant="brand" size="sm">New</Badge>

Button

Wraps @base-ui/react/button.

| Prop | Type | Default | Description | | ----------- | ------------------------------------------------------ | ----------- | --------------------------------------------------------------------------- | | variant | "primary" \| "secondary" \| "destructive" \| "ghost" | "primary" | Visual style. | | size | "sm" \| "md" \| "lg" | "md" | Maps to --control-h-* / --control-px-* so density attribute affects it. | | className | string | — | Extra classes. | | ...props | Base UI Button props | — | Includes disabled, type, onClick, etc. |

<Button variant="primary" size="lg" onClick={save}>Save</Button>
<Button variant="destructive">Delete</Button>
<Button variant="ghost" disabled>Cancel</Button>

Card

Compound component for floating-pill surface content.

| Sub-component | Description | | ------------------ | ----------------------------------------------------- | | Card (root) | The floating surface. Variant controls fill + shadow. | | Card.Header | Row with title + optional action. | | Card.Title | <h3> heading. | | Card.Description | Supporting paragraph. | | Card.Action | Right-aligned controls inside the header. | | Card.Body | Main content area. | | Card.Footer | Bordered footer row with right-aligned controls. |

Card props:

| Prop | Type | Default | Description | | ----------- | ----------------------- | ------------ | ---------------------------------------------------------------------------- | | variant | "elevated" \| "muted" | "elevated" | elevated = white surface with shadow. muted = bg-secondary, no shadow. | | className | string | — | Extra classes. |

<Card>
  <Card.Header>
    <div>
      <Card.Title>Soft-pill surface</Card.Title>
      <Card.Description>White card floating on a gray page.</Card.Description>
    </div>
    <Card.Action>
      <Button size="sm" variant="ghost">
        Manage
      </Button>
    </Card.Action>
  </Card.Header>
  <Card.Body>…</Card.Body>
  <Card.Footer>
    <Button size="sm" variant="ghost">
      Cancel
    </Button>
    <Button size="sm">Save</Button>
  </Card.Footer>
</Card>

Chat

Four standalone components for assembling a chat thread — no Base UI primitive needed, just plain markup. ChatThread is the scroll container, ChatDateDivider renders a centered separator label, ChatMessage is a single message bubble (with grouping, avatar, meta, reactions, and delivery status), and ChatCompose is an auto-growing input bar.

| Component | Description | | ----------------- | ---------------------------------------------------------------- | | ChatThread | Scrollable message-list container. | | ChatDateDivider | Centered separator label (e.g. "Today"). | | ChatMessage | A single message bubble — grouping/avatar/meta/reactions/status. | | ChatCompose | Auto-growing textarea + send button, controlled or uncontrolled. |

ChatMessage props:

| Prop | Type | Default | Description | | ----------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------- | | side | "sent" \| "received" | "received" | Which side of the thread the bubble renders on. | | position | "solo" \| "first" \| "middle" \| "last" | "solo" | Position within a consecutive group — controls bubble-corner rounding and which messages show meta/avatar/status. | | avatar | ReactNode | — | Avatar slot, shown for received messages at solo/last. | | name | string | — | Sender name, shown at solo/first for received messages. | | time | string | — | Timestamp, shown at solo/first. | | status | "sending" \| "sent" \| "delivered" \| "read" | — | Delivery-status row, shown for sent messages at solo/last. | | reactions | { emoji: string; count?: number; mine?: boolean }[] | — | Reaction pills rendered under the bubble. | | className | string | — | Extra classes. |

ChatCompose props:

| Prop | Type | Default | Description | | ------------- | ----------------------------------------------- | -------------------- | ---------------------------------------------------------------- | | value | string | — | Controlled value. Omit for uncontrolled (internal state). | | onChange | (e: ChangeEvent<HTMLTextAreaElement>) => void | — | Change handler (controlled mode). | | onSend | (value: string) => void | — | Called with the trimmed text on send (Enter or the send button). | | placeholder | string | "Write a message…" | Textarea placeholder. | | avatar | ReactNode | — | Avatar slot rendered before the input. | | disabled | boolean | false | Disables the textarea and send button. | | className | string | — | Extra classes. |

<ChatThread>
  <ChatDateDivider>Today</ChatDateDivider>

  <ChatMessage
    side="received"
    name="Lena Torres"
    time="9:41 AM"
    avatar={
      <Avatar>
        <Avatar.Fallback>LT</Avatar.Fallback>
      </Avatar>
    }
    reactions={[{ emoji: "👍", count: 2, mine: true }]}
  >
    Hey — got a minute to look at the new Card variants?
  </ChatMessage>

  <ChatMessage side="sent" status="read">
    Sure, pulling them up now.
  </ChatMessage>
</ChatThread>

<ChatCompose
  avatar={
    <Avatar>
      <Avatar.Fallback>AK</Avatar.Fallback>
    </Avatar>
  }
  onSend={(text) => sendMessage(text)}
/>

Checkbox

Wraps @base-ui/react/checkbox. Supports checked, unchecked, and indeterminate states with built-in SVG indicators.

| Prop | Type | Default | Description | | ----------- | ----------------------------- | ------- | --------------------------------------------------------------------- | | size | "sm" \| "md" \| "lg" | "md" | 16 / 18 / 20 px. | | className | string | — | Extra classes on the root button. | | ...props | Base UI Checkbox.Root props | — | checked, defaultChecked, indeterminate, onCheckedChange, etc. |

<Checkbox defaultChecked />
<Checkbox indeterminate />
<Checkbox disabled />

CommandPalette

Built on @base-ui/react/dialog. A fuzzy-searchable, grouped command list with arrow-key navigation and Enter-to-select — the "/" or ⌘K menu pattern. Pair it with useCommandPalette(), a small hook that owns open state and registers a global ⌘K / Ctrl+K listener.

CommandPalette props:

| Prop | Type | Default | Description | | -------------- | ------------------------------------ | ------------------ | -------------------------------------------------------------------------------- | | open | boolean | — | Whether the palette is open. | | onOpenChange | (open: boolean) => void | — | Called when the palette should open or close. | | groups | CommandPaletteGroup[] | [] | Grouped, filterable command items. | | placeholder | string | "Type to search" | Search input placeholder. | | onSelect | (item: CommandPaletteItem) => void | — | Called when an item is chosen (Enter or click), after the item's own onSelect. | | className | string | — | Extra classes on the popup. |

CommandPaletteGroup{ label?: string; items: CommandPaletteItem[] }

CommandPaletteItem{ id: string; label: string; description?: string; icon?: ReactNode; shortcut?: string; keywords?: string[]; onSelect?: () => void }

const palette = useCommandPalette(); // owns `open` + ⌘K/Ctrl+K toggle

<CommandPalette
  open={palette.open}
  onOpenChange={palette.setOpen}
  groups={[
    {
      label: "Navigation",
      items: [
        { id: "home", label: "Go home", shortcut: "G H" },
        { id: "settings", label: "Settings", keywords: ["preferences"] },
      ],
    },
    {
      label: "Actions",
      items: [{ id: "new", label: "New item", onSelect: () => createItem() }],
    },
  ]}
  onSelect={(item) => console.log("selected", item.id)}
/>;

Container + Grid

Layout primitives. Container centers content and applies page margins. Grid is a 12-column grid; Grid.Col spans columns with optional responsive overrides.

<Container size="lg">
  <Grid>
    <Grid.Col span={12}>full row</Grid.Col>
    <Grid.Col span={6} lgSpan={4}>
      half on mobile, third on lg
    </Grid.Col>
    <Grid.Col span={6} lgSpan={4}>
      …
    </Grid.Col>
    <Grid.Col span={12} lgSpan={4}>
      …
    </Grid.Col>
  </Grid>
</Container>

Container props: size = "sm" | "md" | "lg" | "xl" | "prose" | "fluid" (default "xl").

Grid props: gutter = "default" | "tight" | "flush" (default "default").

Grid.Col props: span, smSpan, mdSpan, lgSpan = 1 | 2 | … | 12 | "full". Default span is 12.


DataTable

Generic, client-side DataTable<T extends { id: string | number }> — search across all columns, sortable columns, row selection (header select-all + per-row, reusing Checkbox), and pagination.

Props:

| Prop | Type | Default | Description | | ------------ | ---------------------- | ------- | ------------------------------------------------------- | | columns | DataTableColumn<T>[] | — | Column definitions, rendered in order. | | data | T[] | — | Rows. Each row must have an id: string \| number. | | selectable | boolean | true | Shows the select-all / per-row checkboxes. | | searchable | boolean | true | Shows the search input; filters across all column keys. | | pageSize | number | 10 | Rows per page. | | actions | ReactNode | — | Right-aligned toolbar content (e.g. a "New" button). | | className | string | — | Extra classes. |

DataTableColumn<T>:

| Field | Type | Description | | ---------------- | --------------------------------------- | ---------------------------------------------- | | key | string | Property name read off each row. | | label | string | Column header text. | | sortable | boolean | Default true — click the header to sort. | | render | (value: unknown, row: T) => ReactNode | Custom cell renderer. | | width | string | CSS width for the <th> / <td>. | | muted / mono | boolean | Styles the cell text as secondary / monospace. |

<DataTable
  columns={[
    { key: "name", label: "Name" },
    { key: "email", label: "Email", muted: true },
    {
      key: "status",
      label: "Status",
      render: (value) => (
        <StatusPill intent={value === "active" ? "success" : "neutral"}>
          {String(value)}
        </StatusPill>
      ),
    },
  ]}
  data={users}
  actions={<Button size="sm">Invite</Button>}
/>

Divider

Horizontal or vertical separator with a semantic role from @base-ui/react/separator.

| Prop | Type | Default | Description | | ------------- | ---------------------------- | -------------- | --------------------------------- | | orientation | "horizontal" \| "vertical" | "horizontal" | Layout direction. | | className | string | — | Extra classes. | | ...props | Base UI Separator props | — | Spread to the underlying element. |

<Divider />
<div style={{ display: "flex", height: "2rem", alignItems: "center" }}>
  <span>A</span><Divider orientation="vertical" /><span>B</span>
</div>

DropdownMenu

Compound component built on @base-ui/react/menu. Supports items, checkbox items, radio groups, labels, and separators.

| Sub-component | Description | | --------------------------- | ---------------------------------------- | | DropdownMenu.Root | The state container. | | DropdownMenu.Trigger | The element that opens the menu. | | DropdownMenu.Content | The portalled popup. | | DropdownMenu.Item | A selectable row. | | DropdownMenu.CheckboxItem | A toggleable row with a check indicator. | | DropdownMenu.RadioGroup | Wrapper for radio items. | | DropdownMenu.RadioItem | A single radio choice. | | DropdownMenu.Group | Logical group of items. | | DropdownMenu.Label | Uppercase group label. | | DropdownMenu.Separator | Thin horizontal divider. |

DropdownMenu.Content props:

| Prop | Type | Default | Description | | ------------ | ------------------------------ | --------- | ---------------------------------- | | sideOffset | number | 6 | Distance in px from the trigger. | | align | "start" \| "center" \| "end" | "start" | Alignment relative to the trigger. | | className | string | — | Extra classes on the popup. | | ...props | Base UI Menu.Popup props | — | Spread to the popup. |

<DropdownMenu.Root>
  <DropdownMenu.Trigger render={<Button variant="secondary">Options</Button>} />
  <DropdownMenu.Content>
    <DropdownMenu.Label>Actions</DropdownMenu.Label>
    <DropdownMenu.Item>Edit</DropdownMenu.Item>
    <DropdownMenu.Item>Duplicate</DropdownMenu.Item>
    <DropdownMenu.Separator />
    <DropdownMenu.CheckboxItem checked>Pinned</DropdownMenu.CheckboxItem>
  </DropdownMenu.Content>
</DropdownMenu.Root>

Input

Wraps @base-ui/react/input. Includes built-in invalid styling via aria-invalid.

| Prop | Type | Default | Description | | ----------- | ------------------------------------ | ------- | ----------------------------------------------------------------------------------------- | | size | "sm" \| "md" \| "lg" | "md" | Density-aware control height. | | invalid | boolean | — | Sets aria-invalid and applies the danger border + focus ring. | | className | string | — | Extra classes. | | ...props | Base UI Input props (minus size) | — | All native input attributes: value, placeholder, type, onChange, disabled, etc. |

<Input placeholder="Email" />
<Input size="lg" invalid value={email} onChange={(e) => setEmail(e.target.value)} />

Modal

Compound component built on @base-ui/react/dialog. Renders into a portal, with backdrop blur and scale-in animation.

| Sub-component | Description | | ------------------- | ---------------------------------- | | Modal.Root | Open-state container. | | Modal.Trigger | Element that opens the modal. | | Modal.Close | Element that closes the modal. | | Modal.Content | The portalled popup with backdrop. | | Modal.Title | Heading text. | | Modal.Description | Sub-text under the title. |

Modal.Content props:

| Prop | Type | Default | Description | | ------------------- | ---------------------------- | ------- | ------------------------------ | | className | string | — | Extra classes on the popup. | | backdropClassName | string | — | Extra classes on the backdrop. | | ...props | Base UI Dialog.Popup props | — | Spread to the popup. |

Modal.Title and Modal.Description accept their Base UI props plus className.

<Modal.Root>
  <Modal.Trigger render={<Button>Open</Button>} />
  <Modal.Content>
    <Modal.Title>Delete project?</Modal.Title>
    <Modal.Description>This action cannot be undone.</Modal.Description>
    <div
      style={{
        marginTop: "1rem",
        display: "flex",
        justifyContent: "flex-end",
        gap: "0.5rem",
      }}
    >
      <Modal.Close render={<Button variant="secondary">Cancel</Button>} />
      <Button variant="destructive">Delete</Button>
    </div>
  </Modal.Content>
</Modal.Root>

Popover

Compound component built on @base-ui/react/popover. Like Tooltip, but for richer content (forms, lists, multi-element panels) that stays open until dismissed.

| Sub-component | Description | | ---------------------------------------------------- | ------------------------------------------------------------------------------------ | | Popover.Root | State container. | | Popover.Trigger | The element that opens the popover. | | Popover.Content | The portalled popup — pre-wires Portal → Positioner → Popup, plus an optional arrow. | | Popover.Title / Popover.Description | Accessible heading / description, wired to ARIA via Base UI. | | Popover.Close | Closes the popover when clicked. | | Popover.Header / Popover.Body / Popover.Footer | Layout slots for Content's children. |

Popover.Content props:

| Prop | Type | Default | Description | | ------------ | ---------------------------------------- | ---------- | ---------------------------------------- | | side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Preferred side. | | align | "start" \| "center" \| "end" | "center" | Alignment along the side. | | sideOffset | number | 10 | Distance from the trigger. | | showArrow | boolean | true | Renders a caret pointing at the trigger. | | className | string | — | Extra classes on the popup. |

<Popover.Root>
  <Popover.Trigger render={<Button variant="secondary">Filters</Button>} />
  <Popover.Content>
    <Popover.Header>
      <Popover.Title>Column visibility</Popover.Title>
    </Popover.Header>
    <Popover.Body>…</Popover.Body>
    <Popover.Footer>
      <Popover.Close
        render={
          <Button size="sm" variant="ghost">
            Reset
          </Button>
        }
      />
      <Button size="sm">Apply</Button>
    </Popover.Footer>
  </Popover.Content>
</Popover.Root>

Radio / RadioGroup

Wraps @base-ui/react/radio and @base-ui/react/radio-group.

Radio props:

| Prop | Type | Default | Description | | ----------- | -------------------------- | ------- | ------------------------- | | size | "sm" \| "md" \| "lg" | "md" | 16 / 18 / 20 px. | | className | string | — | Extra classes. | | ...props | Base UI Radio.Root props | — | value, disabled, etc. |

RadioGroup props:

| Prop | Type | Default | Description | | ----------- | -------------------------- | ------- | ----------------------------------------- | | className | string | — | Override the default vertical stack. | | ...props | Base UI RadioGroup props | — | value, defaultValue, onValueChange. |

<RadioGroup defaultValue="email">
  <label>
    <Radio value="email" /> Email
  </label>
  <label>
    <Radio value="sms" /> SMS
  </label>
</RadioGroup>

Segmented

Compound component built on @base-ui/react/toggle-group (single-select). The selected item rises as a white floating pill.

| Sub-component | Description | | ------------------ | ----------------- | | Segmented (root) | The toggle group. | | Segmented.Item | A single segment. |

Segmented props:

| Prop | Type | Default | Description | | --------------- | ------------------------- | ------- | --------------------------- | | value | string | — | Controlled selected value. | | defaultValue | string | — | Uncontrolled initial value. | | onValueChange | (value: string) => void | — | Fired with the new value. | | size | "sm" \| "md" \| "lg" | "md" | 24 / 28 / 32 px. |

<Segmented value={range} onValueChange={setRange}>
  <Segmented.Item value="day">Day</Segmented.Item>
  <Segmented.Item value="week">Week</Segmented.Item>
  <Segmented.Item value="month">Month</Segmented.Item>
</Segmented>

Select

Compound component built on @base-ui/react/select.

| Sub-component | Description | | ---------------- | ------------------------------------------------------ | | Select.Root | State container (value, onValueChange). | | Select.Trigger | The clickable trigger; renders a chevron icon. | | Select.Value | The selected value's display. | | Select.Content | The portalled popup. | | Select.Item | A selectable row with a check indicator when selected. |

Select.Trigger props: size = "sm" | "md" | "lg" (default "md"), plus className.

Select.Value props: placeholder?: ReactNode, plus className.

Select.Content props: sideOffset?: number (default 6), plus className.

<Select.Root value={fruit} onValueChange={setFruit}>
  <Select.Trigger size="md">
    <Select.Value placeholder="Pick one" />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value="apple">Apple</Select.Item>
    <Select.Item value="banana">Banana</Select.Item>
    <Select.Item value="cherry">Cherry</Select.Item>
  </Select.Content>
</Select.Root>

Skeleton

Loading placeholder with a pulse animation.

| Prop | Type | Default | Description | | ----------- | -------------------------------- | -------- | ---------------------------------------------- | | shape | "line" \| "circle" \| "block" | "line" | Default dimensions and radius. | | className | string | — | Override width/height/radius via your own CSS. | | ...props | HTMLAttributes<HTMLDivElement> | — | Native div attributes. |

<Skeleton />
<Skeleton shape="circle" style={{ width: "2.5rem", height: "2.5rem" }} />
<Skeleton shape="block" style={{ height: "6rem" }} />

StatusPill

Compound floating-pill component for status indicators. Intent drives chip + label color via CSS custom properties.

| Sub-component | Description | | ---------------------- | ----------------------------------------------------------- | | StatusPill (root) | The pill surface. | | StatusPill.Indicator | The leading colored chip. Children render an optional icon. | | StatusPill.Label | The colored text label. |

StatusPill props: intent = "neutral" | "success" | "warning" | "danger" | "info" (default "neutral").

<StatusPill intent="success">
  <StatusPill.Indicator />
  <StatusPill.Label>On track</StatusPill.Label>
</StatusPill>

Switch

Wraps @base-ui/react/switch — a thumb that slides on data-[checked].

| Prop | Type | Default | Description | | ----------- | --------------------------- | ------- | ----------------------------------------------------------- | | size | "sm" \| "md" \| "lg" | "md" | 16 / 20 / 24 px tall. | | className | string | — | Extra classes on the root. | | ...props | Base UI Switch.Root props | — | checked, defaultChecked, onCheckedChange, disabled. |

<Switch defaultChecked />
<Switch size="sm" />

Tabs

Compound component built on @base-ui/react/tabs. The list renders an animated indicator that slides between active tabs.

| Sub-component | Description | | ------------- | --------------------------------------------------------------- | | Tabs (root) | State container; pass value, defaultValue, onValueChange. | | Tabs.List | Horizontal tab strip with a sliding indicator. | | Tabs.Tab | A single tab button. | | Tabs.Panel | The panel paired with a tab value. |

All sub-components accept their Base UI props plus className.

<Tabs defaultValue="profile">
  <Tabs.List>
    <Tabs.Tab value="profile">Profile</Tabs.Tab>
    <Tabs.Tab value="account">Account</Tabs.Tab>
    <Tabs.Tab value="billing">Billing</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="profile">Profile content</Tabs.Panel>
  <Tabs.Panel value="account">Account content</Tabs.Panel>
  <Tabs.Panel value="billing">Billing content</Tabs.Panel>
</Tabs>

Textarea

A thin styled <textarea> mirroring Input's API.

| Prop | Type | Default | Description | | ----------- | --------------------------------------- | ------- | --------------------------------------------------------------- | | size | "sm" \| "md" \| "lg" | "md" | Density-aware control padding. | | invalid | boolean | — | Sets aria-invalid and applies the danger border + focus ring. | | className | string | — | Extra classes. | | ...props | TextareaHTMLAttributes (minus size) | — | All native textarea attributes. |

<Textarea placeholder="Notes" rows={4} />
<Textarea invalid value={text} onChange={(e) => setText(e.target.value)} />

Toast

Built on @base-ui/react/toast. Provides a <Toast.Provider> boundary, a <Toast.Viewport> for positioning, a pre-built <Toast.Toaster> that renders the queue, and the useToast() hook to push toasts.

| Sub-component | Description | | ---------------- | ------------------------------------------------------------------------ | | Toast.Provider | Wrap your app to enable toasts. | | Toast.Viewport | Positioned region where toasts mount (bottom-right by default). | | Toast.Toaster | Pre-styled queue renderer — drop this inside Provider. | | useToast() | Hook returning Base UI's toast manager (.add({ title, description })). |

import { Toast, useToast, Button } from "@bubble-design-system/ui";

function Root({ children }) {
  return (
    <Toast.Provider>
      {children}
      <Toast.Toaster />
    </Toast.Provider>
  );
}

function SaveButton() {
  const toast = useToast();
  return (
    <Button
      onClick={() =>
        toast.add({ title: "Saved", description: "Changes are live." })
      }
    >
      Save
    </Button>
  );
}

Tooltip

Compound component built on @base-ui/react/tooltip. Requires a Tooltip.Provider ancestor (typically once at the app root).

| Sub-component | Description | | ------------------ | ------------------------------- | | Tooltip.Provider | App-level provider. | | Tooltip.Root | Single tooltip state container. | | Tooltip.Trigger | The hovered/focused element. | | Tooltip.Content | The portalled popup. |

Tooltip.Content props:

| Prop | Type | Default | Description | | ------------ | ---------------------------------------- | ---------- | --------------------------- | | side | "top" \| "bottom" \| "left" \| "right" | "top" | Preferred side. | | align | "start" \| "center" \| "end" | "center" | Alignment along the side. | | sideOffset | number | 6 | Distance from the trigger. | | className | string | — | Extra classes on the popup. |

<Tooltip.Provider>
  <Tooltip.Root>
    <Tooltip.Trigger render={<Button variant="ghost">?</Button>} />
    <Tooltip.Content side="top">Helpful hint</Tooltip.Content>
  </Tooltip.Root>
</Tooltip.Provider>

cn() utility

Re-exported clsx wrapper for composing class names — useful when building your own components against the design tokens.

import { cn } from "@bubble-design-system/ui";

cn("my-card", isActive && "my-card--active", className);
// → "my-card my-card--active <consumer className>"

Design tokens

About to write var(--color-bg-*) / var(--radius-*) / var(--shadow-*) on a hand-authored <div>? Tokens are how you theme Bubble's components and the escape hatch for genuine gaps — they are not a second, parallel "build it yourself" API. Check Common UI Patterns first; the surface/pill/menu/banner you're about to build very likely already exists as a themed, accessible component.

All tokens are CSS custom properties defined in src/tokens.css. Reference them directly in your CSS with var(--…). The semantic tokens (those prefixed --color-bg-*, --color-text-*, --color-border-*) re-resolve automatically when an ancestor data-* attribute changes.

Color tokens

Semantic colors map to primitive palettes and are remapped by [data-theme], [data-gray], and [data-brand].

| Token | Purpose | | --------------------------- | ------------------------------------------------ | | --color-bg-primary | Page background. | | --color-bg-secondary | Secondary surface (cards on page). | | --color-bg-tertiary | Tertiary surface (inset wells). | | --color-bg-inverse | Inverted surface (tooltip, toast). | | --color-bg-brand | Brand primary fill. | | --color-bg-brand-hover | Brand hover state. | | --color-bg-brand-active | Brand pressed state. | | --color-bg-brand-subtle | Tinted brand surface (info backgrounds, badges). | | --color-bg-success | Soft success surface. | | --color-bg-success-strong | Solid success fill. | | --color-bg-warning | Soft warning surface. | | --color-bg-warning-strong | Solid warning fill. | | --color-bg-danger | Soft danger surface. | | --color-bg-danger-strong | Solid danger fill (destructive buttons). | | --color-bg-danger-hover | Danger hover state. | | --color-bg-info | Info alert surface. | | --color-bg-hover | Neutral hover. | | --color-bg-pressed | Neutral pressed. | | --color-bg-disabled | Disabled surface. | | --color-text-primary | Body text. | | --color-text-secondary | Supporting text. | | --color-text-tertiary | Placeholder, hints. | | --color-text-disabled | Disabled text. | | --color-text-inverse | Text on bg-inverse. | | --color-text-brand | Brand-colored text (links). | | --color-text-success | Success copy. | | --color-text-warning | Warning copy. | | --color-text-danger | Error copy. | | --color-text-on-brand | Text on bg-brand. | | --color-text-on-danger | Text on bg-danger-strong. | | --color-text-on-success | Text on bg-success-strong. | | --color-border-primary | Default form/control border. | | --color-border-secondary | Section dividers, cards. | | --color-border-tertiary | Subtle inner dividers. | | --color-border-brand | Selected/active accent. | | --color-border-success | Success accent. | | --color-border-warning | Warning accent. | | --color-border-danger | Error accent (invalid inputs). | | --color-border-focus | Focus ring color. |

Primitive palettes

These are the raw color swatches that the semantic tokens reference. You usually shouldn't touch them directly, but they're exposed if you need to.

| Family | Stops | Notes | | -------------------- | ------ | ------------------------------ | | --slate-* | 50–950 | Default gray family. | | --neutral-* | 50–950 | True neutral (no temperature). | | --stone-* | 50–950 | Warm gray. | | --blue-* | 50–950 | Brand option. | | --violet-* | 50–950 | Brand option. | | --emerald-* | 50–950 | Brand option. | | --orange-* | 50–950 | Brand option. | | --green-* | 50–950 | Success palette. | | --amber-* | 50–950 | Warning palette. | | --red-* | 50–950 | Danger palette. | | --white, --black | — | Pure values. |

Aliases follow the active data-* attribute: --gray-* resolves to whichever gray family is selected, --brand-* to whichever brand. The mono and teal brands have special-case treatment for contrast on light/dark themes.

Radius

Selected by [data-radius]. Each scale rewrites the same custom properties.

| Token | default | sharp | soft | pill | | --------------- | ------- | ------ | ------ | ------ | | --radius-xs | 2px | 0px | 4px | 4px | | --radius-sm | 4px | 1px | 8px | 9999px | | --radius-md | 6px | 2px | 12px | 9999px | | --radius-lg | 8px | 3px | 14px | 9999px | | --radius-xl | 12px | 4px | 18px | 18px | | --radius-2xl | 16px | 6px | 24px | 22px | | --radius-full | 9999px | 9999px | 9999px | 9999px |

Plus --ctrl-radius — the control radius used by Button/Input/Select. Pills under [data-tone="soft"], --radius-md elsewhere.

Shadow

Light theme uses cool slate tints; dark theme uses opaque black. The soft tone adds an inset white top-highlight on md/lg/xl. The focus ring tracks the brand color.

| Token | Purpose | | ---------------- | -------------------------------- | | --shadow-xs | Hairline lift. | | --shadow-sm | Subtle card. | | --shadow-md | Standard card. | | --shadow-lg | Popover, dropdown. | | --shadow-xl | Modal. | | --shadow-focus | Focus ring (3–4px brand-tinted). |

Typography

| Token | Value | | -------------------------------------------------------- | -------------------------------- | | --font-size-xs | 0.75rem | | --font-size-sm | 0.875rem | | --font-size-md | 1rem | | --font-size-lg | 1.125rem | | --font-size-xl | 1.25rem | | --font-size-2xl | 1.5rem | | --font-size-3xl | 1.875rem | | --font-size-4xl | 2.25rem | | --font-size-5xl | 3rem | | --font-size-6xl | 3.75rem | | --line-height-tight / snug / normal / relaxed | 1.15 / 1.3 / 1.5 / 1.65 | | --letter-tight / snug / normal / wide | -0.022em / -0.012em / 0 / 0.04em | | --font-weight-regular / medium / semibold / bold | 400 / 500 / 600 / 700 | | --font-sans / --font-mono | Set by [data-font] |

Spacing

--space-0 through --space-24 follow a 0.25rem (4px) scale, doubling to 0.5rem after 4. Reference them with var(--space-4), etc. — they have no utility-class shorthand because the library doesn't ship one.

Density (control sizing)

Selected by [data-density].

| Token | default | compact | comfortable | | ----------------- | ------- | ------- | ----------- | | --control-h-sm | 28px | 24px | 32px | | --control-h-md | 36px | 30px | 42px | | --control-h-lg | 44px | 38px | 52px | | --control-px-sm | 10px | 8px | 12px | | --control-px-md | 14px | 12px | 18px | | --control-px-lg | 18px | 16px | 22px | | --card-p | 24px | 16px | 32px | | --row-gap | 16px | 12px | 20px |

Motion

| Token | Value | | -------------------- | ----------------------------------- | | --duration-instant | 50ms | | --duration-fast | 120ms | | --duration-normal | 200ms | | --duration-slow | 320ms | | --duration-slower | 500ms | | --ease-linear | linear | | --ease-out | cubic-bezier(0.16, 1, 0.3, 1) | | --ease-in-out | cubic-bezier(0.65, 0, 0.35, 1) | | --ease-spring | cubic-bezier(0.34, 1.56, 0.64, 1) |

Browsing tokens visually

Clone the repository and run the docs app — /tokens renders live swatches that react to the data-* attribute switches:

pnpm install
pnpm -C packages/ui build   # build the lib first
pnpm -C apps/docs dev
# open http://localhost:3000/tokens

Overriding styles

Every component emits stable, low-specificity BEM class names. Override them from your own CSS with a single-class selector:

/* Bump up button padding globally */
.pds-btn--md {
  padding-