@usememos/ui 26.11.16 → 26.11.18
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/dist/components/button/index.js +1 -1
- package/dist/components/button/index.js.map +3 -3
- package/dist/components/collapsible/index.js.map +1 -1
- package/dist/components/select/index.js +1 -1
- package/dist/components/select/index.js.map +3 -3
- package/dist/components/tabs/index.js.map +1 -1
- package/dist/components/toggle/index.js +1 -1
- package/dist/components/toggle/index.js.map +3 -3
- package/dist/components/toggle-group/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +2 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +1 -0
- package/dist/theme-tokens.js +1 -1
- package/dist/theme-tokens.js.map +2 -2
- package/dist/tokens.css +1 -1
- package/dist/types/components/button/index.d.ts +4 -2
- package/dist/types/components/collapsible/index.d.ts +1 -1
- package/dist/types/components/select/index.d.ts +2 -2
- package/dist/types/components/tabs/index.d.ts +1 -1
- package/dist/types/components/toggle/index.d.ts +5 -1
- package/dist/types/components/toggle-group/index.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/theme-tokens.d.ts +2 -2
- package/dist/types/types.d.ts +2 -0
- package/llms-full.txt +243 -81
- package/llms.txt +3 -2
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Memos UI design system
|
|
4
4
|
|
|
5
|
-
Memos UI is one calm, dense and flat system for every product we build: memos (the app), the website, the web clipper and whatever comes next.
|
|
5
|
+
Memos UI is one calm, dense and flat system for every product we build: memos (the app), the website, the web clipper and whatever comes next. Quiet neutrals, a warm Light canvas and ink emphasis keep attention on people's content. Hierarchy comes from type, spacing and fills. Components reuse a few shared state treatments; pointer events do not add decorative motion or effects.
|
|
6
6
|
|
|
7
7
|
These docs describe the target system. They are the source of truth for `@usememos/ui`. Where the code in `packages/` differs from them, the code is out of date and should move toward the docs.
|
|
8
8
|
|
|
@@ -17,12 +17,13 @@ It is not a general-purpose UI library for outside developers. Components are ge
|
|
|
17
17
|
|
|
18
18
|
| Doc | What it covers |
|
|
19
19
|
| --- | --- |
|
|
20
|
+
| [Design reference: Linear](design-reference.md) | Official source evidence and how we apply it to Memos UI |
|
|
20
21
|
| [Principles and voice](principles.md) | The taste in six rules, and how interface text is written |
|
|
21
22
|
| [Color](color.md) | Color roles, every color token in Light and Dark, and how themes are chosen |
|
|
22
23
|
| [Depth](depth.md) | Flat surface roles, borders and focus without shadows or outlines |
|
|
23
24
|
| [Typography](typography.md) | The system font, the type scale and text rules |
|
|
24
25
|
| [Size, spacing and layout](layout.md) | Control sizes, the 4px scale, radius, layout widths, breakpoints and layers |
|
|
25
|
-
| [Motion](motion.md) |
|
|
26
|
+
| [Motion](motion.md) | Immediate feedback, minimal opening and closing, and reduced motion |
|
|
26
27
|
| [States](states.md) | Hover, pressed, loading, disabled, invalid and focus |
|
|
27
28
|
| [Icons](icons.md) | Lucide, sizes, the fixed stroke and accessibility |
|
|
28
29
|
| [Components](components/README.md) | The 31 components, what was left out, and one page per component |
|
|
@@ -36,26 +37,61 @@ It is not a general-purpose UI library for outside developers. Components are ge
|
|
|
36
37
|
- **Behavior from Base UI.** Focus management, keyboard support, ARIA, portals and positioning come from [Base UI](https://base-ui.com). Styling, tokens and the component API are our own.
|
|
37
38
|
- **No shadcn.** No shadcn dependency, no copied shadcn code, and no shadcn-compatible component or token names (`card`, `muted`, `destructive`, `foreground` and so on are not used).
|
|
38
39
|
- **Flat and calm by default.** Fills identify controls and state. Reuse shared interaction treatments without press movement, scaling or brightness filters. Light is the default theme; Dark ships with it.
|
|
39
|
-
- **Ink is the accent.** The primary action and
|
|
40
|
-
- **One quiet hue.** `accent` is a quiet blue used only for links and text selection, never as a fill.
|
|
40
|
+
- **Ink is the accent.** The primary action and checked marks use `primary`: near-black in Light, near-white in Dark. Toggles and single choices use quiet saved-state fills.
|
|
41
|
+
- **One quiet hue.** `accent` is a quiet blue used only for links and text selection, never as a fill. Non-text keyboard focus is ink; text-entry focus uses its caret or selection.
|
|
41
42
|
- **Status colors only for status.** Green means success, amber means warning, red means danger or error. None of them is used for emphasis.
|
|
42
43
|
- **Lucide icons** at one fixed screen-pixel stroke, 1.25px, at every size.
|
|
43
44
|
- **The system UI font** for all text. The package ships no font files.
|
|
44
45
|
- **Few sizes, two weights.** App UI uses 13, 14, 16 and 20px text at weights 400 and 500. 20px is for the page title only.
|
|
45
46
|
- **Dense, on a 4px grid.** Controls are 32px tall by default.
|
|
46
47
|
|
|
48
|
+
<!-- docs/design-reference.md -->
|
|
49
|
+
|
|
50
|
+
# Design reference: Linear
|
|
51
|
+
|
|
52
|
+
Linear's product interface is the reference for our hierarchy, restraint and interaction judgment. Our rules below turn that direction into a contract for Memos UI.
|
|
53
|
+
|
|
54
|
+
## What the sources establish
|
|
55
|
+
|
|
56
|
+
- The [March 2026 refresh](https://linear.app/now/behind-the-latest-design-refresh) reduces competing chrome, unnecessary icons and dividing lines. Its neutrals move toward a warmer, less saturated gray while content stays clear.
|
|
57
|
+
- The [2024 redesign](https://linear.app/now/how-we-redesigned-the-linear-ui) treats alignment, density and hierarchy as one system, tested across views, platforms and themes.
|
|
58
|
+
- The [August 2026 styling account](https://linear.app/now/styling-linear-for-the-future-stylex) describes explicit component contracts and shared interaction styling, including a highlight treatment for pointer hover and touch press. It also says Linear does not have a formal design system; these articles are not a complete public component specification.
|
|
59
|
+
- The [contextual-menu account](https://linear.app/now/invisible-details) improves the pointer's path into a submenu. Craft includes reducing effort even when no visible effect is added.
|
|
60
|
+
- Emil Kowalski's [animation guidance](https://emilkowal.ski/ui/you-dont-need-animations), with examples from his work at Linear, distinguishes purposeful motion from repeated effects that slow work. It supports short content transitions and immediate keyboard navigation; it does not prescribe one timing for every Linear component.
|
|
61
|
+
|
|
62
|
+
## How we apply this
|
|
63
|
+
|
|
64
|
+
These are Memos UI decisions informed by those sources and product review, not claims about Linear's private CSS, tokens or animation timings. A screenshot of a focused composer does not establish how all of Linear's inputs or keyboard paths behave.
|
|
65
|
+
|
|
66
|
+
| Before | After | Why |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| Every interaction event calls for a distinct appearance | Implement every behavioral state; add a visible change only when it conveys useful information | Events are not visual variants |
|
|
69
|
+
| A text field emphasizes its entire boundary on focus | Keep border and surface stable; use its caret or text selection | Content and editing stay primary |
|
|
70
|
+
| Holding an action adds a new fill or contrasting border | Reuse one immediate highlight for hover and press; keyboard focus stays independent | Fewer transient treatments |
|
|
71
|
+
| Tabs and single-choice groups animate each selection | Update the selected key immediately | Frequent navigation does not wait for decoration |
|
|
72
|
+
| Select has a separate opening treatment over its chosen value | Open an anchored list with the shared short fade and small offset; keep one chevron and one selected check | Content appears smoothly without extra choreography |
|
|
73
|
+
| Simplifying event feedback also removes opening and closing animations | Retain minimal content transitions; keep hover, focus, press and row navigation immediate | Content visibility and repeated control feedback have different purposes |
|
|
74
|
+
| A switch fades its track, border and knob colors | Update colors immediately; only the knob moves briefly | Motion explains a saved position |
|
|
75
|
+
| Screens restyle controls to solve local taste problems | Fix shared roles and components, then test the contract | Products stay consistent |
|
|
76
|
+
|
|
77
|
+
## Boundaries
|
|
78
|
+
|
|
79
|
+
Keep Memos' warm Light canvas, bright surfaces, ink emphasis, text contrast, system fonts and existing size rows. Base UI owns behavior. These remain our tokens and APIs; adopting this reference does not require another styling framework or Linear's branding.
|
|
80
|
+
|
|
81
|
+
State feedback is immediate. Motion is reserved for spatial changes, appearing or disappearing content, and progress. See [States](states.md), [Motion](motion.md) and [Surfaces and boundaries](depth.md) for the executable rules.
|
|
82
|
+
|
|
47
83
|
<!-- docs/principles.md -->
|
|
48
84
|
|
|
49
85
|
# Principles and voice
|
|
50
86
|
|
|
51
87
|
## Principles
|
|
52
88
|
|
|
53
|
-
1. **
|
|
54
|
-
2. **
|
|
55
|
-
3. **
|
|
56
|
-
4. **
|
|
57
|
-
5. **
|
|
58
|
-
6. **
|
|
89
|
+
1. **Work first.** Content and editing get attention; supporting navigation and controls recede. Light uses a pale warm-gray canvas with bright surfaces. A control should not become more prominent merely because the pointer crosses it.
|
|
90
|
+
2. **Hierarchy before decoration.** Use alignment, spacing, text contrast and surface lightness before adding boundaries or icons. Ink emphasizes the primary action and saved on states. Brand hues do not fill controls.
|
|
91
|
+
3. **One shared vocabulary.** Components and public classes carry the same roles across products. Solve visual problems in that contract instead of accumulating local overrides. Our [Linear reference](design-reference.md) explains the evidence and the choices we make from it.
|
|
92
|
+
4. **Complete behavior, few visual treatments.** An interaction event is not a visual variant. Text entry stays stable through hover, focus and press; the caret or selection shows typing focus. Actions reuse one highlight, persistent choices retain a selected fill or mark, and errors, unavailable controls and waiting remain understandable.
|
|
93
|
+
5. **Immediate feedback, minimal opening and closing.** Hover, press, focus and frequent selection update immediately. Tabs do not animate every navigation step. Keep short shared transitions when popups, dialogs and panels appear or disappear. Motion explains content visibility, useful position changes or progress. No press movement, scaling, brightness filters or decorative event fades. See [Motion](motion.md) for the shared timings and reduced-motion rules.
|
|
94
|
+
6. **Dense and predictable.** Use the documented size rows, type scale, spacing ladder and icon system: controls default to 32px, on a 4px spacing scale. Components stay generic; products compose their screens from them and the shared patterns.
|
|
59
95
|
|
|
60
96
|
## Voice and interface text
|
|
61
97
|
|
|
@@ -65,14 +101,14 @@ It is not a general-purpose UI library for outside developers. Components are ge
|
|
|
65
101
|
- Destructive confirmations name the object and the outcome: "Delete this note?" and "This can't be undone."
|
|
66
102
|
- Name an action with its verb, never "OK".
|
|
67
103
|
- Errors say how to fix the problem, not only what went wrong.
|
|
68
|
-
- No emoji in
|
|
104
|
+
- No decorative emoji in interface labels. User content and reaction choices may contain emoji; compose those as content, not Lucide icons. Numbers use digits ("3 notes").
|
|
69
105
|
|
|
70
106
|
## What to avoid
|
|
71
107
|
|
|
72
108
|
- A brand hue on controls, or blue anywhere at rest except links.
|
|
73
109
|
- Cards and boxes around content. Group with spacing, a heading and a `Separator`. The exceptions are rows of settings or records: a [List](components/list.md), or a [Table](components/table.md) with `bordered`.
|
|
74
110
|
- Textures, glass or blur behind text, and neumorphism.
|
|
75
|
-
- Pills for buttons or inputs.
|
|
111
|
+
- Pills as the default for buttons or inputs. Use the opt-in Button/Toggle `shape="pill"` for compact reaction controls and similar content choices; ordinary actions keep the size-row corners.
|
|
76
112
|
- Color as the only signal. Status colors always come with a word or an icon.
|
|
77
113
|
- Illustrations in empty states.
|
|
78
114
|
|
|
@@ -89,10 +125,11 @@ Memos UI is ink on paper: a light warm-gray canvas, bright surfaces, `primary` i
|
|
|
89
125
|
| Surfaces | `canvas`, `surface`, `overlay` | The page; then inputs, menus, dialogs and toasts one step up. `overlay` dims the page behind dialogs |
|
|
90
126
|
| Wells | `well` | Switch tracks, segmented groups, unchecked boxes and radios, pressed toggles, inline code |
|
|
91
127
|
| Hover | `hover` | Rows, ghost buttons, the highlighted menu row |
|
|
128
|
+
| Saved choices | `selected` | Persistent fill on soft toggles, distinct from hover |
|
|
92
129
|
| Keys | `key`, `key-hover` | Neutral buttons, select triggers, the selected tab, kbd |
|
|
93
130
|
| Text | `text`, `text-secondary`, `text-disabled` | Primary, secondary and disabled text. Each keeps 4.5:1 on the backgrounds named in the token table below |
|
|
94
131
|
| Emphasis | `primary`, `primary-hover`, `on-primary` | Primary button, checked checkbox and radio, switch when on, tooltip |
|
|
95
|
-
| Meta | `accent`, `selection`, `focus`, `focus-edge` | Links and text selection (`accent`); the keyboard focus edge and the
|
|
132
|
+
| Meta | `accent`, `selection`, `focus`, `focus-edge` | Links and text selection (`accent`); the control keyboard focus edge and the forced-colors field focus edge (`focus`, `focus-edge`). Never a fill |
|
|
96
133
|
| Status | `success`, `warning`, `danger` and their `-soft` pairs; `danger-key`, `on-danger` | Toasts, badges, field errors, danger buttons |
|
|
97
134
|
| Lines | `edge`, `control-edge`, `line` | Quiet resting borders on fields, neutral buttons, switch tracks and surfaces; small checkbox/radio markers; separators and table rules |
|
|
98
135
|
|
|
@@ -100,7 +137,7 @@ Memos UI is ink on paper: a light warm-gray canvas, bright surfaces, `primary` i
|
|
|
100
137
|
|
|
101
138
|
- Light uses near-neutral warm grays for its canvas, wells, hover fills and quiet boundaries: one OKLCH hue (95), with chroma at most 0.008. Surfaces and keys stay white. Text, ink emphasis and stronger control boundaries stay pure gray; Dark keeps its pure-gray neutrals.
|
|
102
139
|
- `accent` is a quiet blue used only for links and selection. If a screen has blue on it at rest, apart from links, something is wrong.
|
|
103
|
-
-
|
|
140
|
+
- Text entry uses its visible caret or text selection to indicate focus. Hover, focus and press keep the same field edge and surface in normal colors. Other controls reuse the existing 1px ink `focus` border. On primary and checked fills, the border uses `on-primary`; danger buttons use `on-danger`. Only forced-colors mode uses a 2px system focus edge. See [States](states.md).
|
|
104
141
|
- Status colors appear only when there is a status. Red is for destructive actions and errors, never for emphasis.
|
|
105
142
|
- `danger` is tuned for text. `danger-key` is the danger button's fill; it is darker in Dark so the button is not the loudest thing on screen.
|
|
106
143
|
- Always pair a status color with a word or an icon.
|
|
@@ -115,7 +152,7 @@ There are two themes: **Light** (the default) and **Dark**.
|
|
|
115
152
|
- Each theme sets every color token explicitly. The package's tests check the values against the table on this page and check the contrast promised here.
|
|
116
153
|
- Light starts with a pale warm-gray paper canvas. White surfaces and keys remain brighter; wells and hover fills are darker steps in the same warm-gray family. This is a flat color treatment, with no texture.
|
|
117
154
|
- Dark keeps the same flat surface roles. Wells and keys are distinct gray fills, and `primary` flips to near-white with a dark `on-primary`.
|
|
118
|
-
- `edge` separates bounded surfaces and stays quiet at rest, on hover and under selected/pressed attributes. Text fields keep it
|
|
155
|
+
- `edge` separates bounded surfaces and stays quiet at rest, on hover and under selected/pressed attributes. Text fields keep it during typing focus too; neutral buttons, Selects and off switch tracks do not strengthen it on hover. `control-edge` is reserved for small unchecked Checkbox, Radio and Prose task marks, at least 3:1 against `canvas` and `surface` in both themes. On default toggles use a `well` fill with `text`, without a visible border; soft toggles use `hover` when off and `selected` with `text` when on; segmented choices use a contrasting `key` fill in a quiet well. Text tokens never become persistent borders. Non-text controls retain their focus treatment; invalid fields retain their error edge. Forced colors adds system-colored selection boundaries when fills disappear; `line` separates content. The quiet resting field boundary remains below 3:1; see [boundary contrast](depth.md#rules).
|
|
119
156
|
|
|
120
157
|
### Choosing a theme
|
|
121
158
|
|
|
@@ -127,7 +164,7 @@ The package does not read `prefers-color-scheme` itself. The app does, so that a
|
|
|
127
164
|
|
|
128
165
|
An app sets only these tokens:
|
|
129
166
|
|
|
130
|
-
- the
|
|
167
|
+
- the 29 color tokens in the [table below](#color-tokens),
|
|
131
168
|
- the three font tokens in [Typography](typography.md#fonts): `font-sans`, `font-display` and `font-mono`.
|
|
132
169
|
|
|
133
170
|
Every other token (space, radius, sizes, layout widths, layers, motion) and every `mu-*` class rule is internal. Read those tokens in app CSS (`var(--mu-space-4)`), but don't override them, even though the cascade layer lets an app rule win: the products would drift apart, and the package's tests and screenshots cover only its own values. To change one element, pass `className` or `style` to that component (see [API conventions](building.md#api-conventions)).
|
|
@@ -148,7 +185,7 @@ An app can offer more themes than the two the package ships, such as a warm Pape
|
|
|
148
185
|
```
|
|
149
186
|
|
|
150
187
|
- Set every color token in the table below. A token the theme leaves out keeps its Light value, so a partial theme mixes two palettes.
|
|
151
|
-
- The package exports `themeTokens`, the custom property names of those
|
|
188
|
+
- The package exports `themeTokens`, the custom property names of those 29 tokens (`--mu-canvas` to `--mu-on-danger`). Check in the app's tests that its theme sets each one (Vitest reads a stylesheet with `?raw` only when `test.css` is `true` in its config; without it the file comes back empty):
|
|
152
189
|
|
|
153
190
|
```ts
|
|
154
191
|
import { themeTokens } from "@usememos/ui";
|
|
@@ -158,7 +195,7 @@ An app can offer more themes than the two the package ships, such as a warm Pape
|
|
|
158
195
|
expect(themeTokens.filter((name) => !paper.includes(`${name}:`))).toEqual([]);
|
|
159
196
|
});
|
|
160
197
|
```
|
|
161
|
-
- Keep the rules on this page: `primary` stays ink, not a brand hue; `accent` is only for links and selection; focus
|
|
198
|
+
- Keep the rules on this page: `primary` stays ink, not a brand hue; `accent` is only for links and selection; control focus stays ink; text-entry focus does not change its normal border; status colors only for status.
|
|
162
199
|
- The app owns the theme's contrast. Check in the app's own tests that every text and background pair named in the Usage column below keeps 4.5:1 or more. The package's tests and screenshots cover only Light and Dark.
|
|
163
200
|
- The tokens sit in the `mu` [cascade layer](building.md#cascade-layer), so an unlayered app theme wins over them at any specificity.
|
|
164
201
|
- An app theme is a choice the user makes. It is not a way to restyle the package per product: an app's default theme is Light or Dark.
|
|
@@ -171,22 +208,23 @@ An app can offer more themes than the two the package ships, such as a warm Pape
|
|
|
171
208
|
| `surface` | `#ffffff` | `#1a1a1a` | One lightness step above canvas: inputs, menus, popovers, dialogs, toasts, table headers |
|
|
172
209
|
| `well` | `oklch(0.934 0.008 95)` | `#252525` | Flat parts that hold a state: switch, tab, checkbox and radio wells, pressed toggles, inline code. A boundary is added only where the component needs one. Not for enabled text entry |
|
|
173
210
|
| `hover` | `oklch(0.95 0.006 95)` | `#252525` | Hover on rows, ghost buttons and wells; the highlighted row in menus and selects |
|
|
211
|
+
| `selected` | `oklch(0.919 0.004 95)` | `#343434` | Persistent soft-toggle fill, including while hovered; with `text` or `text-disabled`, never a selected border |
|
|
174
212
|
| `key` | `#ffffff` | `#343434` | Controls and selected parts: neutral buttons, select triggers, the selected tab, kbd. Flat: neutral buttons and selects use `edge`; selected segments use fill without a visible border in normal colors |
|
|
175
213
|
| `key-hover` | `oklch(0.982 0.004 95)` | `#3c3c3c` | Hover on a neutral key |
|
|
176
214
|
| `edge` | `oklch(0.858 0.006 95)` | `#343434` | Quiet resting borders of fields, neutral buttons, switch tracks, well/key surfaces, popups, badges, selects and disabled controls |
|
|
177
215
|
| `control-edge` | `#909090` | `#777777` | Small unchecked Checkbox, Radio and Prose task markers only; never a full-control hover or selected boundary; at least 3:1 on `canvas` and `surface` |
|
|
178
216
|
| `line` | `oklch(0.919 0.004 95)` | `#252525` | Separators, table rules and menu dividers. About 1.2:1 against canvas in Light, so structural lines also need spacing or a heading |
|
|
179
217
|
| `overlay` | `rgba(0,0,0,0.32)` | `rgba(0,0,0,0.6)` | Backdrop behind dialogs and sheets |
|
|
180
|
-
| `text` | `#1a1a1a` | `#ededed` | Primary text and icons on canvas, surface, well and
|
|
218
|
+
| `text` | `#1a1a1a` | `#ededed` | Primary text and icons on canvas, surface, well, key and selected (10:1 or more in both themes) |
|
|
181
219
|
| `text-secondary` | `#5e5e5e` | `#adadad` | Secondary text: descriptions, placeholders, unselected tabs and toggles, shortcuts, timestamps (4.9:1 or more on every surface) |
|
|
182
|
-
| `text-disabled` | `#666666` | `#9e9e9e` | Disabled text and icons, at least 4.5:1 on `canvas`, `surface`, `well` and `
|
|
220
|
+
| `text-disabled` | `#666666` | `#9e9e9e` | Disabled text and icons, at least 4.5:1 on `canvas`, `surface`, `well`, `key` and `selected`; never fade the whole control |
|
|
183
221
|
| `primary` | `#1a1a1a` | `#e6e6e6` | The one emphasized action and every on state: primary buttons, checked boxes and radios, switches when on, tooltips. Ink, not a hue |
|
|
184
222
|
| `primary-hover` | `#333333` | `#ffffff` | Hover on a primary key |
|
|
185
223
|
| `on-primary` | `#ffffff` | `#0f0f0f` | Text, icons and the switch knob on `primary` (16:1 Light, 15:1 Dark) |
|
|
186
224
|
| `accent` | `oklch(0.5 0.13 255)` | `oklch(0.78 0.11 255)` | The only hue at rest: links, the text cursor and selection. 5.7:1 on canvas in Light. Never a fill |
|
|
187
225
|
| `selection` | `rgba(40,99,171,0.18)` | `rgba(135,186,253,0.28)` | Text selection and the selected range in an editor |
|
|
188
226
|
| `focus` | `var(--mu-primary)` | `var(--mu-primary)` | 1px keyboard focus border, on `:focus-visible` only; contrasting text color on filled controls |
|
|
189
|
-
| `focus-edge` | `var(--mu-
|
|
227
|
+
| `focus-edge` | `var(--mu-edge)` | `var(--mu-edge)` | Field focus edge in forced colors, where it becomes system `Highlight`. Normal Input, Textarea and Combobox keep `edge` and show typing focus with the caret or selection |
|
|
190
228
|
| `success` | `oklch(0.52 0.13 150)` | `oklch(0.78 0.14 150)` | Success text and icons on `surface` or `success-soft` (4.6:1 or more). Always with a word or icon |
|
|
191
229
|
| `success-soft` | `#e9f4ec` | `#14261a` | Background of success badges and toasts |
|
|
192
230
|
| `warning` | `oklch(0.5 0.11 70)` | `oklch(0.8 0.13 75)` | Warning text and icons on `surface` or `warning-soft` (5.5:1 or more) |
|
|
@@ -219,11 +257,11 @@ Use these classes before writing new surface styles. `mu-key` covers both a cont
|
|
|
219
257
|
|
|
220
258
|
- No `box-shadow`, `text-shadow`, sheen gradients, focus rings or visible `outline`. `outline: none` or `0` only suppresses a browser outline when another visible focus treatment exists.
|
|
221
259
|
- Text entry keeps its `surface` fill on hover and focus. Enabled fields never use a gray well; disabled fields use `well` with `text-disabled` text.
|
|
222
|
-
- Boundary strength follows its job. Resting, hovered and persistently selected surfaces never borrow text or focus colors for their borders. Text entry keeps its quiet 1px `edge`
|
|
260
|
+
- Boundary strength follows its job. Resting, hovered and persistently selected surfaces never borrow text or focus colors for their borders. Text entry keeps its quiet 1px `edge` under hover, focus and press. A resting text field does not need the same visual weight as a checkbox or switch's state marker. The quiet field boundary is below 3:1 in the built-in themes; this treatment does not guarantee WCAG 1.4.11 for text entry.
|
|
223
261
|
- A checked checkbox or radio uses `primary` with an `on-primary` mark. A switch also moves its knob.
|
|
224
262
|
- Neutral buttons, Select triggers and switch tracks keep the quiet `edge` on hover. Button and Select fills provide hover feedback; fields and switches keep their fill. A switch keeps its contrasting `text` knob when off. Checkbox, Radio and Prose task checkboxes alone use `control-edge` around their small unchecked marks. Do not reuse that marker color around a toolbar item or a full control.
|
|
225
|
-
- A single choice among peers uses a contrasting `key` fill in a quiet well, with `text` for the selected label and icon. The group and selected key have no visible border in normal colors. Hover only increases label and icon contrast; focus reuses `mu-focus`. Borders reserve space, so geometry stays fixed. In forced colors the selected key gains a text-colored border. An independent on/off toggle uses `well` and `text` when on, with a transparent reserved border. Disabled on toggles keep that fill and use `text-disabled`. In forced colors an on toggle gets a system-colored boundary because its fill may disappear.
|
|
226
|
-
- Surface classes describe a surface, not an interaction state. `mu-key` keeps its `key` fill and quiet `edge` even with selected or pressed attributes. Use Toggle for a persistent on/off fill, or a key inside a well for one selected choice. Never turn a text token into a persistent border.
|
|
263
|
+
- A single choice among peers uses a contrasting `key` fill in a quiet well, with `text` for the selected label and icon. The group and selected key have no visible border in normal colors. Hover only increases label and icon contrast; focus reuses `mu-focus`. Borders reserve space, so geometry stays fixed. In forced colors the selected key gains a text-colored border. An independent on/off toggle uses `well` and `text` when on (`selected` and `text` for `variant="soft"`), with a transparent reserved border. Disabled on toggles keep that fill and use `text-disabled`. In forced colors an on toggle gets a system-colored boundary because its fill may disappear.
|
|
264
|
+
- Surface classes describe a surface, not an interaction state. `mu-key` keeps its `key` fill and quiet `edge` even with selected or pressed attributes. Use Toggle for a persistent on/off fill, or a key inside a well for one selected choice. Never turn a text token into a persistent border. Non-text focus and invalid fields are the explicit exceptions to quiet control boundaries.
|
|
227
265
|
- Floating surfaces have a border so they remain distinct over the same surface color. Modal backdrops still use `overlay`.
|
|
228
266
|
- Content is grouped by spacing, headings and separators. The boxed exceptions are List, bordered Table and bordered Form.
|
|
229
267
|
- Borders count toward the documented padding. Reserve their space and compensate inner padding so states never move text or change a control's size.
|
|
@@ -232,7 +270,7 @@ Use these classes before writing new surface styles. `mu-key` covers both a cont
|
|
|
232
270
|
|
|
233
271
|
- `mu-focus` recolors the existing 1px border on `:focus-visible`. The fine edge keeps compact controls calm while contrasting with the fill. It provides the internal `--mu-control-border-width` variable. A custom control reads that variable for its border and subtracts it from its padding, so its text and size stay fixed. Text-only links use an underline instead.
|
|
234
272
|
- The edge uses `focus` ink. On primary and checked fills it uses `on-primary`; danger buttons use `on-danger`, so the edge contrasts with the fill.
|
|
235
|
-
-
|
|
273
|
+
- Input, Textarea and Combobox input boxes keep their border, surface and geometry under hover, focus and press. The visible caret or text selection indicates typing focus; do not recolor the box or animate it. Invalid fields keep `danger`. In forced colors, focused fields use the shared 2px system `focus-edge`. The caret/selection approach follows the text-field examples in [WCAG Focus Visible](https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html).
|
|
236
274
|
- Invalid non-text controls reuse the shared 1px focus edge. Their error message continues to identify the error; do not add a thicker focus variant for validation.
|
|
237
275
|
- Text links use a double underline on keyboard focus, including links already underlined at rest.
|
|
238
276
|
- Menu items and options use their highlighted fill. Opening a popup never draws a focus boundary around the popup.
|
|
@@ -329,7 +367,7 @@ The table gives desktop sizes. Touch targets reserve additional space as describ
|
|
|
329
367
|
- Use `sm` for dense toolbars, table rows and filters inside panels, and `md` everywhere else, including marketing pages, onboarding and sheets. A sidebar with 28px rows puts the `mu-compact` class on itself: md controls and rows inside are `control-compact` (28px) tall, with md text, icons, padding and radius (see [A sidebar navigation row](usage.md#a-sidebar-navigation-row)). It is the only height between sizes, and it is not a `size` value. Floating lists (Menu, Select and Combobox lists) use it for their sm rows too: an sm list has 28px rows with sm text and icons, because a 24px row is cramped inside a 12px-radius popup. Emphasis comes from the `primary` variant, not from a bigger control. On touch screens buttons, fields and option rows grow to at least 44px; checkboxes, radios and switches keep their small marks and reserve 44px of layout space around them.
|
|
330
368
|
- Use `xs` for secondary actions beside 13px text: actions on a row or an entry, inline formatting, filters in a tight header, checkboxes in a dense list. It is the height of a Badge or a Kbd, so they line up. Don't use `xs` for forms, for navigation, or for the main action of a screen.
|
|
331
369
|
- Only Button, Toggle, ToggleGroup, Checkbox, RadioGroup and Radio, Switch, Avatar, Spinner and Skeleton take `xs`. Text entry, menus, tabs and tables are too cramped at 20px, so their `size` is `sm | md`, and an `xs` inherited from a parent renders them at `sm`.
|
|
332
|
-
- The icon side of a control has 2px less padding, so text and icon look centered.
|
|
370
|
+
- The icon side of a control has 2px less padding, so text and icon look centered. Button and Toggle with `shape="pill"` instead keep equal size-row padding at both ends and use `radius-full`; an icon-only control is a circle. Heights, icon sizes and touch targets stay the same.
|
|
333
371
|
- On touch screens (`pointer: coarse`), every control reserves at least `hit-area` (44px) in the layout. Buttons and option rows grow; checkboxes, radios and switches reserve margins around their visual marks. Adjacent hit areas must never overlap. A segmented group adds its 2px inset around 44px items.
|
|
334
372
|
|
|
335
373
|
## Spacing
|
|
@@ -477,52 +515,62 @@ Use these tokens for every `z-index`. Don't invent new layers in product code.
|
|
|
477
515
|
|
|
478
516
|
# Motion
|
|
479
517
|
|
|
480
|
-
|
|
518
|
+
Keep opening and closing animations. They explain when content appears, where it belongs and when it leaves. Hover, press, focus and frequent selection feedback remain immediate. Controls stay still under the pointer.
|
|
519
|
+
|
|
520
|
+
Our [Linear reference](design-reference.md) informs this restraint. The timings below are Memos UI decisions, not Linear's published animation specification.
|
|
481
521
|
|
|
482
522
|
## Rules
|
|
483
523
|
|
|
484
|
-
- **
|
|
485
|
-
|
|
486
|
-
- **
|
|
487
|
-
- **
|
|
488
|
-
- **
|
|
524
|
+
- **Text entry.** Input, Textarea and Combobox input boxes have no hover, focus or press animation. Typing focus uses the caret or selection without a border change. Validation recolors the existing edge immediately; the surface and geometry stay fixed. Do not add a treatment just because an event occurs.
|
|
525
|
+
|
|
526
|
+
- **Frequent feedback.** Hover, press, keyboard focus and label changes have no color or border animation. Actions reuse one highlight rather than adding a held-state edge. `duration-press` is 0ms for these shared treatments. No press translation, scaling or brightness filters.
|
|
527
|
+
- **Selection.** Tabs and single-choice ToggleGroups update their selected key immediately, including its position and size. Checkbox and Radio marks also update immediately. Only the Switch knob uses a short positional slide over `duration-settle` (150ms); track, border and knob colors change immediately.
|
|
528
|
+
- **One popup treatment.** Select, Menu, Combobox, Popover and Tooltip share a short fade and `space-1` (4px) vertical offset over `duration-fade` (100ms), using `ease-out`. Opening settles to full opacity and no offset; closing returns to the same hidden frame. No scale, bounce, blur, stagger or animated trigger chevron. Select opens below the trigger by default; it has no separate motion variant.
|
|
529
|
+
- **Dialogs and drawers.** Dialogs use the same fade and `space-1` offset over `duration-settle` (150ms). A Dialog sheet uses `space-6` (24px) from the bottom. Drawers slide from their attached edge over `duration-settle`, using `ease-settle`, and follow the finger during a swipe. Their backdrops fade with the same duration; no delayed or staged entrance.
|
|
530
|
+
- **Tooltips** wait 400ms on hover before opening; keyboard focus has no opening delay. This delay is separate from the short opening transition. Moving between tooltips in a group skips the delay and replayed entrance.
|
|
531
|
+
- **Collapsible content** uses the shared 100ms fade and 4px offset for opening and closing. Its height never animates. A chevron may change orientation immediately to identify the open state; it does not rotate through a transition.
|
|
532
|
+
- **Toasts** retain their short entrance, exit and stack behavior over `duration-settle`. Do not add bounce, stagger or an extra entrance for their content. See [Toast](components/toast.md) for stack and dismissal behavior.
|
|
533
|
+
- **Keep behavior immediate.** Focus management, keyboard navigation, selection and dismissal do not wait for an animation. Opening by keyboard uses the same short content transition; moving focus or the highlighted row inside the open widget never animates. Rapid open/close changes retarget from the current frame instead of queuing or replaying an entrance. Opening and closing share the same short timing; never add a longer exit.
|
|
534
|
+
- **Implementation.** Use the existing CSS transitions and Base UI lifecycle attributes. Animate only opacity and translation for opening and closing; keep size, padding, borders and colors fixed. Do not add an animation library or animate content that is already present on initial render.
|
|
489
535
|
- **Spinners** turn once every 0.8s. **Skeletons** shimmer once every 1.4s.
|
|
490
|
-
- **Reduced motion.** Under `prefers-reduced-motion`,
|
|
536
|
+
- **Reduced motion.** Under `prefers-reduced-motion: reduce`, opening, closing and Switch position changes are immediate: no translation or opacity transition, including backdrops. Remove shimmer and continuous decorative motion. Keep the visible loading state and all interaction behavior.
|
|
491
537
|
|
|
492
538
|
## Tokens
|
|
493
539
|
|
|
494
540
|
| Token | Value | Usage |
|
|
495
541
|
| --- | --- | --- |
|
|
496
|
-
| `duration-press` |
|
|
497
|
-
| `duration-settle` | 150ms |
|
|
498
|
-
| `duration-fade` | 100ms |
|
|
542
|
+
| `duration-press` | 0ms | Immediate shared hover, press and focus feedback |
|
|
543
|
+
| `duration-settle` | 150ms | Switch knob position; Dialog and Drawer opening and closing |
|
|
544
|
+
| `duration-fade` | 100ms | Shared popup and Collapsible opening and closing |
|
|
499
545
|
| `duration-spin` | 800ms | One turn of a Spinner |
|
|
500
546
|
| `duration-shimmer` | 1400ms | One pass of a Skeleton's shimmer |
|
|
501
|
-
| `ease-settle` | `cubic-bezier(0.2, 0.9, 0.3, 1)` |
|
|
502
|
-
| `ease-out` | `cubic-bezier(0.2, 0, 0, 1)` |
|
|
547
|
+
| `ease-settle` | `cubic-bezier(0.2, 0.9, 0.3, 1)` | Switch and Drawer position changes; no bounce |
|
|
548
|
+
| `ease-out` | `cubic-bezier(0.2, 0, 0, 1)` | Popup and Dialog opening and closing; backdrop fades |
|
|
503
549
|
|
|
504
550
|
<!-- docs/states.md -->
|
|
505
551
|
|
|
506
552
|
# States
|
|
507
553
|
|
|
508
|
-
|
|
554
|
+
Behavioral states are complete, but they do not each require a different appearance. Reuse the smallest treatment that communicates the action or saved state. See the [Linear reference](design-reference.md) for the basis of this contract.
|
|
509
555
|
|
|
510
556
|
| State | Looks like |
|
|
511
557
|
| --- | --- |
|
|
512
558
|
| Hover | Interactive keys go to `key-hover`; actionable rows and ghost buttons use `hover`. Passive content and ordinary table rows do not highlight. Bounded controls keep their quiet `edge`; fields and switches keep their fill. Invalid fields keep `danger`. Muted labels go to `text`. Primary keys go to `primary-hover`. |
|
|
513
|
-
| Pressed (while held) |
|
|
514
|
-
| Pressed toggle (on) |
|
|
515
|
-
| Selected (one of several) | A flat `key` fill in a quiet well that
|
|
559
|
+
| Pressed (while held) | Actions reuse their hover highlight immediately: neutral uses `key-hover`, ghost and actionable rows use `hover`, primary uses `primary-hover`, danger retains `danger-key`. No extra border, movement, scale or filter. Toggles do not imitate a selected state before activation |
|
|
560
|
+
| Pressed toggle (on) | Default: `well` and `text`. Soft: `selected` and `text`, retained on hover; the reserved border stays transparent |
|
|
561
|
+
| Selected (one of several) | A flat `key` fill in a quiet well that updates immediately to the chosen item, with `text` for its label and icon; no visible group or selected-key border in normal colors. Hover only changes label and icon contrast; focus reuses `mu-focus` |
|
|
516
562
|
| Checked | A flat `primary` fill with an `on-primary` mark |
|
|
517
563
|
| Loading | The control becomes inert at once. After 300ms it hides its label and shows a Spinner without changing size; short waits keep the label |
|
|
518
564
|
| Disabled | `text-disabled` text, quiet borders and `well` on all Button variants and other filled controls; no whole-control opacity, hover or press response |
|
|
519
565
|
| Invalid | A `danger` edge on the field, plus an error line with an icon under it |
|
|
520
|
-
| Focus (field) | The
|
|
566
|
+
| Focus (field) | The caret or text selection identifies typing focus. Border and surface stay the same under mouse and keyboard focus, with no ring or animation. Invalid fields keep their danger edge; forced colors uses a 2px system edge |
|
|
521
567
|
| Focus (everything else) | Reuse the shared 1px border on `:focus-visible`; primary/checked fills use `on-primary`, danger buttons use `on-danger`; text links get a double underline. Invalid non-text controls reuse the same focus edge; their error message keeps communicating the error |
|
|
522
568
|
| Focus (inside a widget) | Menu items and options take the `hover` fill. No ring |
|
|
523
569
|
|
|
524
570
|
## Rules
|
|
525
571
|
|
|
572
|
+
- An event does not need its own visual style. Text entry keeps its border, surface and geometry under hover, focus and press. Validation alone changes the ordinary field edge, immediately and without animation.
|
|
573
|
+
- Hover, press and keyboard focus feedback are immediate. Actionable rows and actions reuse a highlight for pointer hover and touch press; do not invent a second held-state frame. Tabs and single-choice groups update their selected key without a slide.
|
|
526
574
|
- Keep state styling minimal. Reuse the shared component and foundation treatments; add a separate treatment only when it communicates a distinct action or state. A component does not need a new fill, border, scale or animation for every pointer or focus event.
|
|
527
575
|
- Persistent states and hover never strengthen a quiet boundary. Reserve `control-edge` for small checkbox/radio markers; text colors are not border colors. Focus and invalid states keep their explicit boundary treatments.
|
|
528
576
|
- Keep selection persistent and independent of transient events. A selected fill is not a focus border. Reuse `mu-focus` for the shared focus behavior instead of defining component-specific focus variants.
|
|
@@ -530,9 +578,10 @@ Every interactive component handles these states the same way.
|
|
|
530
578
|
- Press feedback does not move or resize a control, apply a brightness filter, or animate its mark. Checkbox and Radio marks change immediately. A Switch's knob still moves to show its saved state.
|
|
531
579
|
- Hover never changes size, weight or layout.
|
|
532
580
|
- An input never changes its background on hover or focus, so it never looks disabled.
|
|
533
|
-
- Control focus borders show for keyboard focus only (`:focus-visible`), never on mouse click.
|
|
534
|
-
- Focus changes the existing edge, never adds a ring or visible outline. Filled controls use their contrasting text color at the edge.
|
|
581
|
+
- Control focus borders show for keyboard focus only (`:focus-visible`), never on mouse click. Text fields use the visible caret or text selection for typing focus and keep their normal boundary.
|
|
582
|
+
- Focus changes the existing edge, never adds a ring or visible outline. Filled controls use their contrasting text color at the edge for keyboard focus, not as a new pointer-held border.
|
|
535
583
|
- Popups, dialogs and panels never show a focus ring when they open and take focus.
|
|
584
|
+
- Opening and closing content retain the minimal shared [Motion](motion.md) transition. This does not animate focus, hover, press or navigation within the content; those respond immediately.
|
|
536
585
|
- Focused borders use 2px in the system `Highlight` color in forced-colors mode. No shadow or outline fallback is needed.
|
|
537
586
|
- In forced-colors mode, segmented selection keeps a system-colored key border and on toggles get a system-colored boundary, distinct from the shared focus treatment.
|
|
538
587
|
- Disabled controls stay in place. Prefer explaining why something is unavailable over hiding it.
|
|
@@ -543,7 +592,7 @@ Every interactive component handles these states the same way.
|
|
|
543
592
|
| Component family | Default feedback |
|
|
544
593
|
| --- | --- |
|
|
545
594
|
| Actions: Button and Link | Shared fill, edge or underline feedback, without movement or filters. All Button variants share the disabled well and edge |
|
|
546
|
-
| Fields: Input, Textarea and Combobox input |
|
|
595
|
+
| Fields: Input, Textarea and Combobox input | Keep the border, surface and geometry fixed through hover, focus and press. The caret or text selection indicates typing focus; validation alone recolors the ordinary edge |
|
|
547
596
|
| Choices: Checkbox, Radio, Switch, Toggle, ToggleGroup and Tabs | Keep the saved state explicit through a mark, knob position or selected fill. Do not resize on press or imitate selection before activation |
|
|
548
597
|
| Actionable rows: Menu, Select, Combobox options and List.ItemButton | Use the shared highlighted fill. Keep Base UI's interaction behavior |
|
|
549
598
|
| Containers: Dialog, Drawer, Popover and Toast | Feedback belongs to their controls, not an extra container hover effect. Keep visibility, dismissal and Toast expansion behavior |
|
|
@@ -660,6 +709,7 @@ The short version. [Using the components](../usage.md) has the full decision gui
|
|
|
660
709
|
| A small form (heading, a few fields, buttons) among Lists on a settings page | Form with `bordered` |
|
|
661
710
|
| A status or tag label | Badge |
|
|
662
711
|
| A clickable tag filter | Toggle |
|
|
712
|
+
| Compact reactions and their picker | Soft pill Toggles, a ghost pill Button and Popover; see the [composition](popover.md#compact-reaction-picker) |
|
|
663
713
|
| Content still loading | Skeleton |
|
|
664
714
|
| A control waiting on a server | the control's `loading` prop |
|
|
665
715
|
|
|
@@ -667,6 +717,7 @@ The short version. [Using the components](../usage.md) has the full decision gui
|
|
|
667
717
|
|
|
668
718
|
```ts
|
|
669
719
|
type Size = "xs" | "sm" | "md"; // xs only where a page says so; other components take "sm" | "md"
|
|
720
|
+
type ControlShape = "default" | "pill"; // Button and Toggle; defaults keep size-row corners
|
|
670
721
|
type Tone = "neutral" | "success" | "warning" | "danger";
|
|
671
722
|
/** A Lucide icon element, such as <Pin /> from lucide-react. Size and stroke come from the component. */
|
|
672
723
|
type IconElement = React.ReactElement;
|
|
@@ -695,13 +746,14 @@ import { Plus } from "lucide-react";
|
|
|
695
746
|
interface ButtonProps extends BaseButton.Props {
|
|
696
747
|
variant?: "primary" | "neutral" | "ghost" | "danger"; // default "neutral"
|
|
697
748
|
size?: Size;
|
|
749
|
+
shape?: "default" | "pill"; // default "default"; icon-only pills are circular
|
|
698
750
|
icon?: IconElement; // before the label
|
|
699
751
|
iconEnd?: IconElement; // after the label: "Show more" with a chevron, "Next" with an arrow
|
|
700
752
|
loading?: boolean;
|
|
701
753
|
loadingLabel?: string; // accessible name while loading, such as "Saving"
|
|
702
754
|
}
|
|
703
755
|
|
|
704
|
-
function buttonClasses(options?: { variant?: ButtonVariant; size?: Size; iconOnly?: boolean }): string;
|
|
756
|
+
function buttonClasses(options?: { variant?: ButtonVariant; size?: Size; shape?: ControlShape; iconOnly?: boolean }): string;
|
|
705
757
|
```
|
|
706
758
|
|
|
707
759
|
Wraps Base UI's [Button](https://base-ui.com/react/components/button): `disabled`, `focusableWhenDisabled`, `render`, `nativeButton` and native button attributes pass through. Renders `<button type="button">`.
|
|
@@ -725,8 +777,9 @@ Wraps Base UI's [Button](https://base-ui.com/react/components/button): `disabled
|
|
|
725
777
|
|
|
726
778
|
- A flat control: 20, 24 or 32px tall. `primary` is an ink key with `on-primary` text; `neutral` a `key` with a quiet `edge` boundary; `ghost` flat `text-secondary` with `hover` on hover; `danger` a `danger-key` with `on-danger` text.
|
|
727
779
|
- A neutral button keeps its quiet `edge` on hover; its `key-hover` fill provides feedback. Keyboard focus changes the 1px edge to ink; filled actions retain their contrasting focus edge.
|
|
728
|
-
-
|
|
729
|
-
-
|
|
780
|
+
- `shape="pill"` uses `radius-full` and equal size-row horizontal padding, even with an icon beside a label. Default shape and padding stay unchanged.
|
|
781
|
+
- Only an icon: square (circular with `shape="pill"`). With the default shape, the side with an icon (`icon` at the start, `iconEnd` at the end) has 2px less padding.
|
|
782
|
+
- Hover and press reuse one immediate highlight: `key-hover` for neutral, `hover` for ghost, `primary-hover` for primary; danger retains `danger-key`. Holding the pointer adds no new boundary. Keyboard focus owns the contrasting edge. No color fade, translation, scale or brightness filter.
|
|
730
783
|
- `loading` immediately sets `aria-busy` and `aria-disabled`, ignores presses and stays focusable. The label stays visible for 300ms; if still loading, a Spinner replaces it without changing width. Finishing cancels the delay and restores the label immediately.
|
|
731
784
|
- Disabled: every variant uses `text-disabled` text, a `well` fill and a quiet `edge`, including ghost buttons. This keeps disabled toolbar actions distinct from enabled ghost actions without dimming the text. Opacity stays 1.
|
|
732
785
|
|
|
@@ -750,6 +803,8 @@ import { Pin } from "lucide-react";
|
|
|
750
803
|
```ts
|
|
751
804
|
interface ToggleProps extends BaseToggle.Props {
|
|
752
805
|
size?: Size;
|
|
806
|
+
variant?: "ghost" | "soft"; // default "ghost", preserving the flat off state
|
|
807
|
+
shape?: "default" | "pill"; // default "default"; icon-only pills are circular
|
|
753
808
|
icon?: IconElement; // before the label; alone, the toggle is square
|
|
754
809
|
}
|
|
755
810
|
```
|
|
@@ -765,8 +820,12 @@ Wraps Base UI's [Toggle](https://base-ui.com/react/components/toggle): `pressed`
|
|
|
765
820
|
|
|
766
821
|
## Style
|
|
767
822
|
|
|
768
|
-
-
|
|
769
|
-
-
|
|
823
|
+
- Default (`variant="ghost"`): off: flat `text-secondary`, with `hover` on hover. On: a `well` fill and the `text` color, with a transparent reserved border; hover keeps that selected fill. Fill and text changes are immediate. Hover never adds a boundary; keyboard focus reuses `mu-focus`.
|
|
824
|
+
- `variant="soft"`: off uses `hover` and `text-secondary`; hover uses `well` and `text`; on uses the distinct `selected` fill and `text`, retained on hover. The reserved border stays transparent; focus still uses `mu-focus`. No application `data-pressed` color rules are needed.
|
|
825
|
+
- `shape="pill"` uses `radius-full` and equal size-row horizontal padding. An icon-only pill is circular. Heights, icon sizes and 44px touch targets stay unchanged.
|
|
826
|
+
- Disabled soft toggles use `text-disabled` and `well` when off; an on toggle keeps `selected`. Neither responds to hover.
|
|
827
|
+
- Disabled default toggles: `text-disabled`; an on Toggle keeps its well and transparent border. In forced colors an on Toggle uses a system-colored boundary so selection survives when fills disappear.
|
|
828
|
+
- For emoji, keep the Lucide-only `icon` slot empty. Compose a span in `children`, hide the emoji from assistive technology when the control has an `aria-label`, and use the documented [compact reaction picker](popover.md#compact-reaction-picker) layout for a square emoji cell. This does not rely on the internal icon-only class.
|
|
770
829
|
- A label wraps when its row runs out of room; an icon-only toggle never shrinks. A ToggleGroup is at most as wide as its container and grows in height to keep long translated labels readable. On touch screens each Toggle reserves at least 44px in each direction.
|
|
771
830
|
|
|
772
831
|
<!-- docs/components/toggle-group.md -->
|
|
@@ -799,7 +858,7 @@ import { Bold, Code, Italic } from "lucide-react";
|
|
|
799
858
|
|
|
800
859
|
| Part | Base UI part | Renders |
|
|
801
860
|
| --- | --- | --- |
|
|
802
|
-
| `ToggleGroup` | `ToggleGroup` | A `<div role="group">`. Takes `size`. Without `multiple`, adds the
|
|
861
|
+
| `ToggleGroup` | `ToggleGroup` | A `<div role="group">`. Takes `size`. Without `multiple`, adds the selected key |
|
|
803
862
|
| `Toggle` | `Toggle` | One item, a `<button aria-pressed>` |
|
|
804
863
|
| `Separator` | `Separator` | A line between clusters, `multiple` groups only |
|
|
805
864
|
|
|
@@ -832,7 +891,7 @@ Base UI's props pass through: `value`, `defaultValue`, `onValueChange`, `multipl
|
|
|
832
891
|
- `multiple`: items 2px apart, using Toggle's `well` fill and `text` when on, with no visible border in normal colors. Keyboard focus stays separate from selection.
|
|
833
892
|
- Inside a segmented key, the label's line box can tighten at xs and sm to leave room for the focus border without changing geometry; its font size stays in the size row. Long labels wrap and increase the group's height.
|
|
834
893
|
- Use the existing `md` size for a labeled layout choice at the top of a settings popover: a 32px group, 28px selected face and 16px icons. Keep `sm` (24px) for dense contexts; do not invent a height between the size rows.
|
|
835
|
-
- Without `multiple`: a quiet `well` and contrasting `key` fill, with no visible group or selected-key border in normal colors. The selected label and icon use `text`; hover only changes label and icon contrast, without adding a fill or moving the key. Focus reuses `mu-focus`; do not add a component-specific focus treatment. In forced colors the selected key retains its text-colored border. The key
|
|
894
|
+
- Without `multiple`: a quiet `well` and contrasting `key` fill, with no visible group or selected-key border in normal colors. The selected label and icon use `text`; hover only changes label and icon contrast, without adding a fill or moving the key. Focus reuses `mu-focus`; do not add a component-specific focus treatment. In forced colors the selected key retains its text-colored border. The key updates immediately and re-measures when an item changes width. Selection and label changes do not animate. On touch screens every item reserves at least 44px in each direction, without overlapping its neighbor.
|
|
836
895
|
|
|
837
896
|
<!-- docs/components/menu.md -->
|
|
838
897
|
|
|
@@ -924,7 +983,7 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on
|
|
|
924
983
|
- A Separator has 6px above and below, the same as the popup padding, and runs edge to edge.
|
|
925
984
|
- On touch screens rows are at least 44px tall; menus remain anchored floating lists, including submenus. The highlighted or keyboard-focused row uses `hover` with the row's radius (`radius-sm` at sm, `radius-md` at md), and so does a row whose menu is open (a SubmenuTrigger, or a `menuClasses().item` row that is a `Menu.Trigger`). Disabled rows and their icons use `text-disabled` without fading the whole row. `danger` rows use `danger`.
|
|
926
985
|
- A ghost trigger keeps its `hover` background while the menu is open.
|
|
927
|
-
-
|
|
986
|
+
- Opening and closing use the shared [Motion](../motion.md) fade and `space-1` offset over `duration-fade`. Both are immediate under reduced motion; item highlighting never animates.
|
|
928
987
|
- A menu below or above its trigger sits 4px from it. A submenu opens beside its trigger row, its own padding (6px) from the row. The row sits the same 6px inside the parent menu, so the submenu's edge touches the parent's edge.
|
|
929
988
|
- A menu that opens to the side of its trigger (`side` left, right, `inline-start` or `inline-end`; a submenu always does), aligned to `start` or `end`, moves along the trigger by its own top padding (6px, read when it is placed). Its first row lines up with the trigger. With no room below, it opens upward instead and its last row lines up with the trigger. A menu that opens above or below, or is centered, does not move.
|
|
930
989
|
|
|
@@ -1149,13 +1208,13 @@ Wraps Base UI's [Input](https://base-ui.com/react/components/input), so inside a
|
|
|
1149
1208
|
- Provide a `Field.Label` or `aria-label`. Never a placeholder alone.
|
|
1150
1209
|
- `prefix` and `suffix` hold an icon, a short word or a small ghost Button.
|
|
1151
1210
|
- `sm` for filters inside panels, `md` everywhere else.
|
|
1152
|
-
- An app's own one-line field, such as a tag input around a native `<input>`, takes `inputClasses()` on its box: focus
|
|
1211
|
+
- An app's own one-line field, such as a tag input around a native `<input>`, takes `inputClasses()` on its box: typing focus keeps the edge unchanged, and `aria-invalid="true"` or `aria-disabled="true"` on the box show those states. See [Class functions](../custom-components.md#class-functions).
|
|
1153
1212
|
|
|
1154
1213
|
## Style
|
|
1155
1214
|
|
|
1156
1215
|
- Enabled fields use flat `surface` with a quiet 1px `edge` border. A gray well is reserved for the disabled state.
|
|
1157
1216
|
- Typed text weight 400; placeholder `text-secondary`. On touch screens typed text is 16px at every size, so iOS Safari does not zoom in on focus; the field keeps its height.
|
|
1158
|
-
- Hover
|
|
1217
|
+
- Hover, focus and press keep the same `edge` border and `surface` fill. The caret or text selection indicates typing focus; there is no focus border change, ring or animation. Invalid fields retain their `danger` edge while focused. Disabled fields use `well`, a quiet `edge` and `text-disabled` text. Forced colors uses a 2px system focus edge. See [boundary contrast](../depth.md#rules) for the resting edge's contrast tradeoff.
|
|
1159
1218
|
- The side with a prefix or suffix has 2px less padding.
|
|
1160
1219
|
|
|
1161
1220
|
<!-- docs/components/textarea.md -->
|
|
@@ -1192,11 +1251,11 @@ Base UI has no textarea part, so this renders Base UI's `Field.Control` as a `<t
|
|
|
1192
1251
|
|
|
1193
1252
|
- Provide a `Field.Label` or `aria-label`.
|
|
1194
1253
|
- Rich content is not a textarea: use the editor (`@usememos/editor`) with [Prose](prose.md).
|
|
1195
|
-
- An app's own multi-line field, such as a content-editable editor, takes `textareaClasses()`: focus
|
|
1254
|
+
- An app's own multi-line field, such as a content-editable editor, takes `textareaClasses()`: the caret or text selection indicates focus without changing the edge, and `aria-invalid="true"` or `aria-disabled="true"` show those states. See [Class functions](../custom-components.md#class-functions).
|
|
1196
1255
|
|
|
1197
1256
|
## Style
|
|
1198
1257
|
|
|
1199
|
-
- Looks like [Input](input.md). Resizes vertically only.
|
|
1258
|
+
- Looks like [Input](input.md), including an unchanged border and surface under mouse and keyboard focus. The caret or text selection identifies the active field; state changes have no animation. Resizes vertically only.
|
|
1200
1259
|
- On touch screens typed text is 16px on a 20px line at every size, like Input, so iOS Safari does not zoom in on focus.
|
|
1201
1260
|
|
|
1202
1261
|
<!-- docs/components/select.md -->
|
|
@@ -1250,7 +1309,7 @@ interface Select.TriggerProps extends BaseSelect.Trigger.Props {
|
|
|
1250
1309
|
variant?: "neutral" | "ghost"; // default "neutral"; ghost: a flat row, for a switcher in a sidebar or toolbar
|
|
1251
1310
|
}
|
|
1252
1311
|
interface Select.PopupProps extends BaseSelect.Popup.Props {
|
|
1253
|
-
alignItemWithTrigger?: boolean; // default
|
|
1312
|
+
alignItemWithTrigger?: boolean; // default false: open below the trigger
|
|
1254
1313
|
side?: Side; align?: Align; // used when alignItemWithTrigger is false
|
|
1255
1314
|
container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
|
|
1256
1315
|
}
|
|
@@ -1271,9 +1330,9 @@ Base UI's props pass through: `items`, `value`, `defaultValue`, `onValueChange`,
|
|
|
1271
1330
|
|
|
1272
1331
|
## Style
|
|
1273
1332
|
|
|
1274
|
-
- Trigger: a neutral key ([Button](button.md) `neutral`), a quiet `edge` at rest and on hover, value left-aligned at weight 400, `
|
|
1333
|
+
- Trigger: a neutral key ([Button](button.md) `neutral`), a quiet `edge` at rest and on hover, value left-aligned at weight 400, a single `ChevronDown` in `text-secondary`. `ghost`: a ghost Button with the `text` color, `hover` on hover and while open. Placeholder in `text-secondary`, also weight 400. Invalid: a `danger` edge at rest and on hover; focus reuses the shared 1px edge while the error message stays visible.
|
|
1275
1334
|
- List: `mu-popup`, 6px padding, at least `menu-width` and the trigger's width, at most `menu-max-width` or the trigger's width when that is wider. Rows like [Menu](menu.md) items: 28px at sm, 32px at md, highlight in the row's radius; the highlighted row uses `hover`.
|
|
1276
|
-
-
|
|
1335
|
+
- Opens below the trigger, aligned to its start edge; collision handling can flip it when space is limited. `alignItemWithTrigger={true}` opts into value-aligned positioning. Opening and closing use the shared [Motion](../motion.md) treatment: a 100ms fade and `space-1` offset, with no scale or chevron rotation. Under reduced motion both are immediate. The chosen value uses one check; row highlighting identifies the current pointer or keyboard target and updates without animation.
|
|
1277
1336
|
|
|
1278
1337
|
<!-- docs/components/combobox.md -->
|
|
1279
1338
|
|
|
@@ -1375,12 +1434,12 @@ Base UI's props pass through: `items`, `value`, `defaultValue`, `onValueChange`,
|
|
|
1375
1434
|
|
|
1376
1435
|
## Style
|
|
1377
1436
|
|
|
1378
|
-
- The box is an [Input](input.md): flat `surface`, `edge`,
|
|
1437
|
+
- The box is an [Input](input.md): flat `surface`, `edge`, with no border or surface change under hover, focus or press. The caret or text selection indicates typing focus, without animation. The search icon and chevron are `text-secondary`; the chevron turns `text` on hover. Disabled text and both icons use `text-disabled`. The chevron has a built-in Tooltip using `triggerLabel`.
|
|
1379
1438
|
- List: `mu-popup`, 6px padding, as wide as the input and at least `menu-width`. Rows like [Menu](menu.md) items: 28px at sm, 32px at md, highlight in the row's radius. The list is at most `list-height` (240px) tall, then it scrolls.
|
|
1380
1439
|
- Rows are text weight 400; the highlighted row uses `hover`; the chosen row has a check at the end.
|
|
1381
1440
|
- Empty text: `body` in `text-secondary`, `space-3` by `space-4` padding.
|
|
1382
1441
|
- Grid cells: square, a control height, `inset` apart, `row-radius`; the highlighted cell uses `hover`, the chosen cell sits in a `well` like a pressed Toggle. No check.
|
|
1383
|
-
-
|
|
1442
|
+
- Opening and closing use the shared [Motion](../motion.md) fade and `space-1` offset over `duration-fade`. Both are immediate under reduced motion; input feedback and option highlighting never animate.
|
|
1384
1443
|
|
|
1385
1444
|
<!-- docs/components/checkbox.md -->
|
|
1386
1445
|
|
|
@@ -1458,7 +1517,7 @@ Wraps Base UI's [Switch](https://base-ui.com/react/components/switch) root with
|
|
|
1458
1517
|
|
|
1459
1518
|
- A pill well with a round knob and a quiet `edge` at rest; the well fills with `primary` when on. The track keeps its quiet boundary and fill on hover. Keyboard focus uses a fine 1px contrasting boundary.
|
|
1460
1519
|
- Height = checkbox box + 2px; width = height × 1.7.
|
|
1461
|
-
- The knob is flat `text`, inset 2px; `on-primary` when on.
|
|
1520
|
+
- The knob is flat `text`, inset 2px; `on-primary` when on. Only its position slides with `ease-settle` when the state changes, without growing on press; all colors change immediately. Under reduced motion it has no transition. Disabled switches keep the knob position, but use a quiet `edge` and a `text-disabled` knob. On touch screens margins reserve a 44px target without enlarging the mark.
|
|
1462
1521
|
|
|
1463
1522
|
<!-- docs/components/radio-group.md -->
|
|
1464
1523
|
|
|
@@ -1544,7 +1603,7 @@ import { Bell, User } from "lucide-react";
|
|
|
1544
1603
|
| Part | Base UI part | Renders |
|
|
1545
1604
|
| --- | --- | --- |
|
|
1546
1605
|
| `Tabs.Root` | `Tabs.Root` | A `<div>` around the list and panels. Takes `size` |
|
|
1547
|
-
| `Tabs.List` | `Tabs.List` with `Tabs.Indicator` | The well, a `<div role="tablist">`, with the
|
|
1606
|
+
| `Tabs.List` | `Tabs.List` with `Tabs.Indicator` | The well, a `<div role="tablist">`, with the selected key built in |
|
|
1548
1607
|
| `Tabs.Tab` | `Tabs.Tab` | One tab, a `<button role="tab">`. Takes `icon` |
|
|
1549
1608
|
| `Tabs.Panel` | `Tabs.Panel` | One tab's content, a `<div role="tabpanel">` |
|
|
1550
1609
|
|
|
@@ -1571,7 +1630,7 @@ Base UI's props pass through: `value`, `defaultValue`, `onValueChange` on the ro
|
|
|
1571
1630
|
|
|
1572
1631
|
## Style
|
|
1573
1632
|
|
|
1574
|
-
- Shares the quiet well and
|
|
1633
|
+
- Shares the quiet well and selected key with a single-choice ToggleGroup. Selection and label changes update immediately, without a slide or color fade. Normal selection uses a `key` fill without a visible border; hover only changes label and icon contrast. Focus reuses `mu-focus`, independently of selection. In forced colors the selected key retains its text-colored border. Use the standard sm and md size rows and the shared inset and radius tokens.
|
|
1575
1634
|
- Tabs are sized to their labels and wrap when space is narrow; the well and key grow to fit. On touch screens each tab is at least 44px tall and wide. The key re-measures when a label changes width.
|
|
1576
1635
|
- Selected and hovered labels `text`, others `text-secondary`. Weight never changes, so labels don't shift.
|
|
1577
1636
|
|
|
@@ -1597,7 +1656,7 @@ import { Collapsible } from "@usememos/ui";
|
|
|
1597
1656
|
| Part | Base UI part | Renders |
|
|
1598
1657
|
| --- | --- | --- |
|
|
1599
1658
|
| `Collapsible.Root` | `Collapsible.Root` | A `<div>` around the trigger and panel. Takes `size` |
|
|
1600
|
-
| `Collapsible.Trigger` | `Collapsible.Trigger` | A ghost [Button](button.md) with a chevron at the end that
|
|
1659
|
+
| `Collapsible.Trigger` | `Collapsible.Trigger` | A ghost [Button](button.md) with a chevron at the end that points up while open |
|
|
1601
1660
|
| `Collapsible.Panel` | `Collapsible.Panel` | The content, a `<div>`; unmounted while closed unless `keepMounted` |
|
|
1602
1661
|
|
|
1603
1662
|
## Props
|
|
@@ -1620,9 +1679,9 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `disabled`
|
|
|
1620
1679
|
## Style
|
|
1621
1680
|
|
|
1622
1681
|
- The trigger is a ghost key that bleeds `px` to the left, so its label lines up with the content below.
|
|
1623
|
-
- The chevron is `text-secondary` and
|
|
1682
|
+
- The chevron is `text-secondary` and changes orientation by 180° immediately when open; no rotation transition.
|
|
1624
1683
|
- Disabled labels and chevrons use `text-disabled`.
|
|
1625
|
-
- The panel
|
|
1684
|
+
- The panel opens and closes with the shared [Motion](../motion.md) fade and `space-1` offset over `duration-fade`. Its height never animates. Opening and closing are immediate under reduced motion.
|
|
1626
1685
|
|
|
1627
1686
|
<!-- docs/components/dialog.md -->
|
|
1628
1687
|
|
|
@@ -1708,7 +1767,7 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal`, `d
|
|
|
1708
1767
|
|
|
1709
1768
|
- `mu-popup` with `radius-2xl`, `space-5` padding, `dialog-width` wide; children stack `space-4` apart, and the Footer sits `space-6` under them. The backdrop is `overlay`.
|
|
1710
1769
|
- Title: `title`. Description: `body` in `text-secondary`, `space-1` under the title, so the two read as one header. The X is a small ghost Button at `space-4` from the corner. On touch screens it reserves 44px, and the title leaves room for that target.
|
|
1711
|
-
-
|
|
1770
|
+
- Opening and closing use the shared [Motion](../motion.md) fade and `space-1` offset over `duration-settle`; the backdrop fades with the same timing. A sheet uses `space-6` from the bottom, with square bottom corners and full-width buttons stacked with the action on top. Opening and closing are immediate under reduced motion, including the backdrop.
|
|
1712
1771
|
- Long content scrolls the viewport around the panel.
|
|
1713
1772
|
- `bare`: no padding, no gap and no X; the content is clipped to the radius. Set the width with `className` when it differs from `dialog-width`.
|
|
1714
1773
|
- `fullScreen`: the viewport's padding goes, and the popup fills it with square corners.
|
|
@@ -1775,7 +1834,7 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `onOpenChan
|
|
|
1775
1834
|
- `panel`: content `space-5` in, stacked `space-4` apart, as in a Dialog; the X is a small ghost Button at `space-4` from the top end corner. On touch screens it reserves 44px, and the title leaves room for that target. Title: `title`. Description: `body` in `text-secondary`, `space-1` under the title.
|
|
1776
1835
|
- `bare`: no padding, gap or X. A visually hidden `Drawer.Close` named `closeLabel` follows the content, so screen readers on touch screens can close it.
|
|
1777
1836
|
- The backdrop is `overlay`; it fades as the drawer is swiped away.
|
|
1778
|
-
-
|
|
1837
|
+
- Opens and closes from its attached edge over `duration-settle` with `ease-settle`, with a backdrop fade of the same duration. It follows the finger while swiped. Under reduced motion both panel and backdrop change immediately, without a slide or fade. See [Motion](../motion.md).
|
|
1779
1838
|
|
|
1780
1839
|
<!-- docs/components/popover.md -->
|
|
1781
1840
|
|
|
@@ -1838,7 +1897,106 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on
|
|
|
1838
1897
|
- `mu-popup` (`surface`, a 1px `edge`, `radius-xl`), `space-4` padding, `popover-width` wide. Children stack `space-3` apart.
|
|
1839
1898
|
- Title: `label`. Description: `caption` in `text-secondary`, `space-1` under the title.
|
|
1840
1899
|
- `list`: 6px padding and no gap, like a Menu. A title and description inside line up with the rows' text, with `space-2` above the title and below the description.
|
|
1841
|
-
-
|
|
1900
|
+
- Opening and closing use the shared [Motion](../motion.md) fade and `space-1` offset over `duration-fade`. Both are immediate under reduced motion.
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
## Compact reaction picker
|
|
1904
|
+
|
|
1905
|
+
Compose Popover, Toggle and Button; the library does not ship a ReactionPicker. The app owns counts, user identity, requests, pending/disabled state and whether a successful choice closes the popup. Both places read the same controlled `pressed` values; do not give the picker a separate selection state. This example keeps the popup open so several choices can be toggled.
|
|
1906
|
+
|
|
1907
|
+
On desktop the strip is 24px high (`sm`): soft pills, 14px emoji, 13px counts and 4px between them. The ghost add button is circular and the same height. The picker uses `mu-compact` for 28px cells, 2px between cells and 8px total inset (including its border). It sizes to its contents and Base UI anchors it to the add button. The default eight choices form one row; narrow viewports and coarse pointers use four columns. On coarse pointers all controls actually reserve at least 44px in the layout, so adjacent hit areas never overlap. Text and emoji stay the same size.
|
|
1908
|
+
|
|
1909
|
+
`icon` remains a Lucide slot. An emoji is content in `children`, with its own type class; the small layout class below makes an emoji-only Toggle square without depending on undocumented icon-slot behavior or internal `mu-*` classes. This is a CSS grid of ordinary toggles, not an ARIA grid or menu: Tab reaches each choice, Space/Enter toggles it, Escape dismisses an open Tooltip first, then the popup and restores trigger focus. Give each emoji a localized accessible name and a matching Tooltip. The popup is named with `aria-label` because this compact example has no visible title.
|
|
1910
|
+
|
|
1911
|
+
```tsx
|
|
1912
|
+
import { Button, Popover, Toggle, Tooltip } from "@usememos/ui";
|
|
1913
|
+
import { SmilePlus } from "lucide-react";
|
|
1914
|
+
|
|
1915
|
+
const choices = [
|
|
1916
|
+
{ emoji: "👍", label: "Thumbs up" },
|
|
1917
|
+
{ emoji: "❤️", label: "Heart" },
|
|
1918
|
+
{ emoji: "🎉", label: "Celebrate" },
|
|
1919
|
+
{ emoji: "😄", label: "Smile" },
|
|
1920
|
+
{ emoji: "🤔", label: "Thinking" },
|
|
1921
|
+
{ emoji: "👀", label: "Eyes" },
|
|
1922
|
+
{ emoji: "🚀", label: "Rocket" },
|
|
1923
|
+
{ emoji: "🙏", label: "Thanks" },
|
|
1924
|
+
];
|
|
1925
|
+
|
|
1926
|
+
type Reaction = { emoji: string; label: string; count: number; pressed: boolean };
|
|
1927
|
+
|
|
1928
|
+
// The parent supplies server counts and the current user's selected choices.
|
|
1929
|
+
function CompactReactions({ reactions, onPressedChange }: {
|
|
1930
|
+
reactions: Reaction[];
|
|
1931
|
+
onPressedChange: (emoji: string, pressed: boolean) => void;
|
|
1932
|
+
}) {
|
|
1933
|
+
return (
|
|
1934
|
+
<Tooltip.Provider>
|
|
1935
|
+
<div className="reaction-strip" role="group" aria-label="Reactions">
|
|
1936
|
+
{reactions.filter((reaction) => reaction.count > 0).map((reaction) => (
|
|
1937
|
+
<Toggle
|
|
1938
|
+
key={reaction.emoji}
|
|
1939
|
+
variant="soft" shape="pill" size="sm"
|
|
1940
|
+
aria-label={`${reaction.label}, ${reaction.count} reactions`}
|
|
1941
|
+
pressed={reaction.pressed}
|
|
1942
|
+
onPressedChange={(pressed) => onPressedChange(reaction.emoji, pressed)}
|
|
1943
|
+
>
|
|
1944
|
+
<span className="reaction-label">
|
|
1945
|
+
<span className="mu-body" aria-hidden>{reaction.emoji}</span>
|
|
1946
|
+
<span className="mu-caption reaction-count" aria-hidden>{reaction.count}</span>
|
|
1947
|
+
</span>
|
|
1948
|
+
</Toggle>
|
|
1949
|
+
))}
|
|
1950
|
+
<Popover.Root>
|
|
1951
|
+
<Tooltip.Root>
|
|
1952
|
+
<Tooltip.Trigger render={
|
|
1953
|
+
<Popover.Trigger render={
|
|
1954
|
+
<Button variant="ghost" shape="pill" size="sm" icon={<SmilePlus />} aria-label="Add reaction" />
|
|
1955
|
+
} />
|
|
1956
|
+
} />
|
|
1957
|
+
<Tooltip.Popup>Add reaction</Tooltip.Popup>
|
|
1958
|
+
</Tooltip.Root>
|
|
1959
|
+
<Popover.Popup align="start" aria-label="Choose reactions" className="mu-compact reaction-popup">
|
|
1960
|
+
<div className="reaction-options">
|
|
1961
|
+
{choices.map(({ emoji, label }) => (
|
|
1962
|
+
<Tooltip.Root key={emoji}>
|
|
1963
|
+
<Tooltip.Trigger render={
|
|
1964
|
+
<Toggle
|
|
1965
|
+
variant="soft" className="reaction-option"
|
|
1966
|
+
aria-label={label}
|
|
1967
|
+
pressed={reactions.some((reaction) => reaction.emoji === emoji && reaction.pressed)}
|
|
1968
|
+
onPressedChange={(pressed) => onPressedChange(emoji, pressed)}
|
|
1969
|
+
>
|
|
1970
|
+
<span className="mu-body" aria-hidden>{emoji}</span>
|
|
1971
|
+
</Toggle>
|
|
1972
|
+
} />
|
|
1973
|
+
<Tooltip.Popup>{label}</Tooltip.Popup>
|
|
1974
|
+
</Tooltip.Root>
|
|
1975
|
+
))}
|
|
1976
|
+
</div>
|
|
1977
|
+
</Popover.Popup>
|
|
1978
|
+
</Popover.Root>
|
|
1979
|
+
</div>
|
|
1980
|
+
</Tooltip.Provider>
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
The remaining CSS describes content and layout, not control colors, borders, focus or disabled states. Put it in the app's stylesheet, after the library (or in a later cascade layer). `Popover.Popup` supplies the surface and viewport collision constraints; keep its `max-width`. Its size context is md even if the trigger is sm. `mu-compact` must be on the popup itself because it is portaled.
|
|
1986
|
+
|
|
1987
|
+
```css
|
|
1988
|
+
.reaction-strip { display: flex; flex-wrap: wrap; align-items: center; gap: var(--mu-space-1); }
|
|
1989
|
+
.reaction-label { display: inline-flex; align-items: center; gap: var(--mu-space-1); }
|
|
1990
|
+
.reaction-count { font-variant-numeric: tabular-nums; }
|
|
1991
|
+
.reaction-popup { width: max-content; padding: calc(var(--mu-space-2) - 1px); }
|
|
1992
|
+
.reaction-options { display: grid; grid-auto-flow: column; gap: var(--mu-inset); max-height: var(--mu-list-height); overflow-y: auto; }
|
|
1993
|
+
.reaction-option { width: var(--mu-row-height); padding: 0; }
|
|
1994
|
+
@media (max-width: 639px), (pointer: coarse) {
|
|
1995
|
+
.reaction-options { grid-auto-flow: row; grid-template-columns: repeat(4, max-content); }
|
|
1996
|
+
}
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
A custom app theme now supplies `selected` alongside its other [color tokens](../color.md#app-themes); it must stay distinct from `hover`/`well` and keep at least 4.5:1 with `text` and `text-disabled`. Consumers do not need `data-pressed:bg-*`, `data-pressed:hover:bg-*`, forced-color rules, a custom focus treatment, control corner overrides or touch-size overrides.
|
|
1842
2000
|
|
|
1843
2001
|
<!-- docs/components/tooltip.md -->
|
|
1844
2002
|
|
|
@@ -1896,7 +2054,7 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `disabled`
|
|
|
1896
2054
|
## Style
|
|
1897
2055
|
|
|
1898
2056
|
- `primary` background, `on-primary` text, `label-sm`, `radius-md`, `space-1` by `space-2` padding, at most `tooltip-width` wide.
|
|
1899
|
-
-
|
|
2057
|
+
- Waits 400ms on hover before opening; keyboard focus has no opening delay. Opening and closing use the shared [Motion](../motion.md) fade and `space-1` offset over `duration-fade`. Both transitions are disabled under reduced motion. Moving between tooltips in a group skips the delay and replayed entrance.
|
|
1900
2058
|
|
|
1901
2059
|
<!-- docs/components/toast.md -->
|
|
1902
2060
|
|
|
@@ -1956,7 +2114,7 @@ Base UI's props pass through: `timeout` (default 5000ms) and `limit` (default 3)
|
|
|
1956
2114
|
|
|
1957
2115
|
- `mu-popup`, `toast-width` wide, top-right at `space-6` from the edges; below `breakpoint-sm`, full width across the top at `space-4`, below the status bar.
|
|
1958
2116
|
- A tone icon (`Info`, `CircleCheck`, `TriangleAlert`, `CircleAlert`) in `text-secondary`, `success`, `warning` or `danger`; title in `label`; description in `caption` `text-secondary`; the action as a small neutral Button; the X as a small ghost Button.
|
|
1959
|
-
- Newest in front; the ones behind peek out `space-2` below and shrink. A toast behind hides its text at once and gets a 1px `edge`, so its text never shows below a shorter front toast and the peeks stay visible in Dark. Hover or focus fans them out downward, `space-3` apart. Toasts slide in from above and can be swiped up or to the right. Under reduced motion
|
|
2117
|
+
- Newest in front; the ones behind peek out `space-2` below and shrink. A toast behind hides its text at once and gets a 1px `edge`, so its text never shows below a shorter front toast and the peeks stay visible in Dark. Hover or focus fans them out downward, `space-3` apart. Toasts slide in from above and can be swiped up or to the right. Opening and closing remain short, using `duration-settle`; they add no bounce or staged entrance. Under reduced motion visibility and stack changes are immediate, without a slide, scale or fade. See [Motion](../motion.md).
|
|
1960
2118
|
|
|
1961
2119
|
<!-- docs/components/avatar.md -->
|
|
1962
2120
|
|
|
@@ -2372,7 +2530,7 @@ Every native attribute passes through.
|
|
|
2372
2530
|
## Style
|
|
2373
2531
|
|
|
2374
2532
|
- `surface` fill, a 1px `line` edge, `radius-lg`. No shadow: it is flat, like a field.
|
|
2375
|
-
- Rows pad `space-3` × `space-4` at md and `space-2` × `space-3` at sm, divided by a 1px `line`. No hover on a plain row. An ItemButton fills its row, padded as the row is, lays out its children in a row `space-3` apart, highlights with `hover
|
|
2533
|
+
- Rows pad `space-3` × `space-4` at md and `space-2` × `space-3` at sm, divided by a 1px `line`. No hover on a plain row. An ItemButton fills its row, padded as the row is, lays out its children in a row `space-3` apart, highlights immediately with `hover` on pointer hover or touch press, and rounds with the box at the first and last row; keyboard focus darkens the row's reserved border.
|
|
2376
2534
|
- Text is `body` in `text`.
|
|
2377
2535
|
|
|
2378
2536
|
<!-- docs/components/prose.md -->
|
|
@@ -2799,6 +2957,10 @@ In an extension or an embed, pass the same `container` to every `Popup` and to `
|
|
|
2799
2957
|
<Toast.Viewport container={shadowRoot} />
|
|
2800
2958
|
```
|
|
2801
2959
|
|
|
2960
|
+
## Compact reactions
|
|
2961
|
+
|
|
2962
|
+
Use `Toggle variant="soft" shape="pill" size="sm"` for the display strip and a ghost pill Button for its add action. Compose the picker from Popover and controlled Toggles with `mu-compact`. See the [complete compact reaction picker recipe](components/popover.md#compact-reaction-picker) for emoji content, shared state, content-sized placement and the four-column touch layout. Counts and requests stay in the app.
|
|
2963
|
+
|
|
2802
2964
|
## Size
|
|
2803
2965
|
|
|
2804
2966
|
- **Set `size` once, on the outermost part that takes it.** `Menu.Root`, `Select.Root`, `Combobox.Root`, `Tabs.Root`, `Collapsible.Root`, `Table.Root`, `Field.Root`, `Fieldset.Root`, `RadioGroup` and `ToggleGroup` pass their size to every part and every Button, Toggle, Checkbox or Input inside them. Don't repeat it on the parts.
|
|
@@ -3057,7 +3219,7 @@ An app changes only the color and font tokens. Every other token and every `mu-*
|
|
|
3057
3219
|
|
|
3058
3220
|
- Every class the package ships starts with `mu-`.
|
|
3059
3221
|
- The [public classes](custom-components.md#public-classes) are for apps too: size rows (`mu-s-md`), surface roles (`mu-key`), focus, touch targets, icons and the type scale. Apps apply them to their own parts and never override them.
|
|
3060
|
-
- `mu-positioner` puts a popup's positioner on `layer-popup`, and `mu-popup-motion`
|
|
3222
|
+
- `mu-positioner` puts a popup's positioner on `layer-popup`, and `mu-popup-motion` supplies the shared opening and closing fade and `space-1` offset over `duration-fade`. Under reduced motion it has no transition, including opacity. See [Motion](motion.md).
|
|
3061
3223
|
- The surface classes `mu-well`, `mu-key` and `mu-popup` apply flat fills and borders. Use them for new parts. See [Surfaces and boundaries](depth.md).
|
|
3062
3224
|
- `mu-prose` styles rendered content. See [Prose](components/prose.md).
|
|
3063
3225
|
- `mu-icon` applies icon size and stroke rules outside a component, at the size of the nearest size row; `mu-icon-xs`, `mu-icon-sm` and `mu-icon-md` set a fixed size on their own. See [Icons](icons.md).
|
|
@@ -3115,7 +3277,7 @@ import { Ellipsis, Pin, Trash2 } from "lucide-react";
|
|
|
3115
3277
|
- **`className` and `style`** pass through to the part's element, after our classes.
|
|
3116
3278
|
- **Accessible names.** A control without a visible label takes `aria-label`. Icon-only controls always do.
|
|
3117
3279
|
- **`tone`** describes a label or status treatment. The shared status tones are `"neutral" | "success" | "warning" | "danger"`. Badge also keeps `"outline"` for a quiet label and `"primary"` for emphasis; these are label treatments, not statuses. Badge's user-chosen `color` is allowed only with `"neutral"` (or an omitted tone). Toasts use the shared status tones through Base UI's `type` prop.
|
|
3118
|
-
- **`variant`** selects a component's predefined appearance or structure. Values belong to that component, not a global variant set: Button has `"primary" | "neutral" | "ghost" | "danger"`; Select.Trigger has `"neutral" | "ghost"`; Popover.Popup has `"panel" | "list"`; Dialog.Popup and Drawer.Popup have `"panel" | "bare"`. See each component's page for usage and defaults.
|
|
3280
|
+
- **`variant`** selects a component's predefined appearance or structure. Values belong to that component, not a global variant set: Button has `"primary" | "neutral" | "ghost" | "danger"`; Toggle has `"ghost" | "soft"`; Select.Trigger has `"neutral" | "ghost"`; Popover.Popup has `"panel" | "list"`; Dialog.Popup and Drawer.Popup have `"panel" | "bare"`. See each component's page for usage and defaults.
|
|
3119
3281
|
- **Interaction states are not variants.** Hover, focus, pressed, selected, disabled, invalid and loading use the component's behavior props and state attributes, with the shared treatments in [States](states.md). Do not add variants or component-specific tokens for these states.
|
|
3120
3282
|
- **Native props pass through** to the element a component or part renders.
|
|
3121
3283
|
|
|
@@ -3169,7 +3331,7 @@ A class function returns the classes that make any element look like a component
|
|
|
3169
3331
|
|
|
3170
3332
|
| Function | Returns | For an app's own |
|
|
3171
3333
|
| --- | --- | --- |
|
|
3172
|
-
| `buttonClasses({ variant, size, iconOnly })` | A string | Anchor that looks like an action |
|
|
3334
|
+
| `buttonClasses({ variant, size, shape, iconOnly })` | A string | Anchor that looks like an action |
|
|
3173
3335
|
| `inputClasses({ size })` | A string | One-line field, such as a tag input around a native `<input>` |
|
|
3174
3336
|
| `textareaClasses({ size })` | A string | Multi-line field, such as a content-editable editor |
|
|
3175
3337
|
| `badgeClasses({ tone })` | A string | Label, such as a tag that is also a link |
|
|
@@ -3178,7 +3340,7 @@ A class function returns the classes that make any element look like a component
|
|
|
3178
3340
|
|
|
3179
3341
|
State is set with attributes, never with extra classes:
|
|
3180
3342
|
|
|
3181
|
-
- A field
|
|
3343
|
+
- A text field keeps its normal border and surface when it or its input has focus; use the caret or text selection to identify typing focus. `aria-invalid="true"` shows the error edge, and `aria-disabled="true"` dims it.
|
|
3182
3344
|
- A menu row is highlighted while it has `data-highlighted`, and dimmed with `data-disabled`. Base UI sets both; a list the app drives itself sets them on the active row. A directly focusable button or anchor also gets the row highlight on `:focus-visible`, even without `data-highlighted`. `item` works on a `<div>`, `<button>` or `<a>`. For a two-line row, wrap `label` and `description` in a `text` span.
|
|
3183
3345
|
- A popup fades in with `data-starting-style` and out with `data-ending-style`, which Base UI's popups set.
|
|
3184
3346
|
- A tag with a user's color sets `--mu-badge-color` on the element: `style={{ "--mu-badge-color": tag.color } as React.CSSProperties}`. The fill becomes a tint of it; the text stays ink.
|
|
@@ -3213,7 +3375,7 @@ Apply these classes to an app's own elements. Don't override their rules: the pr
|
|
|
3213
3375
|
| `mu-s-xs` | The xs size row: 20px | Secondary actions beside 13px text |
|
|
3214
3376
|
| `mu-s-sm` | The sm size row: 24px | Dense toolbars, table rows, filters |
|
|
3215
3377
|
| `mu-s-md` | The md size row: 32px | Everything else |
|
|
3216
|
-
| `mu-compact` | md controls and rows inside are `control-compact` (28px) tall; text, icons, padding and radius stay md | An app's sidebar with 28px rows,
|
|
3378
|
+
| `mu-compact` | md controls and rows inside are `control-compact` (28px) tall; text, icons, padding and radius stay md | An app's sidebar with 28px rows, a list Popover beside it, or a [compact picker](components/popover.md#compact-reaction-picker) |
|
|
3217
3379
|
| `mu-base` | The system font, the row's text size and weight 500, the `text` color, and icon sizing for Lucide icons inside | The root of a control |
|
|
3218
3380
|
| `mu-well` | `well` and a quiet 1px `edge` | A recessed surface; add a separate treatment for selection |
|
|
3219
3381
|
| `mu-key` | `key` and a quiet 1px `edge`, including with selected or pressed attributes | A bounded control surface, or the bright selected face inside a well. It does not add interaction-state styling; use Toggle or ToggleGroup for choices |
|
|
@@ -3278,7 +3440,7 @@ An app on Tailwind CSS v4 loads `tailwind.css` after the package's other stylesh
|
|
|
3278
3440
|
@import "@usememos/ui/tailwind.css";
|
|
3279
3441
|
```
|
|
3280
3442
|
|
|
3281
|
-
It maps the
|
|
3443
|
+
It maps the 29 color tokens, seven radius tokens and three font tokens to utilities with an `mu-` prefix, so they never clash with an app's own colors:
|
|
3282
3444
|
|
|
3283
3445
|
| Token | Utility |
|
|
3284
3446
|
| --- | --- |
|