@bubble-design-system/ui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1039 -0
- package/dist/assets/logo-blob.svg +12 -0
- package/dist/assets/logo-wordmark.svg +13 -0
- package/dist/index.cjs +977 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +355 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.js +951 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2122 -0
- package/dist/tokens.css +750 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
# @bubble-design-system/ui
|
|
2
|
+
|
|
3
|
+
> Bubble — a **neutral, composable, token-driven UI foundation** built on [Base UI](https://base-ui.com/), shipped as a single plain CSS file.
|
|
4
|
+
|
|
5
|
+
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=geist · light`.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<html
|
|
11
|
+
data-theme="light"
|
|
12
|
+
data-tone="soft"
|
|
13
|
+
data-brand="teal"
|
|
14
|
+
data-gray="slate"
|
|
15
|
+
data-radius="default"
|
|
16
|
+
data-density="default"
|
|
17
|
+
data-font="geist"
|
|
18
|
+
>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **21 components** — Button, Input, Textarea, Checkbox, Radio, Switch, Select, Badge, Avatar, Divider, Modal, Toast, Tooltip, Tabs, Alert, DropdownMenu, Skeleton, Card, StatusPill, Segmented (plus Container + Grid layout primitives). Each wraps an [`@base-ui/react`](https://base-ui.com/) primitive where one exists — accessible by construction, styled with a single shipped stylesheet.
|
|
22
|
+
- **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 (3 font pairs), layered shadows, and motion.
|
|
23
|
+
- **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.
|
|
24
|
+
- **No build dependency in consumer apps.** One CSS import. No PostCSS, no Tailwind, no preprocessor required.
|
|
25
|
+
- **Dual ESM + CJS** with per-format `.d.ts` / `.d.cts`. `sideEffects` is scoped to the CSS so unused components tree-shake.
|
|
26
|
+
- **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.
|
|
27
|
+
|
|
28
|
+
## Design principles
|
|
29
|
+
|
|
30
|
+
The five rules every decision in Bubble traces back to:
|
|
31
|
+
|
|
32
|
+
1. **Restraint over decoration.** Few colors, light shadows, moderate radius. Every visual element must justify itself.
|
|
33
|
+
2. **Composable, not opinionated.** No business logic baked into components, so the next person can remix freely (a `Card` never forces a header).
|
|
34
|
+
3. **Accessible by default.** Contrast, focus state, keyboard nav pass WCAG AA without extra thought.
|
|
35
|
+
4. **Token-driven.** Nothing is hardcoded in a component — every value references a token, so a new theme re-skins the whole system instantly.
|
|
36
|
+
5. **One way to do things.** If there are two ways to do the same thing, pick one.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Table of contents
|
|
41
|
+
|
|
42
|
+
- [Tech stack](#tech-stack)
|
|
43
|
+
- [Installation](#installation)
|
|
44
|
+
- [Setup](#setup)
|
|
45
|
+
- [Runtime theming](#runtime-theming)
|
|
46
|
+
- [Components](#components)
|
|
47
|
+
- [Design tokens](#design-tokens)
|
|
48
|
+
- [Overriding styles](#overriding-styles)
|
|
49
|
+
- [Local development](#local-development)
|
|
50
|
+
- [Contributing](#contributing)
|
|
51
|
+
- [License](#license)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Tech stack
|
|
56
|
+
|
|
57
|
+
| Concern | Tool |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Framework | React 19 (works with ≥ 18.2) |
|
|
60
|
+
| Primitives | `@base-ui/react` ≥ 1.0 (the post-rename successor to `@base-ui-components/react`) |
|
|
61
|
+
| Styling | Plain CSS — one shipped stylesheet, hand-authored per component |
|
|
62
|
+
| Class composition | `clsx` (re-exported as `cn()`) |
|
|
63
|
+
| Build tool | `tsup` (ESM + CJS + dual `.d.ts`) + a 50-line Node script for CSS bundling |
|
|
64
|
+
| Language | TypeScript 6 |
|
|
65
|
+
| Package manager | `pnpm@10.33.0` |
|
|
66
|
+
| Node | ≥ 20 |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# npm
|
|
74
|
+
npm install @bubble-design-system/ui
|
|
75
|
+
|
|
76
|
+
# pnpm
|
|
77
|
+
pnpm add @bubble-design-system/ui
|
|
78
|
+
|
|
79
|
+
# yarn
|
|
80
|
+
yarn add @bubble-design-system/ui
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then install the peer dependencies your app must have:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install react react-dom @base-ui/react
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Peer dependency | Required version |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `react` | ≥ 18.2 |
|
|
92
|
+
| `react-dom` | ≥ 18.2 |
|
|
93
|
+
| `@base-ui/react` | ≥ 1.0.0 |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Setup
|
|
98
|
+
|
|
99
|
+
### 1. Import the stylesheet
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
/* app/globals.css — or wherever your global styles live */
|
|
105
|
+
@import "@bubble-design-system/ui/styles.css";
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or, in a TS/JS entry file (Vite, Next.js App Router, Webpack, Parcel, …):
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import "@bubble-design-system/ui/styles.css";
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
If you only want the raw CSS custom properties (no component rules), import the tokens file directly:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
@import "@bubble-design-system/ui/tokens.css";
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2. Set the theme attributes on your root element
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// app/layout.tsx (Next.js App Router example)
|
|
124
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
125
|
+
return (
|
|
126
|
+
<html
|
|
127
|
+
lang="en"
|
|
128
|
+
data-theme="light"
|
|
129
|
+
data-tone="soft"
|
|
130
|
+
data-brand="teal"
|
|
131
|
+
data-gray="slate"
|
|
132
|
+
data-radius="default"
|
|
133
|
+
data-density="default"
|
|
134
|
+
data-font="geist"
|
|
135
|
+
>
|
|
136
|
+
<body>{children}</body>
|
|
137
|
+
</html>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Every attribute has a sensible default if omitted, but spelling them out makes the design surface explicit.
|
|
143
|
+
|
|
144
|
+
### 3. Use components
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Button, Modal, Divider } from "@bubble-design-system/ui";
|
|
148
|
+
|
|
149
|
+
export function Example() {
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<Button variant="primary">Save</Button>
|
|
153
|
+
<Divider />
|
|
154
|
+
<Modal.Root>
|
|
155
|
+
<Modal.Trigger render={<Button variant="secondary" />}>Open</Modal.Trigger>
|
|
156
|
+
<Modal.Content>
|
|
157
|
+
<Modal.Title>Confirm</Modal.Title>
|
|
158
|
+
<Modal.Description>Are you sure?</Modal.Description>
|
|
159
|
+
</Modal.Content>
|
|
160
|
+
</Modal.Root>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Runtime theming
|
|
169
|
+
|
|
170
|
+
Seven `data-*` attributes on any ancestor element (typically `<html>` or `<body>`) re-skin every descendant at runtime, with no rebuild.
|
|
171
|
+
|
|
172
|
+
| Attribute | Values | Default | What it controls |
|
|
173
|
+
|---|---|---|---|
|
|
174
|
+
| `data-theme` | `light` · `dark` | `light` | Semantic color mapping (background, text, border, shadow). |
|
|
175
|
+
| `data-tone` | `vivid` · `pastel` · `soft` | `soft` | Surface model, palette saturation, control radius. `soft` is the signature look. |
|
|
176
|
+
| `data-gray` | `slate` · `neutral` · `stone` | `slate` | The gray family used for surfaces and text. |
|
|
177
|
+
| `data-brand` | `blue` · `violet` · `emerald` · `orange` · `mono` · `teal` | `teal` | The brand palette (`--brand-50` through `--brand-950`). |
|
|
178
|
+
| `data-radius` | `default` · `sharp` · `soft` · `pill` | `default` | The corner radius scale (`--radius-xs` through `--radius-2xl`). |
|
|
179
|
+
| `data-density` | `default` · `compact` · `comfortable` | `default` | Control heights and padding (`--control-h-*`, `--control-px-*`). |
|
|
180
|
+
| `data-font` | `geist` · `plex` · `system` | `geist` | The font pair (`--font-sans` / `--font-mono`). |
|
|
181
|
+
|
|
182
|
+
Toggle them with any approach you like — `setAttribute`, React state, a media-query listener, a server cookie:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
186
|
+
document.documentElement.setAttribute("data-brand", "violet");
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
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](https://github.com/mushroomgead/bubble-design-system).
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Components
|
|
194
|
+
|
|
195
|
+
Every component is exported from the package root:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import {
|
|
199
|
+
Alert, Avatar, Badge, Button, Card, Checkbox, Container, Divider,
|
|
200
|
+
DropdownMenu, Grid, Input, Modal, Radio, RadioGroup, Segmented,
|
|
201
|
+
Select, Skeleton, StatusPill, Switch, Tabs, Textarea, Toast, Tooltip,
|
|
202
|
+
useToast, cn,
|
|
203
|
+
} from "@bubble-design-system/ui";
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Conventions shared by all components:
|
|
207
|
+
|
|
208
|
+
- They `forwardRef` to the underlying Base UI primitive.
|
|
209
|
+
- Native HTML attributes are spread via `...props` — `onClick`, `aria-*`, `id`, `style` all just work.
|
|
210
|
+
- 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.
|
|
211
|
+
- Variants and sizes are string-literal enums with documented defaults.
|
|
212
|
+
- Both `:disabled` and `[data-disabled]` are styled (Base UI uses the data-attr form).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### Alert
|
|
217
|
+
|
|
218
|
+
A static informational banner with a variant-specific icon, title, and body.
|
|
219
|
+
|
|
220
|
+
| Prop | Type | Default | Description |
|
|
221
|
+
|---|---|---|---|
|
|
222
|
+
| `variant` | `"info" \| "success" \| "warning" \| "danger"` | `"info"` | Visual tone and default icon. |
|
|
223
|
+
| `icon` | `ReactNode \| false` | variant-specific | Override the default icon, or pass `false` to hide it. |
|
|
224
|
+
| `title` | `ReactNode` | — | Optional bold header line. |
|
|
225
|
+
| `children` | `ReactNode` | — | The body copy, rendered in the secondary text color. |
|
|
226
|
+
| `className` | `string` | — | Extra classes (appended after the library's). |
|
|
227
|
+
| `...props` | `HTMLAttributes<HTMLDivElement>` (minus `title`) | — | All native div attributes. |
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
<Alert variant="success" title="Saved">Your changes are live.</Alert>
|
|
231
|
+
<Alert variant="danger" title="Couldn't save" icon={false}>
|
|
232
|
+
Try again in a moment.
|
|
233
|
+
</Alert>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Avatar
|
|
239
|
+
|
|
240
|
+
Compound component built on `@base-ui/react/avatar` — handles image load failure with a fallback.
|
|
241
|
+
|
|
242
|
+
| Sub-component | Description |
|
|
243
|
+
|---|---|
|
|
244
|
+
| `Avatar` (root) | Sized circular surface. |
|
|
245
|
+
| `Avatar.Image` | The image element. |
|
|
246
|
+
| `Avatar.Fallback` | Shown while the image is loading or after it fails. |
|
|
247
|
+
|
|
248
|
+
**`Avatar` props:**
|
|
249
|
+
|
|
250
|
+
| Prop | Type | Default | Description |
|
|
251
|
+
|---|---|---|---|
|
|
252
|
+
| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | 24 / 32 / 40 / 48 px. |
|
|
253
|
+
| `className` | `string` | — | Extra classes. |
|
|
254
|
+
| `...props` | Base UI `Avatar.Root` props | — | Spread to the root. |
|
|
255
|
+
|
|
256
|
+
`Avatar.Image` and `Avatar.Fallback` accept their Base UI props plus `className`.
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
<Avatar size="lg">
|
|
260
|
+
<Avatar.Image src="/me.jpg" alt="Ada" />
|
|
261
|
+
<Avatar.Fallback>AL</Avatar.Fallback>
|
|
262
|
+
</Avatar>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Badge
|
|
268
|
+
|
|
269
|
+
Small inline pill for status, counts, or labels.
|
|
270
|
+
|
|
271
|
+
| Prop | Type | Default | Description |
|
|
272
|
+
|---|---|---|---|
|
|
273
|
+
| `variant` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger"` | `"neutral"` | Background and text color. |
|
|
274
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Pill height and padding. |
|
|
275
|
+
| `className` | `string` | — | Extra classes. |
|
|
276
|
+
| `...props` | `HTMLAttributes<HTMLSpanElement>` | — | Native span attributes. |
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
<Badge variant="success">Active</Badge>
|
|
280
|
+
<Badge variant="brand" size="sm">New</Badge>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
### Button
|
|
286
|
+
|
|
287
|
+
Wraps `@base-ui/react/button`.
|
|
288
|
+
|
|
289
|
+
| Prop | Type | Default | Description |
|
|
290
|
+
|---|---|---|---|
|
|
291
|
+
| `variant` | `"primary" \| "secondary" \| "destructive" \| "ghost"` | `"primary"` | Visual style. |
|
|
292
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Maps to `--control-h-*` / `--control-px-*` so density attribute affects it. |
|
|
293
|
+
| `className` | `string` | — | Extra classes. |
|
|
294
|
+
| `...props` | Base UI `Button` props | — | Includes `disabled`, `type`, `onClick`, etc. |
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
<Button variant="primary" size="lg" onClick={save}>Save</Button>
|
|
298
|
+
<Button variant="destructive">Delete</Button>
|
|
299
|
+
<Button variant="ghost" disabled>Cancel</Button>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### Card
|
|
305
|
+
|
|
306
|
+
Compound component for floating-pill surface content.
|
|
307
|
+
|
|
308
|
+
| Sub-component | Description |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `Card` (root) | The floating surface. Variant controls fill + shadow. |
|
|
311
|
+
| `Card.Header` | Row with title + optional action. |
|
|
312
|
+
| `Card.Title` | `<h3>` heading. |
|
|
313
|
+
| `Card.Description` | Supporting paragraph. |
|
|
314
|
+
| `Card.Action` | Right-aligned controls inside the header. |
|
|
315
|
+
| `Card.Body` | Main content area. |
|
|
316
|
+
| `Card.Footer` | Bordered footer row with right-aligned controls. |
|
|
317
|
+
|
|
318
|
+
**`Card` props:**
|
|
319
|
+
|
|
320
|
+
| Prop | Type | Default | Description |
|
|
321
|
+
|---|---|---|---|
|
|
322
|
+
| `variant` | `"elevated" \| "muted"` | `"elevated"` | `elevated` = white surface with shadow. `muted` = `bg-secondary`, no shadow. |
|
|
323
|
+
| `className` | `string` | — | Extra classes. |
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
<Card>
|
|
327
|
+
<Card.Header>
|
|
328
|
+
<div>
|
|
329
|
+
<Card.Title>Soft-pill surface</Card.Title>
|
|
330
|
+
<Card.Description>White card floating on a gray page.</Card.Description>
|
|
331
|
+
</div>
|
|
332
|
+
<Card.Action>
|
|
333
|
+
<Button size="sm" variant="ghost">Manage</Button>
|
|
334
|
+
</Card.Action>
|
|
335
|
+
</Card.Header>
|
|
336
|
+
<Card.Body>…</Card.Body>
|
|
337
|
+
<Card.Footer>
|
|
338
|
+
<Button size="sm" variant="ghost">Cancel</Button>
|
|
339
|
+
<Button size="sm">Save</Button>
|
|
340
|
+
</Card.Footer>
|
|
341
|
+
</Card>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### Checkbox
|
|
347
|
+
|
|
348
|
+
Wraps `@base-ui/react/checkbox`. Supports checked, unchecked, and indeterminate states with built-in SVG indicators.
|
|
349
|
+
|
|
350
|
+
| Prop | Type | Default | Description |
|
|
351
|
+
|---|---|---|---|
|
|
352
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 16 / 18 / 20 px. |
|
|
353
|
+
| `className` | `string` | — | Extra classes on the root button. |
|
|
354
|
+
| `...props` | Base UI `Checkbox.Root` props | — | `checked`, `defaultChecked`, `indeterminate`, `onCheckedChange`, etc. |
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
<Checkbox defaultChecked />
|
|
358
|
+
<Checkbox indeterminate />
|
|
359
|
+
<Checkbox disabled />
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### Container + Grid
|
|
365
|
+
|
|
366
|
+
Layout primitives. `Container` centers content and applies page margins. `Grid` is a 12-column grid; `Grid.Col` spans columns with optional responsive overrides.
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
<Container size="lg">
|
|
370
|
+
<Grid>
|
|
371
|
+
<Grid.Col span={12}>full row</Grid.Col>
|
|
372
|
+
<Grid.Col span={6} lgSpan={4}>half on mobile, third on lg</Grid.Col>
|
|
373
|
+
<Grid.Col span={6} lgSpan={4}>…</Grid.Col>
|
|
374
|
+
<Grid.Col span={12} lgSpan={4}>…</Grid.Col>
|
|
375
|
+
</Grid>
|
|
376
|
+
</Container>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**`Container` props:** `size` = `"sm" | "md" | "lg" | "xl" | "prose" | "fluid"` (default `"xl"`).
|
|
380
|
+
|
|
381
|
+
**`Grid` props:** `gutter` = `"default" | "tight" | "flush"` (default `"default"`).
|
|
382
|
+
|
|
383
|
+
**`Grid.Col` props:** `span`, `smSpan`, `mdSpan`, `lgSpan` = `1 | 2 | … | 12 | "full"`. Default `span` is `12`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
### Divider
|
|
388
|
+
|
|
389
|
+
Horizontal or vertical separator with a semantic role from `@base-ui/react/separator`.
|
|
390
|
+
|
|
391
|
+
| Prop | Type | Default | Description |
|
|
392
|
+
|---|---|---|---|
|
|
393
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction. |
|
|
394
|
+
| `className` | `string` | — | Extra classes. |
|
|
395
|
+
| `...props` | Base UI `Separator` props | — | Spread to the underlying element. |
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
<Divider />
|
|
399
|
+
<div style={{ display: "flex", height: "2rem", alignItems: "center" }}>
|
|
400
|
+
<span>A</span><Divider orientation="vertical" /><span>B</span>
|
|
401
|
+
</div>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
### DropdownMenu
|
|
407
|
+
|
|
408
|
+
Compound component built on `@base-ui/react/menu`. Supports items, checkbox items, radio groups, labels, and separators.
|
|
409
|
+
|
|
410
|
+
| Sub-component | Description |
|
|
411
|
+
|---|---|
|
|
412
|
+
| `DropdownMenu.Root` | The state container. |
|
|
413
|
+
| `DropdownMenu.Trigger` | The element that opens the menu. |
|
|
414
|
+
| `DropdownMenu.Content` | The portalled popup. |
|
|
415
|
+
| `DropdownMenu.Item` | A selectable row. |
|
|
416
|
+
| `DropdownMenu.CheckboxItem` | A toggleable row with a check indicator. |
|
|
417
|
+
| `DropdownMenu.RadioGroup` | Wrapper for radio items. |
|
|
418
|
+
| `DropdownMenu.RadioItem` | A single radio choice. |
|
|
419
|
+
| `DropdownMenu.Group` | Logical group of items. |
|
|
420
|
+
| `DropdownMenu.Label` | Uppercase group label. |
|
|
421
|
+
| `DropdownMenu.Separator` | Thin horizontal divider. |
|
|
422
|
+
|
|
423
|
+
**`DropdownMenu.Content` props:**
|
|
424
|
+
|
|
425
|
+
| Prop | Type | Default | Description |
|
|
426
|
+
|---|---|---|---|
|
|
427
|
+
| `sideOffset` | `number` | `6` | Distance in px from the trigger. |
|
|
428
|
+
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment relative to the trigger. |
|
|
429
|
+
| `className` | `string` | — | Extra classes on the popup. |
|
|
430
|
+
| `...props` | Base UI `Menu.Popup` props | — | Spread to the popup. |
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
<DropdownMenu.Root>
|
|
434
|
+
<DropdownMenu.Trigger render={<Button variant="secondary">Options</Button>} />
|
|
435
|
+
<DropdownMenu.Content>
|
|
436
|
+
<DropdownMenu.Label>Actions</DropdownMenu.Label>
|
|
437
|
+
<DropdownMenu.Item>Edit</DropdownMenu.Item>
|
|
438
|
+
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
|
|
439
|
+
<DropdownMenu.Separator />
|
|
440
|
+
<DropdownMenu.CheckboxItem checked>Pinned</DropdownMenu.CheckboxItem>
|
|
441
|
+
</DropdownMenu.Content>
|
|
442
|
+
</DropdownMenu.Root>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
### Input
|
|
448
|
+
|
|
449
|
+
Wraps `@base-ui/react/input`. Includes built-in invalid styling via `aria-invalid`.
|
|
450
|
+
|
|
451
|
+
| Prop | Type | Default | Description |
|
|
452
|
+
|---|---|---|---|
|
|
453
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Density-aware control height. |
|
|
454
|
+
| `invalid` | `boolean` | — | Sets `aria-invalid` and applies the danger border + focus ring. |
|
|
455
|
+
| `className` | `string` | — | Extra classes. |
|
|
456
|
+
| `...props` | Base UI `Input` props (minus `size`) | — | All native input attributes: `value`, `placeholder`, `type`, `onChange`, `disabled`, etc. |
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
<Input placeholder="Email" />
|
|
460
|
+
<Input size="lg" invalid value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### Modal
|
|
466
|
+
|
|
467
|
+
Compound component built on `@base-ui/react/dialog`. Renders into a portal, with backdrop blur and scale-in animation.
|
|
468
|
+
|
|
469
|
+
| Sub-component | Description |
|
|
470
|
+
|---|---|
|
|
471
|
+
| `Modal.Root` | Open-state container. |
|
|
472
|
+
| `Modal.Trigger` | Element that opens the modal. |
|
|
473
|
+
| `Modal.Close` | Element that closes the modal. |
|
|
474
|
+
| `Modal.Content` | The portalled popup with backdrop. |
|
|
475
|
+
| `Modal.Title` | Heading text. |
|
|
476
|
+
| `Modal.Description` | Sub-text under the title. |
|
|
477
|
+
|
|
478
|
+
**`Modal.Content` props:**
|
|
479
|
+
|
|
480
|
+
| Prop | Type | Default | Description |
|
|
481
|
+
|---|---|---|---|
|
|
482
|
+
| `className` | `string` | — | Extra classes on the popup. |
|
|
483
|
+
| `backdropClassName` | `string` | — | Extra classes on the backdrop. |
|
|
484
|
+
| `...props` | Base UI `Dialog.Popup` props | — | Spread to the popup. |
|
|
485
|
+
|
|
486
|
+
`Modal.Title` and `Modal.Description` accept their Base UI props plus `className`.
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<Modal.Root>
|
|
490
|
+
<Modal.Trigger render={<Button>Open</Button>} />
|
|
491
|
+
<Modal.Content>
|
|
492
|
+
<Modal.Title>Delete project?</Modal.Title>
|
|
493
|
+
<Modal.Description>This action cannot be undone.</Modal.Description>
|
|
494
|
+
<div style={{ marginTop: "1rem", display: "flex", justifyContent: "flex-end", gap: "0.5rem" }}>
|
|
495
|
+
<Modal.Close render={<Button variant="secondary">Cancel</Button>} />
|
|
496
|
+
<Button variant="destructive">Delete</Button>
|
|
497
|
+
</div>
|
|
498
|
+
</Modal.Content>
|
|
499
|
+
</Modal.Root>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
### Radio / RadioGroup
|
|
505
|
+
|
|
506
|
+
Wraps `@base-ui/react/radio` and `@base-ui/react/radio-group`.
|
|
507
|
+
|
|
508
|
+
**`Radio` props:**
|
|
509
|
+
|
|
510
|
+
| Prop | Type | Default | Description |
|
|
511
|
+
|---|---|---|---|
|
|
512
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 16 / 18 / 20 px. |
|
|
513
|
+
| `className` | `string` | — | Extra classes. |
|
|
514
|
+
| `...props` | Base UI `Radio.Root` props | — | `value`, `disabled`, etc. |
|
|
515
|
+
|
|
516
|
+
**`RadioGroup` props:**
|
|
517
|
+
|
|
518
|
+
| Prop | Type | Default | Description |
|
|
519
|
+
|---|---|---|---|
|
|
520
|
+
| `className` | `string` | — | Override the default vertical stack. |
|
|
521
|
+
| `...props` | Base UI `RadioGroup` props | — | `value`, `defaultValue`, `onValueChange`. |
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
<RadioGroup defaultValue="email">
|
|
525
|
+
<label><Radio value="email" /> Email</label>
|
|
526
|
+
<label><Radio value="sms" /> SMS</label>
|
|
527
|
+
</RadioGroup>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
### Segmented
|
|
533
|
+
|
|
534
|
+
Compound component built on `@base-ui/react/toggle-group` (single-select). The selected item rises as a white floating pill.
|
|
535
|
+
|
|
536
|
+
| Sub-component | Description |
|
|
537
|
+
|---|---|
|
|
538
|
+
| `Segmented` (root) | The toggle group. |
|
|
539
|
+
| `Segmented.Item` | A single segment. |
|
|
540
|
+
|
|
541
|
+
**`Segmented` props:**
|
|
542
|
+
|
|
543
|
+
| Prop | Type | Default | Description |
|
|
544
|
+
|---|---|---|---|
|
|
545
|
+
| `value` | `string` | — | Controlled selected value. |
|
|
546
|
+
| `defaultValue` | `string` | — | Uncontrolled initial value. |
|
|
547
|
+
| `onValueChange` | `(value: string) => void` | — | Fired with the new value. |
|
|
548
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 24 / 28 / 32 px. |
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
<Segmented value={range} onValueChange={setRange}>
|
|
552
|
+
<Segmented.Item value="day">Day</Segmented.Item>
|
|
553
|
+
<Segmented.Item value="week">Week</Segmented.Item>
|
|
554
|
+
<Segmented.Item value="month">Month</Segmented.Item>
|
|
555
|
+
</Segmented>
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
### Select
|
|
561
|
+
|
|
562
|
+
Compound component built on `@base-ui/react/select`.
|
|
563
|
+
|
|
564
|
+
| Sub-component | Description |
|
|
565
|
+
|---|---|
|
|
566
|
+
| `Select.Root` | State container (`value`, `onValueChange`). |
|
|
567
|
+
| `Select.Trigger` | The clickable trigger; renders a chevron icon. |
|
|
568
|
+
| `Select.Value` | The selected value's display. |
|
|
569
|
+
| `Select.Content` | The portalled popup. |
|
|
570
|
+
| `Select.Item` | A selectable row with a check indicator when selected. |
|
|
571
|
+
|
|
572
|
+
**`Select.Trigger` props:** `size` = `"sm" | "md" | "lg"` (default `"md"`), plus `className`.
|
|
573
|
+
|
|
574
|
+
**`Select.Value` props:** `placeholder?: ReactNode`, plus `className`.
|
|
575
|
+
|
|
576
|
+
**`Select.Content` props:** `sideOffset?: number` (default `6`), plus `className`.
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
<Select.Root value={fruit} onValueChange={setFruit}>
|
|
580
|
+
<Select.Trigger size="md">
|
|
581
|
+
<Select.Value placeholder="Pick one" />
|
|
582
|
+
</Select.Trigger>
|
|
583
|
+
<Select.Content>
|
|
584
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
585
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
586
|
+
<Select.Item value="cherry">Cherry</Select.Item>
|
|
587
|
+
</Select.Content>
|
|
588
|
+
</Select.Root>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
### Skeleton
|
|
594
|
+
|
|
595
|
+
Loading placeholder with a pulse animation.
|
|
596
|
+
|
|
597
|
+
| Prop | Type | Default | Description |
|
|
598
|
+
|---|---|---|---|
|
|
599
|
+
| `shape` | `"line" \| "circle" \| "block"` | `"line"` | Default dimensions and radius. |
|
|
600
|
+
| `className` | `string` | — | Override width/height/radius via your own CSS. |
|
|
601
|
+
| `...props` | `HTMLAttributes<HTMLDivElement>` | — | Native div attributes. |
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
<Skeleton />
|
|
605
|
+
<Skeleton shape="circle" style={{ width: "2.5rem", height: "2.5rem" }} />
|
|
606
|
+
<Skeleton shape="block" style={{ height: "6rem" }} />
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
### StatusPill
|
|
612
|
+
|
|
613
|
+
Compound floating-pill component for status indicators. Intent drives chip + label color via CSS custom properties.
|
|
614
|
+
|
|
615
|
+
| Sub-component | Description |
|
|
616
|
+
|---|---|
|
|
617
|
+
| `StatusPill` (root) | The pill surface. |
|
|
618
|
+
| `StatusPill.Indicator` | The leading colored chip. Children render an optional icon. |
|
|
619
|
+
| `StatusPill.Label` | The colored text label. |
|
|
620
|
+
|
|
621
|
+
**`StatusPill` props:** `intent` = `"neutral" | "success" | "warning" | "danger" | "info"` (default `"neutral"`).
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
<StatusPill intent="success">
|
|
625
|
+
<StatusPill.Indicator />
|
|
626
|
+
<StatusPill.Label>On track</StatusPill.Label>
|
|
627
|
+
</StatusPill>
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
### Switch
|
|
633
|
+
|
|
634
|
+
Wraps `@base-ui/react/switch` — a thumb that slides on `data-[checked]`.
|
|
635
|
+
|
|
636
|
+
| Prop | Type | Default | Description |
|
|
637
|
+
|---|---|---|---|
|
|
638
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 16 / 20 / 24 px tall. |
|
|
639
|
+
| `className` | `string` | — | Extra classes on the root. |
|
|
640
|
+
| `...props` | Base UI `Switch.Root` props | — | `checked`, `defaultChecked`, `onCheckedChange`, `disabled`. |
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
<Switch defaultChecked />
|
|
644
|
+
<Switch size="sm" />
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
### Tabs
|
|
650
|
+
|
|
651
|
+
Compound component built on `@base-ui/react/tabs`. The list renders an animated indicator that slides between active tabs.
|
|
652
|
+
|
|
653
|
+
| Sub-component | Description |
|
|
654
|
+
|---|---|
|
|
655
|
+
| `Tabs` (root) | State container; pass `value`, `defaultValue`, `onValueChange`. |
|
|
656
|
+
| `Tabs.List` | Horizontal tab strip with a sliding indicator. |
|
|
657
|
+
| `Tabs.Tab` | A single tab button. |
|
|
658
|
+
| `Tabs.Panel` | The panel paired with a tab `value`. |
|
|
659
|
+
|
|
660
|
+
All sub-components accept their Base UI props plus `className`.
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
<Tabs defaultValue="profile">
|
|
664
|
+
<Tabs.List>
|
|
665
|
+
<Tabs.Tab value="profile">Profile</Tabs.Tab>
|
|
666
|
+
<Tabs.Tab value="account">Account</Tabs.Tab>
|
|
667
|
+
<Tabs.Tab value="billing">Billing</Tabs.Tab>
|
|
668
|
+
</Tabs.List>
|
|
669
|
+
<Tabs.Panel value="profile">Profile content</Tabs.Panel>
|
|
670
|
+
<Tabs.Panel value="account">Account content</Tabs.Panel>
|
|
671
|
+
<Tabs.Panel value="billing">Billing content</Tabs.Panel>
|
|
672
|
+
</Tabs>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
### Textarea
|
|
678
|
+
|
|
679
|
+
A thin styled `<textarea>` mirroring Input's API.
|
|
680
|
+
|
|
681
|
+
| Prop | Type | Default | Description |
|
|
682
|
+
|---|---|---|---|
|
|
683
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Density-aware control padding. |
|
|
684
|
+
| `invalid` | `boolean` | — | Sets `aria-invalid` and applies the danger border + focus ring. |
|
|
685
|
+
| `className` | `string` | — | Extra classes. |
|
|
686
|
+
| `...props` | `TextareaHTMLAttributes` (minus `size`) | — | All native textarea attributes. |
|
|
687
|
+
|
|
688
|
+
```tsx
|
|
689
|
+
<Textarea placeholder="Notes" rows={4} />
|
|
690
|
+
<Textarea invalid value={text} onChange={(e) => setText(e.target.value)} />
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
### Toast
|
|
696
|
+
|
|
697
|
+
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.
|
|
698
|
+
|
|
699
|
+
| Sub-component | Description |
|
|
700
|
+
|---|---|
|
|
701
|
+
| `Toast.Provider` | Wrap your app to enable toasts. |
|
|
702
|
+
| `Toast.Viewport` | Positioned region where toasts mount (bottom-right by default). |
|
|
703
|
+
| `Toast.Toaster` | Pre-styled queue renderer — drop this inside `Provider`. |
|
|
704
|
+
| `useToast()` | Hook returning Base UI's toast manager (`.add({ title, description })`). |
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
import { Toast, useToast, Button } from "@bubble-design-system/ui";
|
|
708
|
+
|
|
709
|
+
function Root({ children }) {
|
|
710
|
+
return (
|
|
711
|
+
<Toast.Provider>
|
|
712
|
+
{children}
|
|
713
|
+
<Toast.Toaster />
|
|
714
|
+
</Toast.Provider>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function SaveButton() {
|
|
719
|
+
const toast = useToast();
|
|
720
|
+
return (
|
|
721
|
+
<Button onClick={() => toast.add({ title: "Saved", description: "Changes are live." })}>
|
|
722
|
+
Save
|
|
723
|
+
</Button>
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
### Tooltip
|
|
731
|
+
|
|
732
|
+
Compound component built on `@base-ui/react/tooltip`. Requires a `Tooltip.Provider` ancestor (typically once at the app root).
|
|
733
|
+
|
|
734
|
+
| Sub-component | Description |
|
|
735
|
+
|---|---|
|
|
736
|
+
| `Tooltip.Provider` | App-level provider. |
|
|
737
|
+
| `Tooltip.Root` | Single tooltip state container. |
|
|
738
|
+
| `Tooltip.Trigger` | The hovered/focused element. |
|
|
739
|
+
| `Tooltip.Content` | The portalled popup. |
|
|
740
|
+
|
|
741
|
+
**`Tooltip.Content` props:**
|
|
742
|
+
|
|
743
|
+
| Prop | Type | Default | Description |
|
|
744
|
+
|---|---|---|---|
|
|
745
|
+
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Preferred side. |
|
|
746
|
+
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the side. |
|
|
747
|
+
| `sideOffset` | `number` | `6` | Distance from the trigger. |
|
|
748
|
+
| `className` | `string` | — | Extra classes on the popup. |
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
<Tooltip.Provider>
|
|
752
|
+
<Tooltip.Root>
|
|
753
|
+
<Tooltip.Trigger render={<Button variant="ghost">?</Button>} />
|
|
754
|
+
<Tooltip.Content side="top">Helpful hint</Tooltip.Content>
|
|
755
|
+
</Tooltip.Root>
|
|
756
|
+
</Tooltip.Provider>
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
### `cn()` utility
|
|
762
|
+
|
|
763
|
+
Re-exported `clsx` wrapper for composing class names — useful when building your own components against the design tokens.
|
|
764
|
+
|
|
765
|
+
```tsx
|
|
766
|
+
import { cn } from "@bubble-design-system/ui";
|
|
767
|
+
|
|
768
|
+
cn("my-card", isActive && "my-card--active", className);
|
|
769
|
+
// → "my-card my-card--active <consumer className>"
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## Design tokens
|
|
775
|
+
|
|
776
|
+
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.
|
|
777
|
+
|
|
778
|
+
### Color tokens
|
|
779
|
+
|
|
780
|
+
Semantic colors map to primitive palettes and are remapped by `[data-theme]`, `[data-gray]`, and `[data-brand]`.
|
|
781
|
+
|
|
782
|
+
| Token | Purpose |
|
|
783
|
+
|---|---|
|
|
784
|
+
| `--color-bg-primary` | Page background. |
|
|
785
|
+
| `--color-bg-secondary` | Secondary surface (cards on page). |
|
|
786
|
+
| `--color-bg-tertiary` | Tertiary surface (inset wells). |
|
|
787
|
+
| `--color-bg-inverse` | Inverted surface (tooltip, toast). |
|
|
788
|
+
| `--color-bg-brand` | Brand primary fill. |
|
|
789
|
+
| `--color-bg-brand-hover` | Brand hover state. |
|
|
790
|
+
| `--color-bg-brand-active` | Brand pressed state. |
|
|
791
|
+
| `--color-bg-brand-subtle` | Tinted brand surface (info backgrounds, badges). |
|
|
792
|
+
| `--color-bg-success` | Soft success surface. |
|
|
793
|
+
| `--color-bg-success-strong` | Solid success fill. |
|
|
794
|
+
| `--color-bg-warning` | Soft warning surface. |
|
|
795
|
+
| `--color-bg-warning-strong` | Solid warning fill. |
|
|
796
|
+
| `--color-bg-danger` | Soft danger surface. |
|
|
797
|
+
| `--color-bg-danger-strong` | Solid danger fill (destructive buttons). |
|
|
798
|
+
| `--color-bg-danger-hover` | Danger hover state. |
|
|
799
|
+
| `--color-bg-info` | Info alert surface. |
|
|
800
|
+
| `--color-bg-hover` | Neutral hover. |
|
|
801
|
+
| `--color-bg-pressed` | Neutral pressed. |
|
|
802
|
+
| `--color-bg-disabled` | Disabled surface. |
|
|
803
|
+
| `--color-text-primary` | Body text. |
|
|
804
|
+
| `--color-text-secondary` | Supporting text. |
|
|
805
|
+
| `--color-text-tertiary` | Placeholder, hints. |
|
|
806
|
+
| `--color-text-disabled` | Disabled text. |
|
|
807
|
+
| `--color-text-inverse` | Text on `bg-inverse`. |
|
|
808
|
+
| `--color-text-brand` | Brand-colored text (links). |
|
|
809
|
+
| `--color-text-success` | Success copy. |
|
|
810
|
+
| `--color-text-warning` | Warning copy. |
|
|
811
|
+
| `--color-text-danger` | Error copy. |
|
|
812
|
+
| `--color-text-on-brand` | Text on `bg-brand`. |
|
|
813
|
+
| `--color-text-on-danger` | Text on `bg-danger-strong`. |
|
|
814
|
+
| `--color-text-on-success` | Text on `bg-success-strong`. |
|
|
815
|
+
| `--color-border-primary` | Default form/control border. |
|
|
816
|
+
| `--color-border-secondary` | Section dividers, cards. |
|
|
817
|
+
| `--color-border-tertiary` | Subtle inner dividers. |
|
|
818
|
+
| `--color-border-brand` | Selected/active accent. |
|
|
819
|
+
| `--color-border-success` | Success accent. |
|
|
820
|
+
| `--color-border-warning` | Warning accent. |
|
|
821
|
+
| `--color-border-danger` | Error accent (invalid inputs). |
|
|
822
|
+
| `--color-border-focus` | Focus ring color. |
|
|
823
|
+
|
|
824
|
+
### Primitive palettes
|
|
825
|
+
|
|
826
|
+
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.
|
|
827
|
+
|
|
828
|
+
| Family | Stops | Notes |
|
|
829
|
+
|---|---|---|
|
|
830
|
+
| `--slate-*` | 50–950 | Default gray family. |
|
|
831
|
+
| `--neutral-*` | 50–950 | True neutral (no temperature). |
|
|
832
|
+
| `--stone-*` | 50–950 | Warm gray. |
|
|
833
|
+
| `--blue-*` | 50–950 | Brand option. |
|
|
834
|
+
| `--violet-*` | 50–950 | Brand option. |
|
|
835
|
+
| `--emerald-*` | 50–950 | Brand option. |
|
|
836
|
+
| `--orange-*` | 50–950 | Brand option. |
|
|
837
|
+
| `--green-*` | 50–950 | Success palette. |
|
|
838
|
+
| `--amber-*` | 50–950 | Warning palette. |
|
|
839
|
+
| `--red-*` | 50–950 | Danger palette. |
|
|
840
|
+
| `--white`, `--black` | — | Pure values. |
|
|
841
|
+
|
|
842
|
+
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.
|
|
843
|
+
|
|
844
|
+
### Radius
|
|
845
|
+
|
|
846
|
+
Selected by `[data-radius]`. Each scale rewrites the same custom properties.
|
|
847
|
+
|
|
848
|
+
| Token | default | sharp | soft | pill |
|
|
849
|
+
|---|---|---|---|---|
|
|
850
|
+
| `--radius-xs` | 2px | 0px | 4px | 4px |
|
|
851
|
+
| `--radius-sm` | 4px | 1px | 8px | 9999px |
|
|
852
|
+
| `--radius-md` | 6px | 2px | 12px | 9999px |
|
|
853
|
+
| `--radius-lg` | 8px | 3px | 14px | 9999px |
|
|
854
|
+
| `--radius-xl` | 12px | 4px | 18px | 18px |
|
|
855
|
+
| `--radius-2xl` | 16px | 6px | 24px | 22px |
|
|
856
|
+
| `--radius-full` | 9999px | 9999px | 9999px | 9999px |
|
|
857
|
+
|
|
858
|
+
Plus `--ctrl-radius` — the control radius used by Button/Input/Select. Pills under `[data-tone="soft"]`, `--radius-md` elsewhere.
|
|
859
|
+
|
|
860
|
+
### Shadow
|
|
861
|
+
|
|
862
|
+
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.
|
|
863
|
+
|
|
864
|
+
| Token | Purpose |
|
|
865
|
+
|---|---|
|
|
866
|
+
| `--shadow-xs` | Hairline lift. |
|
|
867
|
+
| `--shadow-sm` | Subtle card. |
|
|
868
|
+
| `--shadow-md` | Standard card. |
|
|
869
|
+
| `--shadow-lg` | Popover, dropdown. |
|
|
870
|
+
| `--shadow-xl` | Modal. |
|
|
871
|
+
| `--shadow-focus` | Focus ring (3–4px brand-tinted). |
|
|
872
|
+
|
|
873
|
+
### Typography
|
|
874
|
+
|
|
875
|
+
| Token | Value |
|
|
876
|
+
|---|---|
|
|
877
|
+
| `--font-size-xs` | 0.75rem |
|
|
878
|
+
| `--font-size-sm` | 0.875rem |
|
|
879
|
+
| `--font-size-md` | 1rem |
|
|
880
|
+
| `--font-size-lg` | 1.125rem |
|
|
881
|
+
| `--font-size-xl` | 1.25rem |
|
|
882
|
+
| `--font-size-2xl` | 1.5rem |
|
|
883
|
+
| `--font-size-3xl` | 1.875rem |
|
|
884
|
+
| `--font-size-4xl` | 2.25rem |
|
|
885
|
+
| `--font-size-5xl` | 3rem |
|
|
886
|
+
| `--font-size-6xl` | 3.75rem |
|
|
887
|
+
| `--line-height-tight` / `snug` / `normal` / `relaxed` | 1.15 / 1.3 / 1.5 / 1.65 |
|
|
888
|
+
| `--letter-tight` / `snug` / `normal` / `wide` | -0.022em / -0.012em / 0 / 0.04em |
|
|
889
|
+
| `--font-weight-regular` / `medium` / `semibold` / `bold` | 400 / 500 / 600 / 700 |
|
|
890
|
+
| `--font-sans` / `--font-mono` | Set by `[data-font]` |
|
|
891
|
+
|
|
892
|
+
### Spacing
|
|
893
|
+
|
|
894
|
+
`--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.
|
|
895
|
+
|
|
896
|
+
### Density (control sizing)
|
|
897
|
+
|
|
898
|
+
Selected by `[data-density]`.
|
|
899
|
+
|
|
900
|
+
| Token | default | compact | comfortable |
|
|
901
|
+
|---|---|---|---|
|
|
902
|
+
| `--control-h-sm` | 28px | 24px | 32px |
|
|
903
|
+
| `--control-h-md` | 36px | 30px | 42px |
|
|
904
|
+
| `--control-h-lg` | 44px | 38px | 52px |
|
|
905
|
+
| `--control-px-sm` | 10px | 8px | 12px |
|
|
906
|
+
| `--control-px-md` | 14px | 12px | 18px |
|
|
907
|
+
| `--control-px-lg` | 18px | 16px | 22px |
|
|
908
|
+
| `--card-p` | 24px | 16px | 32px |
|
|
909
|
+
| `--row-gap` | 16px | 12px | 20px |
|
|
910
|
+
|
|
911
|
+
### Motion
|
|
912
|
+
|
|
913
|
+
| Token | Value |
|
|
914
|
+
|---|---|
|
|
915
|
+
| `--duration-instant` | 50ms |
|
|
916
|
+
| `--duration-fast` | 120ms |
|
|
917
|
+
| `--duration-normal` | 200ms |
|
|
918
|
+
| `--duration-slow` | 320ms |
|
|
919
|
+
| `--duration-slower` | 500ms |
|
|
920
|
+
| `--ease-linear` | `linear` |
|
|
921
|
+
| `--ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` |
|
|
922
|
+
| `--ease-in-out` | `cubic-bezier(0.65, 0, 0.35, 1)` |
|
|
923
|
+
| `--ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` |
|
|
924
|
+
|
|
925
|
+
### Browsing tokens visually
|
|
926
|
+
|
|
927
|
+
Clone the repository and run the docs app — `/tokens` renders live swatches that react to the `data-*` attribute switches:
|
|
928
|
+
|
|
929
|
+
```bash
|
|
930
|
+
pnpm install
|
|
931
|
+
pnpm -C packages/ui build # build the lib first
|
|
932
|
+
pnpm -C apps/docs dev
|
|
933
|
+
# open http://localhost:3000/tokens
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## Overriding styles
|
|
939
|
+
|
|
940
|
+
Every component emits stable, low-specificity BEM class names. Override them from your own CSS with a single-class selector:
|
|
941
|
+
|
|
942
|
+
```css
|
|
943
|
+
/* Bump up button padding globally */
|
|
944
|
+
.pds-btn--md {
|
|
945
|
+
padding-inline: 1.25rem;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/* Give your app's brand button a different shadow */
|
|
949
|
+
.my-app .pds-btn--primary {
|
|
950
|
+
box-shadow: 0 6px 18px -4px rgba(0, 200, 200, 0.4);
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Or pass `className` directly to a component — it's appended after the library's own classes:
|
|
955
|
+
|
|
956
|
+
```tsx
|
|
957
|
+
<Button className="my-special-button">Action</Button>
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
```css
|
|
961
|
+
.my-special-button {
|
|
962
|
+
letter-spacing: 0.05em;
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
The full BEM block list lives in `dist/styles.css` if you want to grep it; the canonical authoring source is `packages/ui/src/components/*.css` in the repository.
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
## Local development
|
|
971
|
+
|
|
972
|
+
All commands run from the repo root. Package manager is `pnpm@10.33.0` (Node ≥ 20).
|
|
973
|
+
|
|
974
|
+
```bash
|
|
975
|
+
pnpm install # install all workspaces
|
|
976
|
+
|
|
977
|
+
# Library
|
|
978
|
+
pnpm -C packages/ui typecheck # tsc --noEmit
|
|
979
|
+
pnpm -C packages/ui build # tsup → dist/, then build-css.mjs concatenates styles.css
|
|
980
|
+
pnpm -C packages/ui dev # tsup --watch (CSS changes need a fresh `pnpm -C packages/ui build`)
|
|
981
|
+
pnpm -C packages/ui clean # rm -rf dist
|
|
982
|
+
|
|
983
|
+
# Docs app
|
|
984
|
+
pnpm -C apps/docs typecheck
|
|
985
|
+
pnpm -C apps/docs dev # Next.js 16 Turbopack on :3000
|
|
986
|
+
pnpm -C apps/docs build # next build
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
There is no test runner configured yet, and no root-level lint. Typecheck is the only static gate.
|
|
990
|
+
|
|
991
|
+
The docs app imports `@bubble-design-system/ui/styles.css` directly from the lib's `dist/`, so any CSS change in the lib needs a fresh `pnpm -C packages/ui build` to show up in the docs dev server. (Component prop / JSX changes hot-reload normally.)
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
## Contributing
|
|
996
|
+
|
|
997
|
+
1. **Read `PROGRESS.md` first.** It is the source of truth for project state, decision history (with rationale), the next-step task list, and verification commands. Update it after any non-trivial change.
|
|
998
|
+
|
|
999
|
+
2. **Component authoring rules** (enforced by convention, not lint):
|
|
1000
|
+
|
|
1001
|
+
- Content goes through `children`, not `label`/`title`/`text` props. The exception is semantically distinct slots like `Toast` title + description.
|
|
1002
|
+
- Always `forwardRef` to the Base UI primitive.
|
|
1003
|
+
- Spread `...props` so consumers get every native HTML attribute for free.
|
|
1004
|
+
- `Omit<BaseProps, "className">` then re-add `className?: string` to narrow Base UI's union type.
|
|
1005
|
+
- Variant / size are discriminated string enums with sensible defaults.
|
|
1006
|
+
- `cn("pds-block", `pds-block--${variant}`, `pds-block--${size}`, className)` — user `className` **last**, so it appears after the library's defaults in the output string.
|
|
1007
|
+
- Handle both `disabled:` and `[data-disabled]` (Base UI uses the data-attr form) in the component's CSS file.
|
|
1008
|
+
- Export the component and its `Props` type from `src/index.ts`.
|
|
1009
|
+
|
|
1010
|
+
3. **CSS authoring rules:**
|
|
1011
|
+
|
|
1012
|
+
- One CSS file per component under `src/components/`. The block name is `pds-<component>` (kebab-case for multi-word components, e.g. `pds-status-pill`).
|
|
1013
|
+
- Use BEM: `.pds-btn`, `.pds-btn__icon`, `.pds-btn--primary`. Avoid descendant or nested selectors that raise specificity above (0, 1, 0).
|
|
1014
|
+
- Reference tokens directly: `background-color: var(--color-bg-brand)`. Never hard-code a value that has a token.
|
|
1015
|
+
- The build pipeline concatenates all component CSS plus `tokens.css` + `base.css` into one shipped `dist/styles.css`.
|
|
1016
|
+
|
|
1017
|
+
4. **Token rules:**
|
|
1018
|
+
|
|
1019
|
+
- Token names are part of the design spec and round-trip 1:1 with code. Do **not** rename tokens for cosmetic reasons.
|
|
1020
|
+
- Component CSS reads `var(--…)` at use-site so runtime `data-*` switching keeps working. If a rule resolves a token to a literal value, it freezes — never inline a token's resolved value.
|
|
1021
|
+
|
|
1022
|
+
5. **Stack gotchas:**
|
|
1023
|
+
|
|
1024
|
+
- TypeScript 6 needs `ignoreDeprecations: "6.0"` (already set in `tsconfig.base.json`) because `tsup`'s `rollup-plugin-dts` still uses the deprecated `baseUrl`. Leave the flag until that toolchain catches up.
|
|
1025
|
+
- `@base-ui/react` was renamed from `@base-ui-components/react` at the 1.0 stable release (2025-12-11). Import paths use the new scope: `@base-ui/react/<primitive>`.
|
|
1026
|
+
- The library ships a global `box-sizing: border-box` in `base.css`. Component CSS assumes it.
|
|
1027
|
+
|
|
1028
|
+
6. **Before opening a PR:**
|
|
1029
|
+
|
|
1030
|
+
- `pnpm -C packages/ui typecheck`
|
|
1031
|
+
- `pnpm -C packages/ui build`
|
|
1032
|
+
- `pnpm -C apps/docs typecheck`
|
|
1033
|
+
- Spot-check the docs gallery and verify toggling `data-theme` / `data-brand` / `data-radius` / `data-density` re-skins live (no rebuild needed). If live switching stops working, the most likely cause is a CSS rule that hard-coded a value instead of referencing a token.
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## License
|
|
1038
|
+
|
|
1039
|
+
MIT
|