@codenhub/styles 0.1.0 → 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/README.md +8 -69
- package/dist/aesthetics/chunky-tile.css +1 -1
- package/dist/aesthetics/index.css +1 -1
- package/dist/components.css +1 -1
- package/dist/index.css +1 -1
- package/dist/native.css +1 -1
- package/dist/theme.css +1 -1
- package/dist/tw/aesthetics/chunky-tile.css +20 -2
- package/dist/tw/box.css +45 -2
- package/dist/tw/components/button.css +38 -29
- package/dist/tw/components/feedback.css +58 -41
- package/dist/tw/components/form.css +139 -47
- package/dist/tw/components/index.css +16 -6
- package/dist/tw/components/loader.css +2 -16
- package/dist/tw/components/surface.css +122 -5
- package/dist/tw/components/tooltip.css +102 -91
- package/dist/tw/elevation.css +57 -14
- package/dist/tw/index.css +23 -5
- package/dist/tw/intent.css +36 -12
- package/dist/tw/native.css +4 -20
- package/dist/tw/reset.css +40 -32
- package/dist/tw/theme.css +46 -8
- package/dist/tw/utilities/content.css +104 -18
- package/dist/tw/utilities/index.css +1 -1
- package/dist/tw/utilities/layout.css +33 -0
- package/docs/accessibility.md +68 -48
- package/docs/changelog/0.1.0.md +37 -0
- package/docs/changelog/0.1.1.md +10 -0
- package/docs/changelog/0.2.0.md +41 -0
- package/docs/changelog/index.md +11 -0
- package/docs/concepts.md +21 -46
- package/docs/index.md +18 -45
- package/docs/integrating/astro.md +13 -7
- package/docs/integrating/index.md +25 -16
- package/docs/integrating/nextjs.md +31 -8
- package/docs/integrating/svelte.md +14 -8
- package/docs/integrating/tailwind.md +31 -15
- package/docs/integrating/vue.md +13 -7
- package/docs/setup.md +31 -32
- package/docs/usage/aesthetics.md +31 -107
- package/docs/usage/buttons.md +24 -36
- package/docs/usage/composing.md +31 -102
- package/docs/usage/content-and-layout.md +65 -83
- package/docs/usage/customizing.md +25 -90
- package/docs/usage/feedback.md +25 -56
- package/docs/usage/forms.md +48 -80
- package/docs/usage/index.md +9 -17
- package/docs/usage/theming.md +25 -81
- package/docs/usage/tooltips.md +55 -35
- package/llms-full.txt +1193 -1510
- package/package.json +121 -44
package/llms-full.txt
CHANGED
|
@@ -12,8 +12,7 @@ pnpm add @codenhub/styles
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
Import the compiled stylesheet for the complete token, reset, utility, and
|
|
16
|
-
component-class surface:
|
|
15
|
+
Import the compiled stylesheet for the complete token, reset, utility, and component-class surface:
|
|
17
16
|
|
|
18
17
|
```css
|
|
19
18
|
@import "@codenhub/styles";
|
|
@@ -27,72 +26,17 @@ component-class surface:
|
|
|
27
26
|
</main>
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```css
|
|
33
|
-
@import "tailwindcss";
|
|
34
|
-
@import "@codenhub/styles/tw";
|
|
35
|
-
|
|
36
|
-
@source "./src";
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
The root follows the system color-scheme preference. Apply `.light` or `.dark`
|
|
40
|
-
to force a theme for the root or a subtree.
|
|
41
|
-
|
|
42
|
-
```html
|
|
43
|
-
<html class="dark">
|
|
44
|
-
<body>
|
|
45
|
-
<button class="btn primary">Dark themed button</button>
|
|
46
|
-
</body>
|
|
47
|
-
</html>
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The aliases `.theme-light`, `.theme-dark`, and `data-theme="light|dark"` are
|
|
51
|
-
also supported. See [Concepts](./docs/concepts.md#theme-selection) for
|
|
52
|
-
precedence details.
|
|
53
|
-
|
|
54
|
-
`.solid`, `.soft`, and `.ghost` set how much intent fills a component; `.edged`
|
|
55
|
-
and `.edgeless` set whether it draws a boundary. Both axes cascade, so a
|
|
56
|
-
container sets the look below it and any element can still override it.
|
|
57
|
-
`.solid` also takes the boundary away, because a filled box ringed in another
|
|
58
|
-
color is what the blend behind `.edged` exists to prevent; `.solid.edged` is the
|
|
59
|
-
way back to a line.
|
|
60
|
-
|
|
61
|
-
```html
|
|
62
|
-
<div class="soft">
|
|
63
|
-
<button class="btn primary">Soft</button>
|
|
64
|
-
<span class="badge success">Soft</span>
|
|
65
|
-
<button class="btn primary ghost edged">Outlined</button>
|
|
66
|
-
</div>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Aesthetics decide what a component is made of. They are opt-in, so import the
|
|
70
|
-
stylesheet after the base one to make `.neobrutalism`, `.glass`, `.pixel`, and
|
|
71
|
-
`.chunky-tile` available. They cascade to any subtree the same way, and each is
|
|
72
|
-
also importable on its own from `@codenhub/styles/aesthetics/<name>`.
|
|
73
|
-
|
|
74
|
-
```css
|
|
75
|
-
@import "@codenhub/styles";
|
|
76
|
-
@import "@codenhub/styles/aesthetics";
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
```html
|
|
80
|
-
<section class="neobrutalism">
|
|
81
|
-
<button class="btn primary">Thick ink and a hard shadow</button>
|
|
82
|
-
</section>
|
|
83
|
-
```
|
|
29
|
+
For the Tailwind CSS v4 entrypoint, theme selection, the intent/presentation axes, and opt-in aesthetics, see [Setup](./docs/setup.md), [Concepts](./docs/concepts.md), and [Usage](./docs/usage/index.md).
|
|
84
30
|
|
|
85
31
|
## Documentation
|
|
86
32
|
|
|
87
|
-
- [Documentation overview](./docs/index.md): What the package is, what it
|
|
88
|
-
does and does not do, and where to go next.
|
|
33
|
+
- [Documentation overview](./docs/index.md): What the package is, what it does and does not do, and where to go next.
|
|
89
34
|
- [Setup](./docs/setup.md): Installation, entrypoints, and configuration.
|
|
90
35
|
- [Concepts](./docs/concepts.md): The three-axis model and the token system.
|
|
91
|
-
- [Usage](./docs/usage/index.md): Composing, theming, customizing,
|
|
92
|
-
|
|
93
|
-
- [Integrating](./docs/integrating/index.md): Wiring the stylesheet into
|
|
94
|
-
Next.js, Vue, Svelte, Astro, or a Tailwind CSS v4 build.
|
|
36
|
+
- [Usage](./docs/usage/index.md): Composing, theming, customizing, aesthetics, and each component family.
|
|
37
|
+
- [Integrating](./docs/integrating/index.md): Wiring the stylesheet into Next.js, Vue, Svelte, Astro, or a Tailwind CSS v4 build.
|
|
95
38
|
- [Accessibility](./docs/accessibility.md): CSS accessibility hooks and non-goals.
|
|
39
|
+
- [Changelog](./docs/changelog/index.md): Release history, including breaking changes.
|
|
96
40
|
|
|
97
41
|
## Requirements
|
|
98
42
|
|
|
@@ -105,169 +49,166 @@ also importable on its own from `@codenhub/styles/aesthetics/<name>`.
|
|
|
105
49
|
|
|
106
50
|
## Notes
|
|
107
51
|
|
|
108
|
-
This package has no JavaScript behavior.
|
|
109
|
-
`--layout-stack-gap` and `--layout-cluster-gap` tokens; no compatibility aliases
|
|
110
|
-
are provided.
|
|
52
|
+
This package has no JavaScript behavior; no version provides compatibility aliases for a removed class or token. `0.2.0` removes `.btn.loading` (compose a disabled `.btn` with a `.loader` child instead) and `.alert.icon`'s built-in glyph (drop a consumer-supplied icon into `.alert-icon` instead), and `.tooltip` now requires a `.tooltip-bubble` child element rather than a `data-tooltip` attribute. See the [changelog](./docs/changelog/index.md) for the complete history.
|
|
111
53
|
|
|
112
54
|
## License
|
|
113
55
|
|
|
114
|
-
Licensed under Apache-2.0. Embedded SVG icons derived from
|
|
115
|
-
[Lucide](https://lucide.dev) are ISC-licensed, and loaders derived from
|
|
116
|
-
[svg-spinners](https://github.com/n3r4zzurr0/svg-spinners) are MIT-licensed.
|
|
117
|
-
See [NOTICE](NOTICE) for the required notices.
|
|
56
|
+
Licensed under Apache-2.0. Embedded SVG icons derived from [Lucide](https://lucide.dev) are ISC-licensed, and loaders derived from [svg-spinners](https://github.com/n3r4zzurr0/svg-spinners) are MIT-licensed. See [NOTICE](NOTICE) for the required notices.
|
|
118
57
|
|
|
119
58
|
<!-- Source: docs/index.md -->
|
|
120
59
|
|
|
121
60
|
# @codenhub/styles
|
|
122
61
|
|
|
123
|
-
`@codenhub/styles` is a CSS-only design system: tokens, a reset, typography and
|
|
124
|
-
layout utilities, and composable classes for buttons, forms, feedback,
|
|
125
|
-
surfaces, and tooltips. Every component separates three independent choices —
|
|
126
|
-
which color, how much of it, and what it's made of — so changing one never
|
|
127
|
-
requires a component-specific variant. Four optional aesthetics
|
|
128
|
-
(`.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`) restyle that same
|
|
129
|
-
component set without touching markup.
|
|
62
|
+
`@codenhub/styles` is a CSS-only design system: tokens, a reset, typography and layout utilities, and composable classes for buttons, forms, feedback, surfaces, and tooltips. Every component separates three independent choices — which color, how much of it, and what it's made of — so changing one never requires a component-specific variant. Four optional aesthetics (`.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`) restyle that same component set without touching markup.
|
|
130
63
|
|
|
131
|
-
The package ships as ready-to-import compiled CSS, or as Tailwind CSS v4
|
|
132
|
-
source entrypoints for a project that already runs Tailwind. Both cover the
|
|
133
|
-
same classes; the difference is which build processes them.
|
|
64
|
+
The package ships as ready-to-import compiled CSS, or as Tailwind CSS v4 source entrypoints for a project that already runs Tailwind. Both cover the same classes; the difference is which build processes them.
|
|
134
65
|
|
|
135
66
|
## What it does
|
|
136
67
|
|
|
137
|
-
- Publishes color, foundation, intent, presentation, and material design
|
|
138
|
-
|
|
139
|
-
- Provides
|
|
140
|
-
|
|
141
|
-
- Provides
|
|
142
|
-
badges, loaders, progress, cards/panels, tables, quotes, code, and
|
|
143
|
-
tooltips.
|
|
144
|
-
- Provides four opt-in aesthetics that restyle the same components by setting
|
|
145
|
-
material tokens, not by adding component variants.
|
|
146
|
-
- Provides classless mappings so plain HTML elements pick up base styling
|
|
147
|
-
without added classes, from a dedicated entrypoint.
|
|
68
|
+
- Publishes color, foundation, intent, presentation, and material design tokens as CSS custom properties, themed with `light-dark()`.
|
|
69
|
+
- Provides a reset, focus-visible handling, and responsive layout/content utilities.
|
|
70
|
+
- Provides composable classes for buttons, form controls and toggles, alerts, badges, loaders, progress, cards/panels, tables, quotes, code, and tooltips.
|
|
71
|
+
- Provides four opt-in aesthetics that restyle the same components by setting material tokens, not by adding component variants.
|
|
72
|
+
- Provides classless mappings so plain HTML elements pick up base styling without added classes, from a dedicated entrypoint.
|
|
148
73
|
- Publishes Tailwind CSS v4 source entrypoints alongside the compiled output.
|
|
149
74
|
|
|
150
75
|
## What it does not do
|
|
151
76
|
|
|
152
|
-
- No JavaScript. There is no runtime, no framework bindings, and no build-time
|
|
153
|
-
|
|
154
|
-
- No
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- No theme-toggling logic. Theme selection is a class or attribute a consumer
|
|
158
|
-
applies; wiring a toggle to system preference, storage, or a user setting is
|
|
159
|
-
outside this package. The `@codenhub/theme` package covers that need for
|
|
160
|
-
consumers who want it.
|
|
161
|
-
- No shipped font binaries. `.pixel` and `.chunky-tile` accept a
|
|
162
|
-
consumer-supplied font and fall back to a system stack when none is given.
|
|
163
|
-
- No compatibility layer for removed tokens or classes. Breaking changes are
|
|
164
|
-
documented, not silently aliased.
|
|
77
|
+
- No JavaScript. There is no runtime, no framework bindings, and no build-time code generation beyond the CSS itself.
|
|
78
|
+
- No semantic HTML, ARIA, keyboard behavior, focus management, validation, or announcement timing. See [Accessibility](docs/accessibility.md) for the exact line between what the CSS provides and what an application must still add.
|
|
79
|
+
- No theme-toggling logic. Theme selection is a class or attribute a consumer applies; wiring a toggle to system preference, storage, or a user setting is outside this package. The `@codenhub/theme` package covers that need for consumers who want it.
|
|
80
|
+
- No shipped font binaries. `.pixel` and `.chunky-tile` accept a consumer-supplied font and fall back to a system stack when none is given.
|
|
81
|
+
- No compatibility layer for removed tokens or classes. Breaking changes are documented, not silently aliased.
|
|
165
82
|
|
|
166
83
|
## Next steps
|
|
167
84
|
|
|
168
|
-
- [Setup](docs/setup.md): Install the package, choose an entrypoint, and render a
|
|
169
|
-
|
|
170
|
-
- [
|
|
171
|
-
|
|
172
|
-
- [
|
|
173
|
-
aesthetics, and each component family.
|
|
174
|
-
- [Integrating](docs/integrating/index.md): Wiring the stylesheet into Next.js,
|
|
175
|
-
Vue, Svelte, Astro, or a Tailwind CSS v4 build.
|
|
176
|
-
- [Accessibility](docs/accessibility.md): CSS accessibility hooks and what
|
|
177
|
-
remains an application responsibility.
|
|
85
|
+
- [Setup](docs/setup.md): Install the package, choose an entrypoint, and render a first component.
|
|
86
|
+
- [Concepts](docs/concepts.md): The three-axis model, the token system, and how theme selection works — read this before the usage guides.
|
|
87
|
+
- [Usage](docs/usage/index.md): Guides for composing axes, theming, customizing, aesthetics, and each component family.
|
|
88
|
+
- [Integrating](docs/integrating/index.md): Wiring the stylesheet into Next.js, Vue, Svelte, Astro, or a Tailwind CSS v4 build.
|
|
89
|
+
- [Accessibility](docs/accessibility.md): CSS accessibility hooks and what remains an application responsibility.
|
|
178
90
|
|
|
179
|
-
<!-- Source: docs/
|
|
91
|
+
<!-- Source: docs/setup.md -->
|
|
180
92
|
|
|
181
|
-
#
|
|
93
|
+
# Setup
|
|
182
94
|
|
|
183
|
-
|
|
95
|
+
## Installation
|
|
184
96
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
189
|
-
| `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
|
|
190
|
-
| Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
|
|
191
|
-
| Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice; hover and focus take the tone whole. |
|
|
192
|
-
| Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
|
|
193
|
-
| Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
|
|
194
|
-
| Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest. |
|
|
195
|
-
| `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
|
|
196
|
-
| Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
|
|
197
|
-
| Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
|
|
198
|
-
| Current page | `[aria-current="page"]` applies primary color and stronger font weight. |
|
|
199
|
-
| Open state | `[data-state="open"]` styles supported surfaces and tooltips. |
|
|
200
|
-
| Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
|
|
201
|
-
| Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
|
|
202
|
-
| Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
|
|
203
|
-
| Filled labels | Every intent's `.solid` label meets 1.4.3 (4.5:1) as normal text, in both themes. Lowest is success at 5.14:1. |
|
|
204
|
-
| Warning ink | `--color-warning-contrast` is the near-black tone, not the page tone. Amber dark enough to carry white is not amber. |
|
|
205
|
-
| Partial fills | Contrast ink appears only past a half fill, so a capped plate keeps `--intent-strong` rather than walking toward the page. |
|
|
206
|
-
| Intent hue in dark | `--intent-strong` is the `-300` shade in dark, so a soft or ghost label keeps its own hue instead of reading white. |
|
|
207
|
-
| Toggle marks | A checked mark meets 3:1 as a state indicator on every plate its fill class can paint, in both themes. |
|
|
208
|
-
| Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
|
|
209
|
-
| Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
|
|
210
|
-
|
|
211
|
-
The loader fallback is embedded in loader CSS, so focused imports that include
|
|
212
|
-
loaders honor reduced motion without the reset. This includes
|
|
213
|
-
`@codenhub/styles/components`, `@codenhub/styles/tw/components`,
|
|
214
|
-
`@codenhub/styles/tw/button`, and `@codenhub/styles/tw/loader`. The complete and
|
|
215
|
-
native entrypoints include both that fallback and the reset's broader animation
|
|
216
|
-
and transition shortening.
|
|
217
|
-
|
|
218
|
-
In forced colors, class-based form controls including `.text-control` receive a
|
|
219
|
-
2px `Highlight` system-color focus outline. The native entrypoint provides the
|
|
220
|
-
same visible system outline for unclassed text inputs, selects, textareas,
|
|
221
|
-
checkboxes, and radios.
|
|
222
|
-
|
|
223
|
-
Custom checkboxes and radios use the system `Canvas`, `CanvasText`, `Highlight`,
|
|
224
|
-
and `HighlightText` colors in forced-colors mode so checked and unchecked states
|
|
225
|
-
remain distinct. Shape, size, and spacing remain unchanged.
|
|
226
|
-
|
|
227
|
-
## Required Outside CSS
|
|
97
|
+
```sh
|
|
98
|
+
pnpm add @codenhub/styles
|
|
99
|
+
```
|
|
228
100
|
|
|
229
|
-
|
|
101
|
+
## Quick start
|
|
230
102
|
|
|
231
|
-
|
|
232
|
-
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
233
|
-
| Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
|
|
234
|
-
| Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
|
|
235
|
-
| Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
|
|
236
|
-
| Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
|
|
237
|
-
| Tooltips | Provide accessible names/descriptions. CSS pseudo-element content is not enough for all assistive tech. |
|
|
238
|
-
| Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
|
|
239
|
-
| Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
|
|
240
|
-
| Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
|
|
103
|
+
Import the compiled stylesheet for the complete token, reset, utility, and component-class surface, then compose helper classes in markup:
|
|
241
104
|
|
|
242
|
-
|
|
105
|
+
```css
|
|
106
|
+
@import "@codenhub/styles";
|
|
107
|
+
```
|
|
243
108
|
|
|
244
|
-
|
|
109
|
+
```html
|
|
110
|
+
<main class="stack">
|
|
111
|
+
<p class="text-label">Status</p>
|
|
112
|
+
<h1 class="text-title">Ready to publish</h1>
|
|
113
|
+
<button class="btn primary">Continue</button>
|
|
114
|
+
</main>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The full stylesheet applies global reset and focus-visible rules, but does not apply the optional classless native-element mappings, and does not include aesthetics. Import focused entrypoints when global rules are not appropriate; see [Import paths](#import-paths) below.
|
|
118
|
+
|
|
119
|
+
Aesthetics are opt-in. Import the combined aesthetics stylesheet, or one aesthetic on its own, after the base stylesheet so its rules win:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
@import "@codenhub/styles";
|
|
123
|
+
@import "@codenhub/styles/aesthetics";
|
|
124
|
+
```
|
|
245
125
|
|
|
246
126
|
```html
|
|
247
|
-
<
|
|
248
|
-
<
|
|
127
|
+
<section class="neobrutalism">
|
|
128
|
+
<button class="btn primary">Thick ink and a hard shadow</button>
|
|
129
|
+
</section>
|
|
249
130
|
```
|
|
250
131
|
|
|
251
|
-
|
|
132
|
+
A project already running Tailwind CSS v4 imports the source entrypoint instead; see [Integrating → Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
The root follows the operating-system color-scheme preference. Apply `.light` or `.dark` to the root or any ancestor to force token values for that subtree. `.theme-light`, `.theme-dark`, and `data-theme="light|dark"` are aliases.
|
|
252
137
|
|
|
253
138
|
```html
|
|
254
|
-
<
|
|
255
|
-
<
|
|
139
|
+
<html class="dark">
|
|
140
|
+
<body>
|
|
141
|
+
<button class="btn primary">Dark themed button</button>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
256
144
|
```
|
|
257
145
|
|
|
258
|
-
|
|
146
|
+
See [Concepts → Theme selection](docs/concepts.md#theme-selection) for how selectors resolve and [Usage → Theming](docs/usage/theming.md) for the full color token reference.
|
|
147
|
+
|
|
148
|
+
### Dark variant
|
|
149
|
+
|
|
150
|
+
`/tw` source entrypoints define a custom `dark:` variant for your own utilities. It fires under an explicit dark selector (`.dark`, `.theme-dark`, or `[data-theme="dark"]`, on the element or an ancestor), or under the system preference when nothing under it forces a theme explicitly:
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<html>
|
|
154
|
+
<body>
|
|
155
|
+
<p class="dark:text-white">Follows the OS preference here</p>
|
|
156
|
+
<div class="light">
|
|
157
|
+
<p class="dark:text-white">Stays light -- this subtree forced it</p>
|
|
158
|
+
</div>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
An explicit selector at any depth wins over the system preference, in either direction, the same as the token theme it tracks.
|
|
164
|
+
|
|
165
|
+
The one place it does not track the token theme: nesting an explicit selector inside the opposite one. `color-scheme` inherits, so a `.light` nested inside `.dark` themes its own tokens correctly regardless of depth -- the nearest declaration always wins. `dark:` matches by selector instead, and `.dark *` matches every descendant of a `.dark` element with no way to stop at a nearer `.light` in between, so a `dark:` utility inside that nested `.light` still fires even though the tokens around it are light. Apply one theme per subtree rather than nesting opposite ones if a page uses both tokens and `dark:` utilities together.
|
|
166
|
+
|
|
167
|
+
## Import paths
|
|
168
|
+
|
|
169
|
+
Compiled entrypoints are ready-to-import CSS and require only tooling that can resolve package CSS imports. `/tw` entrypoints publish copied, uncompiled source from `dist/tw`; a Tailwind v4 build must still process their `@theme`, `@utility`, `@apply`, and related directives. Focused source component entrypoints include theme tokens so their classes can work independently.
|
|
170
|
+
|
|
171
|
+
| Import path | Composition and effects |
|
|
172
|
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| `@codenhub/styles` | Compiled complete surface: Tailwind base/theme output, tokens, typography, utilities, components, and reset. Applies global reset, root theme, and focus rules. |
|
|
174
|
+
| `@codenhub/styles/theme` | Compiled Tailwind theme variables, Codenhub tokens, and the presentation classes. Sets root theme properties and system/explicit theme selectors; no reset or component classes. |
|
|
175
|
+
| `@codenhub/styles/components` | Compiled theme tokens, typography utilities, and every component class. Includes static reduced-motion loader masks; no reset, layout/content utilities, or native mappings. |
|
|
176
|
+
| `@codenhub/styles/native` | Compiled complete surface plus classless mappings for headings, text, content elements, form controls, and buttons. Applies the reset and broad element selectors. |
|
|
177
|
+
| `@codenhub/styles/aesthetics` | Compiled `.neobrutalism`, `.glass`, `.pixel`, and `.chunky-tile` classes. Adds no tokens or components of its own; import after the base stylesheet. |
|
|
178
|
+
| `@codenhub/styles/aesthetics/*` | One compiled aesthetic: `/neobrutalism`, `/glass`, `/pixel`, or `/chunky-tile`. Same rules and ordering as the combined file. |
|
|
179
|
+
| `@codenhub/styles/tw` | Source equivalent of the complete stylesheet. Imports Tailwind itself, theme, typography, utilities, components, and reset; applies the same global rules. |
|
|
180
|
+
| `@codenhub/styles/tw/theme` | Source theme, tokens, and presentation classes. Imports Tailwind's theme layer; emits root/system/explicit theme selectors and defines the package's custom `dark:` variant. |
|
|
181
|
+
| `@codenhub/styles/tw/components` | Published Tailwind source for theme, typography, buttons, feedback, forms, loaders, surfaces, and tooltips, including static reduced-motion loader masks. No reset, layout/content utilities, or native mappings. |
|
|
182
|
+
| `@codenhub/styles/tw/surface` | Source theme plus `.surface`, `.card`, and `.panel`. |
|
|
183
|
+
| `@codenhub/styles/tw/button` | Published Tailwind source for theme and `.btn`. Pair it with `/tw/loader` for a loading state composed from `.loader`. |
|
|
184
|
+
| `@codenhub/styles/tw/form` | Source theme plus field, public `text-control`, input, textarea, select, checkbox, radio, and switch utilities. |
|
|
185
|
+
| `@codenhub/styles/tw/feedback` | Source theme plus alert, badge, skeleton, and progress utilities and their keyframes. |
|
|
186
|
+
| `@codenhub/styles/tw/loader` | Published Tailwind source for activity indicators and loaders, including static reduced-motion masks. It has no theme or reset side effects and uses `currentColor`. |
|
|
187
|
+
| `@codenhub/styles/tw/tooltip` | Source theme plus tooltip utilities. |
|
|
188
|
+
| `@codenhub/styles/tw/reset` | Source theme, typography utilities, and global reset/accessibility rules. It changes root/body/elements and selection, motion, focus, scrollbar, and forced colors. |
|
|
189
|
+
| `@codenhub/styles/tw/native` | Source complete surface plus reset and classless native-element mappings; broadest global effects. |
|
|
190
|
+
| `@codenhub/styles/tw/typography` | Source theme plus typography utilities; sets theme selectors but no reset or classless mappings. |
|
|
191
|
+
| `@codenhub/styles/tw/utilities` | Source theme and typography composition plus layout and content utilities; safelists the package utility names. |
|
|
192
|
+
| `@codenhub/styles/tw/aesthetics` | Source aesthetics, combined. `/tw/aesthetics/neobrutalism`, `/glass`, `/pixel`, and `/chunky-tile` publish one apiece. Plain CSS with no theme or reset side effects. |
|
|
193
|
+
|
|
194
|
+
Avoid importing overlapping entrypoints in the same build unless duplicate generated CSS is acceptable; pick the narrowest entrypoint that covers what a page needs.
|
|
195
|
+
|
|
196
|
+
## Requirements
|
|
197
|
+
|
|
198
|
+
- Consumer tooling must resolve package CSS imports.
|
|
199
|
+
- Chrome 123, Safari 17.5, or Firefox 121 and newer. Color tokens are declared with `light-dark()` and selected by `color-scheme` (Firefox 120), and `.input-group` propagates a nested control's invalid and disabled state with `:has()` (Firefox 121).
|
|
200
|
+
- Tailwind CSS 4 or newer is required only for `/tw` source entrypoints.
|
|
201
|
+
- The package has no JavaScript runtime. Apps must provide semantic HTML, ARIA, keyboard behavior, focus management, validation, and announcements; see [Accessibility](docs/accessibility.md).
|
|
259
202
|
|
|
260
203
|
<!-- Source: docs/concepts.md -->
|
|
261
204
|
|
|
262
205
|
# Concepts
|
|
263
206
|
|
|
264
|
-
Read this before the usage guides. It explains the mental model every
|
|
265
|
-
component, token, and class in this package shares.
|
|
207
|
+
Read this before the usage guides. It explains the mental model every component, token, and class in this package shares.
|
|
266
208
|
|
|
267
209
|
## The three axes
|
|
268
210
|
|
|
269
|
-
Every component separates three independent choices, so each can change
|
|
270
|
-
without a component-specific variant:
|
|
211
|
+
Every component separates three independent choices, so each can change without a component-specific variant:
|
|
271
212
|
|
|
272
213
|
| Axis | Question | Classes | Inheritance |
|
|
273
214
|
| ------------ | --------------- | ----------------------------------------------------------------------------------------- | ------------ |
|
|
@@ -282,25 +223,16 @@ without a component-specific variant:
|
|
|
282
223
|
</section>
|
|
283
224
|
```
|
|
284
225
|
|
|
285
|
-
Intent stays on the component so a semantic container cannot accidentally
|
|
286
|
-
recolor nested controls. Presentation and aesthetic cascade from a container;
|
|
287
|
-
putting either class directly on a component overrides the inherited choice.
|
|
288
|
-
Components apply documented clamps when an axis would remove an essential edge
|
|
289
|
-
or overwhelm compact geometry.
|
|
226
|
+
Intent stays on the component so a semantic container cannot accidentally recolor nested controls. Presentation and aesthetic cascade from a container; putting either class directly on a component overrides the inherited choice. Components apply documented clamps when an axis would remove an essential edge or overwhelm compact geometry.
|
|
290
227
|
|
|
291
|
-
The maintained contract is the intersection of the combinations documented in
|
|
292
|
-
[Usage](docs/usage/index.md) and demonstrated by the playground. Undocumented and
|
|
293
|
-
undemonstrated combinations may produce CSS but are not supported behavior.
|
|
228
|
+
The maintained contract is the intersection of the combinations documented in [Usage](docs/usage/index.md) and demonstrated by the playground. Undocumented and undemonstrated combinations may produce CSS but are not supported behavior.
|
|
294
229
|
|
|
295
|
-
- [Usage → Composing](docs/usage/composing.md) shows how the three axes combine
|
|
296
|
-
|
|
297
|
-
- [Usage → Aesthetics](docs/usage/aesthetics.md) covers each aesthetic and its
|
|
298
|
-
documented exceptions in depth.
|
|
230
|
+
- [Usage → Composing](docs/usage/composing.md) shows how the three axes combine on each component, with the full axis-support matrix.
|
|
231
|
+
- [Usage → Aesthetics](docs/usage/aesthetics.md) covers each aesthetic and its documented exceptions in depth.
|
|
299
232
|
|
|
300
233
|
## The token system
|
|
301
234
|
|
|
302
|
-
Tokens are CSS custom properties, grouped by what they control rather than
|
|
303
|
-
where they're used:
|
|
235
|
+
Tokens are CSS custom properties, grouped by what they control rather than where they're used:
|
|
304
236
|
|
|
305
237
|
| Category | Controls | Reference |
|
|
306
238
|
| ------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
@@ -311,369 +243,501 @@ where they're used:
|
|
|
311
243
|
| Material | What a component is made of — radius, border width, shadow geometry, translucency. | [Usage → Customizing](docs/usage/customizing.md#material-tokens) |
|
|
312
244
|
| Aesthetic | Per-aesthetic knobs such as `--pixel-unit` or `--tile-lift`. | [Usage → Customizing](docs/usage/customizing.md#aesthetic-tokens) |
|
|
313
245
|
|
|
314
|
-
Color and foundation tokens are the ones most apps touch first, to match a
|
|
315
|
-
brand palette or spacing scale. Intent, presentation, and material tokens are
|
|
316
|
-
lower-level: components read them, and only presentation classes, aesthetic
|
|
317
|
-
classes, or a consumer override should set them.
|
|
246
|
+
Color and foundation tokens are the ones most apps touch first, to match a brand palette or spacing scale. Intent, presentation, and material tokens are lower-level: components read them, and only presentation classes, aesthetic classes, or a consumer override should set them.
|
|
318
247
|
|
|
319
248
|
## Theme selection
|
|
320
249
|
|
|
321
|
-
The root follows the operating-system `color-scheme` preference until an
|
|
322
|
-
explicit selector overrides it.
|
|
250
|
+
The root follows the operating-system `color-scheme` preference until an explicit selector overrides it.
|
|
323
251
|
|
|
324
252
|
| Theme | Equivalent selectors |
|
|
325
253
|
| ----- | ------------------------------------------------ |
|
|
326
254
|
| Light | `.light`, `.theme-light`, `[data-theme="light"]` |
|
|
327
255
|
| Dark | `.dark`, `.theme-dark`, `[data-theme="dark"]` |
|
|
328
256
|
|
|
329
|
-
- Theme selectors set `color-scheme` and nothing else. Because `color-scheme`
|
|
330
|
-
|
|
331
|
-
-
|
|
332
|
-
|
|
333
|
-
-
|
|
334
|
-
|
|
335
|
-
-
|
|
336
|
-
themed ancestor controls that subtree at any depth.
|
|
337
|
-
- If conflicting light and dark selectors are placed on the same element, the
|
|
338
|
-
dark declaration wins because it appears later with equal specificity. Do
|
|
339
|
-
not rely on this conflict behavior; apply one theme per element.
|
|
340
|
-
- Component colors resolve from tokens or `currentColor`, so they follow the
|
|
341
|
-
theme without separate dark-variant rules, with no exception: every glyph
|
|
342
|
-
the package draws is a `currentColor` mask. Icon artwork a consumer brings
|
|
343
|
-
is theirs to theme.
|
|
344
|
-
|
|
345
|
-
Setting `color-scheme` yourself on an element also re-themes the tokens below
|
|
346
|
-
it, because that is the only signal the palette reads. This also themes native
|
|
347
|
-
UI such as scrollbars and form-control internals to match.
|
|
348
|
-
|
|
349
|
-
This mechanism works from Chrome 123, Safari 17.5, and Firefox 120; the package
|
|
350
|
-
overall requires Firefox 121 for `:has()` (see [Setup](docs/setup.md#requirements)).
|
|
257
|
+
- Theme selectors set `color-scheme` and nothing else. Because `color-scheme` inherits, a selector themes its entire subtree.
|
|
258
|
+
- With no explicit selector, `:root` uses `color-scheme: light dark`, so the system preference decides.
|
|
259
|
+
- A theme selector on `:root` overrides the system preference in either direction.
|
|
260
|
+
- A nested explicit selector overrides an inherited theme, so the nearest themed ancestor controls that subtree at any depth.
|
|
261
|
+
- If conflicting light and dark selectors are placed on the same element, the dark declaration wins because it appears later with equal specificity. Do not rely on this conflict behavior; apply one theme per element.
|
|
262
|
+
- Component colors resolve from tokens or `currentColor`, so they follow the theme without separate dark-variant rules, with no exception: every glyph the package draws is a `currentColor` mask. Icon artwork a consumer brings is theirs to theme.
|
|
263
|
+
- The Tailwind source entrypoints' `dark:` variant tracks this rule for the explicit-selector-versus-system-preference choice: it fires under an explicit dark selector, or under the system preference when the subtree carries no explicit selector at all. It does not track the previous rule the same way: that one holds because `color-scheme` inherits, so the nearest declaration always wins regardless of depth, while `dark:` matches by selector -- `.dark *` matches every descendant of a `.dark` element, with no way to stop at a nearer `.light` in between. A `.light` subtree nested inside `.dark` themes its tokens correctly but still matches `dark:`'s explicit arm. See [Setup → Dark variant](docs/setup.md#dark-variant).
|
|
351
264
|
|
|
352
|
-
|
|
265
|
+
Setting `color-scheme` yourself on an element also re-themes the tokens below it, because that is the only signal the palette reads. This also themes native UI such as scrollbars and form-control internals to match.
|
|
353
266
|
|
|
354
|
-
#
|
|
267
|
+
This mechanism works from Chrome 123, Safari 17.5, and Firefox 120; the package overall requires Firefox 121 for `:has()` (see [Setup](docs/setup.md#requirements)).
|
|
355
268
|
|
|
356
|
-
|
|
357
|
-
uses that layout loads it once:
|
|
269
|
+
<!-- Source: docs/usage/index.md -->
|
|
358
270
|
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
// src/layouts/Layout.astro
|
|
362
|
-
import "@codenhub/styles";
|
|
363
|
-
---
|
|
271
|
+
# Usage
|
|
364
272
|
|
|
365
|
-
|
|
366
|
-
<body>
|
|
367
|
-
<slot />
|
|
368
|
-
</body>
|
|
369
|
-
</html>
|
|
370
|
-
```
|
|
273
|
+
These guides assume the [Concepts](docs/concepts.md) mental model: intent, presentation, and aesthetic as three independent axes, resolved through tokens a component reads.
|
|
371
274
|
|
|
372
|
-
|
|
373
|
-
|
|
275
|
+
- [Composing](docs/usage/composing.md): Combining the three axes on a component, the axis-support matrix, and elevation modifiers.
|
|
276
|
+
- [Theming](docs/usage/theming.md): The color, intent, and foundation token reference.
|
|
277
|
+
- [Customizing](docs/usage/customizing.md): The presentation and material token reference, aesthetic knobs, and defining a custom intent.
|
|
278
|
+
- [Aesthetics](docs/usage/aesthetics.md): Each shipped aesthetic in depth, including its documented exceptions.
|
|
279
|
+
- [Buttons](docs/usage/buttons.md)
|
|
280
|
+
- [Forms](docs/usage/forms.md)
|
|
281
|
+
- [Feedback](docs/usage/feedback.md)
|
|
282
|
+
- [Tooltips](docs/usage/tooltips.md)
|
|
283
|
+
- [Content and layout](docs/usage/content-and-layout.md): Layout helpers, tables, quotes, code, surfaces, and typography utilities.
|
|
374
284
|
|
|
375
|
-
|
|
376
|
-
Astro's Tailwind integration instead; see [Tailwind CSS v4](docs/integrating/tailwind.md)
|
|
377
|
-
for the `@source` directive Astro needs to scan `src/`.
|
|
285
|
+
## Supported surface
|
|
378
286
|
|
|
379
|
-
|
|
287
|
+
A class, component, and axis combination is supported when this documentation describes it and the package playground demonstrates it. Undocumented and undemonstrated combinations may still produce CSS, but are not maintained behavior. Playground variant matrices are the exhaustive demonstrations; small examples elsewhere illustrate usage without widening that contract.
|
|
380
288
|
|
|
381
|
-
|
|
289
|
+
<!-- Source: docs/usage/composing.md -->
|
|
382
290
|
|
|
383
|
-
|
|
384
|
-
[Introduction](docs/index.md#what-it-does-not-do). Integrating it into a
|
|
385
|
-
framework is entirely about where that framework loads a global stylesheet,
|
|
386
|
-
and whether the project runs Tailwind CSS v4.
|
|
291
|
+
# Composing the three axes
|
|
387
292
|
|
|
388
|
-
|
|
293
|
+
See [Concepts](docs/concepts.md#the-three-axes) for what each axis means. This page covers how they combine in markup, what the presentation classes mean in depth, which components support which axis, and elevation.
|
|
389
294
|
|
|
390
|
-
|
|
391
|
-
`@codenhub/styles` for any build, or `@codenhub/styles/tw` when the
|
|
392
|
-
project already runs Tailwind CSS v4 — from wherever that framework loads
|
|
393
|
-
global CSS.
|
|
394
|
-
2. Import `@codenhub/styles/aesthetics` (or `/tw/aesthetics`) after it, only
|
|
395
|
-
if the project uses an [aesthetic](docs/usage/aesthetics.md).
|
|
295
|
+
## Combining axes
|
|
396
296
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
297
|
+
```html
|
|
298
|
+
<div class="soft">
|
|
299
|
+
<button class="btn primary">Soft primary button</button>
|
|
300
|
+
<span class="badge success">Soft success badge</span>
|
|
301
|
+
<button class="btn primary ghost edged">Outlined; the element wins</button>
|
|
302
|
+
</div>
|
|
303
|
+
```
|
|
404
304
|
|
|
405
|
-
A
|
|
406
|
-
nothing beyond [Setup](docs/setup.md#quick-start): import the compiled
|
|
407
|
-
stylesheet from the project's existing global CSS entry.
|
|
305
|
+
A declaration on the element always beats one inherited from a container, so a container sets the default look for its subtree and any element opts out.
|
|
408
306
|
|
|
409
|
-
|
|
307
|
+
Intent classes do not cascade. `.primary` and its siblings stay on the element that shows the intent, because a container silently recoloring every descendant is a trap rather than a feature — see [Theming → Intent tokens](docs/usage/theming.md#intent-tokens). Use `.neutral` to opt an element back out of an inherited intent context. Presentation and [material tokens](docs/usage/customizing.md#material-tokens) do cascade, so a container can set the look of its whole subtree while any element overrides it.
|
|
410
308
|
|
|
411
|
-
|
|
309
|
+
## Presentation
|
|
412
310
|
|
|
413
|
-
|
|
414
|
-
package from the root layout in the App Router, or from the custom App in
|
|
415
|
-
the Pages Router — not from a page or component further down the tree.
|
|
311
|
+
Presentation has two independent axes. `.solid`, `.soft`, and `.ghost` decide how much intent fills a component; `.edged` and `.edgeless` decide whether it draws a boundary. They set [presentation tokens](docs/usage/customizing.md#presentation-tokens) and nothing else, so they work on the component itself or on any ancestor.
|
|
416
312
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
313
|
+
| Class | Look |
|
|
314
|
+
| ----------- | ---------------------------------------------------------- |
|
|
315
|
+
| `.solid` | Intent-colored fill with contrast text. |
|
|
316
|
+
| `.soft` | Lightly tinted fill with intent-colored text. |
|
|
317
|
+
| `.ghost` | No fill at rest, with intent-colored text. |
|
|
318
|
+
| `.edged` | Intent-colored boundary at the aesthetic's material width. |
|
|
319
|
+
| `.edgeless` | No boundary. |
|
|
420
320
|
|
|
421
|
-
|
|
422
|
-
return (
|
|
423
|
-
<html lang="en">
|
|
424
|
-
<body>{children}</body>
|
|
425
|
-
</html>
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
```
|
|
321
|
+
Values each class ships:
|
|
429
322
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
323
|
+
| Class | `--ui-fill` | `--ui-fg-on-fill` | `--ui-border` |
|
|
324
|
+
| ----------- | ----------- | ----------------- | ------------- |
|
|
325
|
+
| `.solid` | `100%` | `100%` | |
|
|
326
|
+
| `.soft` | `12%` | `0%` | |
|
|
327
|
+
| `.ghost` | `0%` | `0%` | |
|
|
328
|
+
| `.edged` | | | `100%` |
|
|
329
|
+
| `.edgeless` | | | `0%` |
|
|
434
330
|
|
|
435
|
-
|
|
436
|
-
return <Component {...pageProps} />;
|
|
437
|
-
}
|
|
438
|
-
```
|
|
331
|
+
`.card`, `.panel`, `.alert`, `.kbd`, `.code`, and `.pre` override `.soft`'s fill at rest to `0%` for the no-named-intent case only, and reveal it on `.card`'s own `:hover`/`.interactive`/`.hoverable` -- the one place a component remaps what a presentation class resolves to for part of its intent range, the same way text controls cap it at `6%` cascaded. A named intent on any of the six keeps the ordinary `12%`. `.badge` reads the identical intent/presentation classes but keeps its 12% wash at rest even with no named intent, since it has no border to fall back on and would otherwise disappear rather than read quieter. See [Content and layout → Surfaces](docs/usage/content-and-layout.md#surfaces) and [Feedback](docs/usage/feedback.md).
|
|
439
332
|
|
|
440
|
-
|
|
441
|
-
the system preference; see [Setup → Configuration](docs/setup.md#configuration).
|
|
333
|
+
The combinations mean exactly what they spell:
|
|
442
334
|
|
|
443
|
-
```
|
|
444
|
-
<
|
|
335
|
+
```html
|
|
336
|
+
<button class="btn primary">Solid, the button default</button>
|
|
337
|
+
<button class="btn soft edged">Tinted with a border</button>
|
|
338
|
+
<button class="btn primary ghost edged">Outline</button>
|
|
339
|
+
<button class="btn ghost">Ghost; picks up a tint on hover</button>
|
|
340
|
+
<input class="ipt soft edgeless" />
|
|
341
|
+
<!-- A field sunk into the page -->
|
|
445
342
|
```
|
|
446
343
|
|
|
447
|
-
A
|
|
448
|
-
same file instead; see [Tailwind CSS v4](docs/integrating/tailwind.md) for the `@source`
|
|
449
|
-
directive Next.js needs to scan its own `app`/`src` directories.
|
|
450
|
-
|
|
451
|
-
<!-- Source: docs/integrating/svelte.md -->
|
|
344
|
+
A fill class never decides an edge and an edge class never decides a fill. Use `.solid.edgeless` for a filled box with no line — and prefer it on a neutral one, because a capped fill leaves the box translucent and the edge blend then paints a second coat of the same tint rather than disappearing into it (measured 1.53:1 against its own plate in light, 1.82:1 in dark).
|
|
452
345
|
|
|
453
|
-
#
|
|
346
|
+
The edge is the silhouette and nothing else. Rules _inside_ a component that has an inside are a separate switch — see [Content and layout → Table rules](docs/usage/content-and-layout.md#table-rules) — because tying them to this axis made them arrive by implication rather than by request.
|
|
454
347
|
|
|
455
|
-
|
|
456
|
-
every route:
|
|
348
|
+
With no presentation class in scope, each component keeps its own default: buttons are filled, alerts and badges are tinted, surfaces and controls are neutral and bordered.
|
|
457
349
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
350
|
+
| Component | Reads |
|
|
351
|
+
| ----------------------------------------------- | ------------------------------------------- |
|
|
352
|
+
| `.btn`, `.alert`, `.badge`, `.card`, `.panel` | Fill, text, border, and border width. |
|
|
353
|
+
| `.kbd` | Fill, text, border, and border width. |
|
|
354
|
+
| `.data-table` | Header fill and text, border, border width. |
|
|
355
|
+
| `.ipt`, `.textarea`, `.select`, `.text-control` | Border, border width, and a capped fill. |
|
|
356
|
+
| `.input-group` | Border, border width, and a capped fill. |
|
|
357
|
+
| `.checkbox`, `.radio` | A capped fill, checked and unchecked. |
|
|
358
|
+
| `.switch` | Border, border width, and a capped fill. |
|
|
359
|
+
| `.quote` | Fill, text, border, and width. |
|
|
360
|
+
| `.code`, `.pre`, `.tooltip-bubble` | Fill, text, border, and material. |
|
|
361
|
+
| `.loader`, `.skeleton`, `.progress`, `.divider` | Intent only, not presentation. |
|
|
464
362
|
|
|
465
|
-
|
|
466
|
-
```
|
|
363
|
+
A text control caps a **cascaded** fill at `6%`, keeping typed text legible when a `.solid` container reaches a field nobody classed. A fill class written on the control itself names its own cap instead, because that is a consumer describing what they want: `.soft` takes `12%` and `.solid` takes `20%`.
|
|
467
364
|
|
|
468
|
-
|
|
365
|
+
| On a text control | fill |
|
|
366
|
+
| ----------------- | ---- |
|
|
367
|
+
| `.ghost` | 0% |
|
|
368
|
+
| cascaded `.solid` | 6% |
|
|
369
|
+
| `.soft` | 12% |
|
|
370
|
+
| `.solid` | 20% |
|
|
469
371
|
|
|
470
|
-
|
|
471
|
-
// main.ts
|
|
472
|
-
import "@codenhub/styles";
|
|
473
|
-
import { mount } from "svelte";
|
|
474
|
-
import App from "./App.svelte";
|
|
372
|
+
A toggle's cap does not have that split: it is `40%` whether the presentation class is on the checkbox/radio/switch itself or cascaded from a container, the same value for all three. That is where an unchecked `.ghost` (0%), `.soft`/default (12%), and `.solid` (the cap) read as three distinct plates instead of shades of the same wash. Being checked pins the fill to `100%` instead, whatever the presentation class asks — a checkbox, a radio, and a switch each have one "on" look, and presentation only shapes them unchecked. See [Forms](docs/usage/forms.md) for the full toggle reference.
|
|
475
373
|
|
|
476
|
-
|
|
477
|
-
```
|
|
374
|
+
The same split governs the edge: a cascaded `.edgeless` is floored and the element's own is honoured — except on `.checkbox` and `.radio`, which never drop their line at all, and on `.switch.ghost`, which keeps its line specifically against its own `.edgeless`: `.ghost` already asks for zero fill, so removing the line too would leave nothing marking the track but the knob. `.switch.ghost.edgeless` renders the same as `.switch.ghost.edged` rather than as a distinct, boundary-less look. See [Forms](docs/usage/forms.md) for the full toggle and text control reference.
|
|
478
375
|
|
|
479
|
-
|
|
480
|
-
force a theme instead of following the system preference; see
|
|
481
|
-
[Setup → Configuration](docs/setup.md#configuration).
|
|
376
|
+
A tooltip bubble is filled, over a tinted ground. An intent fills it with that intent's own color: `.tooltip-bubble.primary` is the primary color, black on a light page and white on a dark one, and `.tooltip-bubble.destructive` is a red bubble.
|
|
482
377
|
|
|
483
|
-
|
|
484
|
-
app's CSS entry instead; see [Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
378
|
+
With no intent the bubble is `--color-tooltip`, the one plate in the package chosen per theme rather than derived from an intent. Composed like everything else it was `20%` of the page's ink over the surface tone, which steps _lighter_ than the surface on a dark page and _darker_ on a light one — so the dark bubble read as lifted and the light one as a mid-grey slab two steps darker than every card on the screen. The token states each direction instead: near-white over a light page, a mid grey over a dark one. `--color-tooltip-contrast` is its ink.
|
|
485
379
|
|
|
486
|
-
|
|
380
|
+
Near-white is only 1.04:1 against a light page, so the bubble draws a hairline as well, and floors it: a container's `.solid` or `.edgeless` cannot take the boundary away from a bubble nobody classed, while `.tooltip-bubble.edgeless` still can. The ground keeps the bubble opaque at every fill, so a cascaded presentation leaves it readable over whatever it floats above. It reads the material tokens too, so an [aesthetic](docs/usage/aesthetics.md) in scope shapes the bubble like any other component. See [Tooltips](docs/usage/tooltips.md) for the full tooltip reference.
|
|
487
381
|
|
|
488
|
-
|
|
382
|
+
## Component axis reference
|
|
489
383
|
|
|
490
|
-
|
|
491
|
-
source instead of its compiled output. `/tw` entrypoints publish copied,
|
|
492
|
-
uncompiled source from `dist/tw`; your own Tailwind build compiles it
|
|
493
|
-
alongside your app, so its `@theme`, `@utility`, and `@apply` directives
|
|
494
|
-
resolve together with yours.
|
|
384
|
+
This table is the authoritative axis map for demonstrated components. "Shape" means the component reads applicable radius, border-width, shadow, hover, or clip/edge material tokens; an aesthetic may intentionally affect only the materials that component draws.
|
|
495
385
|
|
|
496
|
-
|
|
497
|
-
@import "tailwindcss";
|
|
498
|
-
@import "@codenhub/styles/tw";
|
|
386
|
+
Presentation is two independent axes, so they get a column each. A `Yes` means the class changes what the component computes, which `tests/browser/axes.spec.ts` asserts against `registry.json` in both directions: an axis marked `Yes` must move, and an axis marked `No` must not.
|
|
499
387
|
|
|
500
|
-
|
|
501
|
-
|
|
388
|
+
| Components | Intent | Fill | Edge | Aesthetic material |
|
|
389
|
+
| --------------------------------------------------------- | ------ | ---- | ---- | ------------------ |
|
|
390
|
+
| `.btn`, `.alert`, `.badge`, `.card`, `.panel` | Yes | Yes | Yes | Shape |
|
|
391
|
+
| `.loader` | Yes | No | No | No |
|
|
392
|
+
| `.kbd`, `.data-table` | Yes | Yes | Yes | Radius and edge |
|
|
393
|
+
| `.quote` | Yes | Yes | Yes | Edge width |
|
|
394
|
+
| `.divider`, `.skeleton` | Yes | No | No | No |
|
|
395
|
+
| `.ipt`, `.textarea`, `.select`, `.text-control` | Yes | Yes | Yes | Shape |
|
|
396
|
+
| `.input-group` | Yes | Yes | Yes | Shape |
|
|
397
|
+
| `.surface` | Yes | Yes | Yes | Shape |
|
|
398
|
+
| `.checkbox`, `.radio` | Yes | Yes | No | Radius and edge |
|
|
399
|
+
| `.switch` | Yes | Yes | Yes | Radius and edge |
|
|
400
|
+
| `.progress` | Yes | No | No | No |
|
|
401
|
+
| `.code`, `.pre` | Yes | Yes | Yes | Radius and clip |
|
|
402
|
+
| `.tooltip-bubble` | Yes | Yes | Yes | Shape and overlay |
|
|
403
|
+
| `.table-wrap`, `.quote-inline`, layout and text utilities | No | No | No | No |
|
|
502
404
|
|
|
503
|
-
|
|
504
|
-
class names actually used in your markup, so it can generate only what is
|
|
505
|
-
referenced. Point it at your app's source, not at this package — `/tw`
|
|
506
|
-
already ships every class definition it publishes.
|
|
405
|
+
`.checkbox` and `.radio` are the only components that read one axis and not the other. Their line is the only thing marking an unchecked box, so it is drawn whatever the edge class says, from a container or from the element itself. `.edgeless` on either is unsupported rather than merely discouraged.
|
|
507
406
|
|
|
508
|
-
|
|
509
|
-
after the base import:
|
|
407
|
+
Every other text control floors a _cascaded_ `.edgeless` and honours its own: a `.edgeless` toolbar will not erase the line of a field nobody classed, while `.ipt.edgeless` is a consumer describing what they want and gets it.
|
|
510
408
|
|
|
511
|
-
|
|
512
|
-
@import "tailwindcss";
|
|
513
|
-
@import "@codenhub/styles/tw";
|
|
514
|
-
@import "@codenhub/styles/tw/aesthetics";
|
|
409
|
+
Intent aliases such as `.danger` and `.error` occupy the same intent axis as `.destructive`; they do not add component behavior. State and modifier classes such as `.interactive`, `.hoverable`, `.pressable`, `.invalid`, `.compact`, and `.vertical` sit above this map and are documented with their component.
|
|
515
410
|
|
|
516
|
-
|
|
411
|
+
## Elevation
|
|
412
|
+
|
|
413
|
+
Depth is a modifier, not an axis. Nothing is raised until an [aesthetic](docs/usage/aesthetics.md) draws depth or one of these classes asks for it.
|
|
414
|
+
|
|
415
|
+
| Class | Alias | Depth |
|
|
416
|
+
| ----------------- | ----------- | ------------------------------------------------------------------------ |
|
|
417
|
+
| `.elevation-none` | `.flat` | Removes part-based elevation; glass surfaces keep their complete shadow. |
|
|
418
|
+
| `.elevation-sm` | `.raised` | One unit of the depth in scope. Cards, tiles, popovers that sit close. |
|
|
419
|
+
| `.elevation-md` | `.floating` | Twice it, for menus and popovers. |
|
|
420
|
+
|
|
421
|
+
Both names in a row are equivalent, interchangeable classes — `.elevation-sm` and `.raised` are aliases, not a canonical form and a deprecated one, the way `.destructive` and `.danger` are on the intent axis. The `none`/`sm`/`md` naming matches the raw `--elevation-*` tokens in [Theming](docs/usage/theming.md#foundation-tokens) for the same weight of depth, even though the two are read from different places.
|
|
422
|
+
|
|
423
|
+
Each is one unitless multiplier over whatever shadow geometry is in scope, so the same class reads as a soft blur on a plain page and as a hard offset slab under `.neobrutalism` — the aesthetic decides what depth looks like, the class decides how much of it this element takes.
|
|
424
|
+
|
|
425
|
+
```html
|
|
426
|
+
<article class="card elevation-sm">Lifted</article>
|
|
427
|
+
<div class="elevation-md panel">A menu surface</div>
|
|
428
|
+
<article class="card elevation-none">Flat, whatever the page or container says</article>
|
|
517
429
|
```
|
|
518
430
|
|
|
519
|
-
|
|
520
|
-
`/tw/button` or `/tw/form` — when a build only needs one component family;
|
|
521
|
-
see the full [import path table](docs/setup.md#import-paths) for what each one
|
|
522
|
-
composes. Tailwind CSS 4 or newer is required for every `/tw` entrypoint.
|
|
431
|
+
The multiplier does not inherit: an elevation class lifts or flattens the element it is written on only, and an unclassed element inside it takes no depth of its own. Give each element that needs depth its own elevation class. Spread is deliberately left out of the multiplication, because an aesthetic that draws its edge as an inset ring spends spread on it and scaling that would erase the edge.
|
|
523
432
|
|
|
524
|
-
<!-- Source: docs/
|
|
433
|
+
<!-- Source: docs/usage/theming.md -->
|
|
525
434
|
|
|
526
|
-
#
|
|
435
|
+
# Theming
|
|
527
436
|
|
|
528
|
-
|
|
437
|
+
Color tokens are how you restyle the palette; see [Concepts → Theme selection](docs/concepts.md#theme-selection) for how a theme is chosen. Intent tokens are how a component reads that palette once an intent class is applied. Foundation tokens are the shared layout, shape, and motion defaults underneath everything.
|
|
529
438
|
|
|
530
|
-
|
|
531
|
-
// main.ts
|
|
532
|
-
import "@codenhub/styles";
|
|
533
|
-
import { createApp } from "vue";
|
|
534
|
-
import App from "./App.vue";
|
|
439
|
+
## Color tokens
|
|
535
440
|
|
|
536
|
-
|
|
441
|
+
Each color token holds both of its theme values at once through `light-dark()`, and the element's `color-scheme` picks one.
|
|
442
|
+
|
|
443
|
+
Every row is an override point: set it on `:root` to retheme globally, or on any element to retheme that subtree. The `-hover`, `-subtle`, `-strong`, and `-contrast` rows are companion tones wired into the intent slots, so overriding a base color such as `--color-primary` on its own does not shift its companions — retheme the whole family. The `text` family has no `-subtle` row: the neutral intent draws its subtle tone from `--color-surface`.
|
|
444
|
+
|
|
445
|
+
```html
|
|
446
|
+
<section data-theme="dark">
|
|
447
|
+
<article>Dark token subtree</article>
|
|
448
|
+
</section>
|
|
537
449
|
```
|
|
538
450
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
451
|
+
| Token | Purpose |
|
|
452
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
453
|
+
| `--color-primary` | Primary action/content color. |
|
|
454
|
+
| `--color-primary-contrast` | Text/icon color on primary filled surface. |
|
|
455
|
+
| `--color-primary-hover` | Primary hover state. |
|
|
456
|
+
| `--color-primary-subtle` | Low-emphasis companion tone for primary. Use as soft surface or subtle background. |
|
|
457
|
+
| `--color-primary-strong` | High-emphasis companion tone for primary. Use as readable text, icon, or border on subtle surfaces. |
|
|
458
|
+
| `--color-accent` | Secondary/accent surface. |
|
|
459
|
+
| `--color-accent-contrast` | Text/icon color on accent filled surface. |
|
|
460
|
+
| `--color-accent-hover` | Accent hover state. |
|
|
461
|
+
| `--color-accent-subtle` | Low-emphasis companion tone for accent. Use as soft surface or subtle background. |
|
|
462
|
+
| `--color-accent-strong` | High-emphasis companion tone for accent. Use as readable text, icon, or border on subtle surfaces. |
|
|
463
|
+
| `--color-border` | Default border color. |
|
|
464
|
+
| `--color-border-hover` | Interactive border color. |
|
|
465
|
+
| `--color-control-border` | Resting line on the six text controls; one value, drawn at a fraction on text inputs. |
|
|
466
|
+
| `--color-background` | Page and default raised surface background. |
|
|
467
|
+
| `--color-foreground` | Subtle raised foreground surface. |
|
|
468
|
+
| `--color-surface` | Nested or muted surface. |
|
|
469
|
+
| `--color-text` | Primary text color. |
|
|
470
|
+
| `--color-text-secondary` | Secondary text color. |
|
|
471
|
+
| `--color-text-contrast` | Readable color on top of a full fill of `--color-text`. A checked toggle prints it. |
|
|
472
|
+
| `--color-text-hover` | Text hover state. |
|
|
473
|
+
| `--color-text-strong` | High-emphasis companion tone for text. Use as readable text, icon, or border on subtle surfaces. |
|
|
474
|
+
| `--color-tooltip` | Tooltip bubble plate with no intent. Chosen per theme rather than derived: near-white in light, mid grey in dark. |
|
|
475
|
+
| `--color-tooltip-contrast` | Ink on `--color-tooltip`. |
|
|
476
|
+
| `--color-success` | Success state color. |
|
|
477
|
+
| `--color-success-contrast` | Text/icon color on success filled surface. Meets 4.5:1 as normal text on `--color-success` (5.14:1). |
|
|
478
|
+
| `--color-success-hover` | Success hover tone. |
|
|
479
|
+
| `--color-success-subtle` | Low-emphasis companion tone for success. |
|
|
480
|
+
| `--color-success-strong` | High-emphasis companion tone for success. |
|
|
481
|
+
| `--color-warning` | Warning state color. |
|
|
482
|
+
| `--color-warning-contrast` | The near-black tone, not the page tone. Meets 4.5:1 as normal text on `--color-warning` (6.19:1). |
|
|
483
|
+
| `--color-warning-hover` | Warning hover tone. |
|
|
484
|
+
| `--color-warning-subtle` | Low-emphasis companion tone for warning. |
|
|
485
|
+
| `--color-warning-strong` | High-emphasis companion tone for warning. |
|
|
486
|
+
| `--color-destructive` | Destructive/error state color. |
|
|
487
|
+
| `--color-destructive-contrast` | Text/icon color on destructive filled surface. Meets 4.5:1 as normal text on `--color-destructive` (5.78:1). |
|
|
488
|
+
| `--color-destructive-hover` | Destructive hover tone. |
|
|
489
|
+
| `--color-destructive-subtle` | Low-emphasis companion tone for destructive. |
|
|
490
|
+
| `--color-destructive-strong` | High-emphasis companion tone for destructive. |
|
|
491
|
+
| `--color-info` | Informational state color. |
|
|
492
|
+
| `--color-info-contrast` | Text/icon color on info filled surface. Meets 4.5:1 as normal text on `--color-info` (6.19:1). |
|
|
493
|
+
| `--color-info-hover` | Info hover tone. |
|
|
494
|
+
| `--color-info-subtle` | Low-emphasis companion tone for info. |
|
|
495
|
+
| `--color-info-strong` | High-emphasis companion tone for info. |
|
|
542
496
|
|
|
543
|
-
|
|
544
|
-
entry file instead of `main.ts`, using the `@tailwindcss/vite` plugin; see
|
|
545
|
-
[Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
497
|
+
> **Intent token contract**: color intent tokens own meaning and tone variants. Intent classes map one family onto the shared intent slots below, and presentation classes decide how much of it a component shows. Theme changes belong in token values, not broad component-level theme checks. Component-level theme handling should exist only when a component has an internal structure that cannot be expressed through the token palette alone.
|
|
546
498
|
|
|
547
|
-
|
|
499
|
+
## Intent tokens
|
|
548
500
|
|
|
549
|
-
|
|
501
|
+
Intent classes set these seven slots; components read them. Because every supporting component reads the same seven names, an intent works on every component that supports intent, and a custom intent needs no component changes.
|
|
550
502
|
|
|
551
|
-
|
|
503
|
+
| Token | Purpose |
|
|
504
|
+
| ------------------- | ------------------------------------------------------------------------------------- |
|
|
505
|
+
| `--intent-color` | The intent's base color. What a fill is made of. |
|
|
506
|
+
| `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
|
|
507
|
+
| `--intent-hover` | The intent's hovered base color. |
|
|
508
|
+
| `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
|
|
509
|
+
| `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
|
|
510
|
+
| `--intent-border` | Line color. Resolves to `--color-border` when no intent is set. |
|
|
511
|
+
| `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
|
|
552
512
|
|
|
553
|
-
|
|
554
|
-
pnpm add @codenhub/styles
|
|
555
|
-
```
|
|
513
|
+
`--intent-color` and `--intent-strong` are two ends of the same family and not interchangeable. The base is a ground, chosen to be filled with; the strong tone is chosen to be read against a page. A `.soft` or `.ghost` component prints the strong one, which is why an unfilled warning reads in amber-800 rather than the amber-600 its solid sibling is filled with.
|
|
556
514
|
|
|
557
|
-
|
|
515
|
+
The strong tone has to stay recognisably its own hue, not just readable. On a dark page the `-100` shade of every family is a pale tint that carries almost no chroma at small sizes: a soft success badge and a soft destructive one printed labels that both read white, and a soft radio's dot stopped matching the ring around it. The dark tones are `-300` for that reason, and the tinted grounds under them are `-900` so there is room — measured at 5.0:1 or better everywhere a partial fill prints text, and past 9:1 on the page.
|
|
558
516
|
|
|
559
|
-
|
|
560
|
-
|
|
517
|
+
`--intent-border` exists because a neutral border must stay the quiet border gray rather than the text color. Everything that draws a line reads it: control borders, table rules, dividers, key caps, and quote bars.
|
|
518
|
+
|
|
519
|
+
`--intent-fill-max` exists for the neutral intent, whose color is the page's own ink. Every other intent is a hue, legible pale at 12% and saturated at 100%; a grey filled to 100% is a black or white slab, which is the loudest thing on the page and the wrong look for the intent that means _nothing in particular_. Neutral stops at `20%`, so `.solid` gives it a quiet plate that still reads as a step past `.soft`. Hover adds its step on top of the cap, so a capped fill still deepens under the pointer.
|
|
520
|
+
|
|
521
|
+
The foreground is gated by the plate rather than tied to the fill. `--intent-contrast` is the ink for a _full_ fill, so printing it at full strength on a fifth of one puts white on light grey. Contrast ink therefore appears only once the fill is past halfway and reaches full at a full fill; below that a component prints `--intent-strong`, the tone chosen to be read.
|
|
522
|
+
|
|
523
|
+
That means a capped neutral takes the strong tone whole rather than a fraction of the way toward the page. Tying it to the fill instead cost a neutral `.solid` button four points of contrast and a neutral tooltip nearly ten. A state that lifts the cap on purpose — a checked checkbox is filled with its own intent by definition — reaches a full fill and gets the contrast whole.
|
|
524
|
+
|
|
525
|
+
Set them directly to define an intent this package does not ship. **State every slot**, including `--intent-fill-max`: components reset all seven to the neutral values, so a slot your intent leaves out keeps neutral's — and an intent that omits the cap fills to 20% of its color instead of 100%.
|
|
561
526
|
|
|
562
527
|
```css
|
|
563
|
-
|
|
528
|
+
.brand {
|
|
529
|
+
--intent-color: var(--color-violet-600);
|
|
530
|
+
--intent-contrast: var(--color-neutral-50);
|
|
531
|
+
--intent-hover: var(--color-violet-700);
|
|
532
|
+
--intent-strong: var(--color-violet-800);
|
|
533
|
+
--intent-subtle: var(--color-violet-100);
|
|
534
|
+
--intent-border: var(--color-violet-600);
|
|
535
|
+
--intent-fill-max: 100%;
|
|
536
|
+
}
|
|
564
537
|
```
|
|
565
538
|
|
|
566
539
|
```html
|
|
567
|
-
<
|
|
568
|
-
<p class="text-label">Status</p>
|
|
569
|
-
<h1 class="text-title">Ready to publish</h1>
|
|
570
|
-
<button class="btn primary">Continue</button>
|
|
571
|
-
</main>
|
|
540
|
+
<button class="btn brand">Custom intent</button> <span class="badge brand soft">Works everywhere</span>
|
|
572
541
|
```
|
|
573
542
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
543
|
+
> **Intent does not cascade.** Components reset these slots to the text palette at their own root, so a `.success` container never recolors a nested destructive button. Put the intent class on the element that shows the intent.
|
|
544
|
+
|
|
545
|
+
## Foundation tokens
|
|
546
|
+
|
|
547
|
+
Foundation tokens are not aliases for one color. They define layout, shape, motion, focus, depth, and layering behavior used by helper classes.
|
|
548
|
+
|
|
549
|
+
The four `--elevation-*` values below are raw shadows for elements you style yourself. They are a separate thing from `.elevation-none`/`.elevation-sm`/ `.elevation-md` (aliased as `.flat`/`.raised`/`.floating`), which scale a _component's own_ shadow geometry and default to no shadow at all until an aesthetic or one of those classes asks for depth — see [Composing → Elevation](docs/usage/composing.md#elevation) for that system. The two share `none`/`sm`/`md`/`lg` naming for the same weight of depth, but one is a fixed value and the other multiplies shadow parts, so they will not always paint identically.
|
|
550
|
+
|
|
551
|
+
| Token | Purpose |
|
|
552
|
+
| -------------------------- | ----------------------------------------------------------------------- |
|
|
553
|
+
| `--font-default` | Default app font family. |
|
|
554
|
+
| `--container-narrow` | Narrow content width. |
|
|
555
|
+
| `--container-max` | Default content width. |
|
|
556
|
+
| `--container-wide` | Wide content width. |
|
|
557
|
+
| `--layout-gutter` | Responsive inline page padding. |
|
|
558
|
+
| `--layout-section-block` | Responsive section vertical padding. |
|
|
559
|
+
| `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
|
|
560
|
+
| `--layout-grid-min` | Minimum column width for `.auto-grid`. |
|
|
561
|
+
| `--radius-small` | Small radius for compact utility surfaces such as code elements. |
|
|
562
|
+
| `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
|
|
563
|
+
| `--radius-surface` | Radius for cards, panels, and alerts. |
|
|
564
|
+
| `--control-height` | Default minimum height for controls. |
|
|
565
|
+
| `--border-width` | Default border width. |
|
|
566
|
+
| `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
|
|
567
|
+
| `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
|
|
568
|
+
| `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
|
|
569
|
+
| `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
|
|
570
|
+
| `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
|
|
571
|
+
| `--focus-ring` | Focus-visible ring color. |
|
|
572
|
+
| `--focus-ring-offset` | Focus-visible outline offset. Negative, so the ring lands on the edge. |
|
|
573
|
+
| `--focus-ring-width` | Focus-visible outline/ring width. |
|
|
574
|
+
| `--motion-duration-fast` | Fast transition for buttons and input interactions (`120ms`). |
|
|
575
|
+
| `--motion-duration-normal` | Normal transition for layout and modals (`200ms`). |
|
|
576
|
+
| `--motion-duration-slow` | Slow animation for skeleton loaders and progress bars (`400ms`). |
|
|
577
|
+
| `--motion-ease` | Default easing curve. |
|
|
578
|
+
| `--z-popover` | Popover/tooltip z-index. |
|
|
579
|
+
| `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
|
|
580
|
+
| `--breakpoint-2xl` | Extended large Tailwind responsive breakpoint. |
|
|
581
|
+
|
|
582
|
+
Next: [Customizing](docs/usage/customizing.md) covers presentation and material tokens — how much of an intent shows, and what a component is made of.
|
|
583
|
+
|
|
584
|
+
<!-- Source: docs/usage/customizing.md -->
|
|
585
|
+
|
|
586
|
+
# Customizing
|
|
587
|
+
|
|
588
|
+
Presentation tokens decide how much of an intent shows. Material tokens decide what a component is made of. Aesthetic tokens are the small set of named knobs a shipped aesthetic exposes to scale its own look.
|
|
589
|
+
|
|
590
|
+
## Presentation tokens
|
|
578
591
|
|
|
579
|
-
|
|
580
|
-
|
|
592
|
+
Presentation tokens describe _how much_ of an intent a component shows, never _which_ intent it shows. They are unitless numbers and percentages, so they carry no color and inherit safely: a container sets them once and every component below resolves them against its own intent.
|
|
593
|
+
|
|
594
|
+
| Token | Purpose |
|
|
595
|
+
| ----------------- | -------------------------------------------------------------------------- |
|
|
596
|
+
| `--ui-fill` | Percentage of the intent color mixed into the background. |
|
|
597
|
+
| `--ui-fg-on-fill` | Percentage blended from the intent readable tone toward its contrast tone. |
|
|
598
|
+
| `--ui-border` | Percentage of the intent color mixed into the border color. |
|
|
599
|
+
|
|
600
|
+
A component resolves them with `color-mix` from its published default. Hover is derived by adding `--hover-step` to the resting fill and using `--intent-hover`; there are no per-presentation hover tokens.
|
|
601
|
+
|
|
602
|
+
No fill class writes `--ui-border` and no edge class writes `--ui-fill`. A filled box with no line is `.solid.edgeless`, which is worth reaching for on a neutral component: neutral caps its fill, so the box stays translucent and a border over it paints a second coat of the same tint instead of blending into it.
|
|
581
603
|
|
|
582
604
|
```css
|
|
583
|
-
|
|
584
|
-
@import "@codenhub/styles/aesthetics";
|
|
605
|
+
background: color-mix(in oklab, var(--color-primary) var(--ui-fill, 100%), transparent);
|
|
585
606
|
```
|
|
586
607
|
|
|
608
|
+
Two choices there are deliberate. Mixing toward `transparent` rather than a background token lets a tinted surface adapt to whatever it is placed on. Mixing in `oklab` keeps the wide-gamut token palette intact and blends perceptually, where `srgb` would clip it.
|
|
609
|
+
|
|
610
|
+
A computed color therefore reaches the page as a resolved `color-mix` result rather than the token's own `oklch` syntax. It is the same color; code that compares computed color strings should compare colors instead.
|
|
611
|
+
|
|
612
|
+
> **Presentation token contract**: only presentation classes and consumer overrides set these tokens; components only read them. Setting a value that no presentation class produces is supported but unvalidated. See [Composing](docs/usage/composing.md) for the classes that ship these values.
|
|
613
|
+
|
|
614
|
+
## Material tokens
|
|
615
|
+
|
|
616
|
+
Material tokens describe what a component is _made of_ rather than which intent it shows or how strongly. They control shape, borders, shadows, translucency, filters, and transforms.
|
|
617
|
+
|
|
618
|
+
Components read each with a fallback, so leaving them unset produces the default look. Setting them on a container restyles the whole subtree. Only shadow geometry is split into colorless parts that inherit safely; color-capable inputs such as `--ui-ink` and `--ui-surface-shadow` may include their own colors.
|
|
619
|
+
|
|
620
|
+
| Token | Purpose | Fallback |
|
|
621
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
622
|
+
| `--ui-radius` | Corner radius for controls. | `--radius-control` |
|
|
623
|
+
| `--ui-radius-surface` | Corner radius for surfaces. | `--radius-surface` |
|
|
624
|
+
| `--ui-border-width` | Edge thickness. | `--border-width` |
|
|
625
|
+
| `--ui-border-max` | Ceiling on computed edge width. | `100px` |
|
|
626
|
+
| `--ui-ink` | Neutral line color when no intent is set. | `--color-border` |
|
|
627
|
+
| `--ui-rule` | Lines inside a table. Unset draws the head and foot boundaries only; `0%` draws none; `100%` rules every row. | unset |
|
|
628
|
+
| `--ui-shadow-x` | Shadow horizontal offset. | `0px` |
|
|
629
|
+
| `--ui-shadow-y` | Shadow vertical offset. | `0px` |
|
|
630
|
+
| `--ui-shadow-blur` | Shadow blur radius. | `0px` |
|
|
631
|
+
| `--ui-shadow-spread` | Shadow spread radius. | `0px` |
|
|
632
|
+
| `--ui-shadow-inset` | The `inset` keyword for an inner ring. | Empty |
|
|
633
|
+
| `--ui-hover-shadow-x` | Shadow horizontal offset while hovered. | `--ui-shadow-x` |
|
|
634
|
+
| `--ui-hover-shadow-y` | Shadow vertical offset while hovered. | `--ui-shadow-y` |
|
|
635
|
+
| `--ui-active-shadow-x` | Shadow horizontal offset while pressed. | `--ui-shadow-x` |
|
|
636
|
+
| `--ui-active-shadow-y` | Shadow vertical offset while pressed. | `--ui-shadow-y` |
|
|
637
|
+
| `--ui-active-transform` | Transform while pressed. `none` under `prefers-reduced-motion`. | `scale(0.97)` |
|
|
638
|
+
| `--ui-shadow-ink` | Percentage of shadow color taken from intent ink. | `0%` |
|
|
639
|
+
| `--ui-shadow-edge` | Declared, even empty, when the shadow is the element's edge rather than its depth. Read for presence. | Unset |
|
|
640
|
+
| `--ui-elevation` | Unitless multiplier over shadow geometry. | `1` |
|
|
641
|
+
| `--ui-surface-shadow` | Complete multi-layer shadow accepted by surfaces only. | Unset |
|
|
642
|
+
| `--ui-surface-ground` | Ground a surface sits on. | `--color-background` |
|
|
643
|
+
| `--ui-bg-alpha` | Multiplier over fill for translucent materials. | `1` |
|
|
644
|
+
| `--ui-backdrop` | Backdrop filter accepted by surfaces only. | `none` |
|
|
645
|
+
| `--ui-hover-transform` | Transform while an interactive element is hovered. | `none` |
|
|
646
|
+
| `--ui-clip` | Structural component silhouette. | `none` |
|
|
647
|
+
| `--ui-clip-tight` | Compact component silhouette. | `--ui-clip` |
|
|
648
|
+
| `--ui-focus-inset` | Inset focus layer width. | Unset |
|
|
649
|
+
|
|
587
650
|
```html
|
|
588
|
-
<section
|
|
589
|
-
<button class="btn primary">
|
|
651
|
+
<section style="--ui-radius: 0; --ui-border-width: 3px">
|
|
652
|
+
<button class="btn primary">Square and heavy</button>
|
|
653
|
+
<input class="ipt" placeholder="Matches" />
|
|
590
654
|
</section>
|
|
591
655
|
```
|
|
592
656
|
|
|
593
|
-
|
|
594
|
-
instead; see [Integrating → Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
657
|
+
> **Material token contract**: components read these; aesthetic classes and consumer overrides set them.
|
|
595
658
|
|
|
596
|
-
|
|
659
|
+
### Shape and ring composition
|
|
660
|
+
|
|
661
|
+
Clipping removes borders, outlines, and outer shadows. An aesthetic that uses a clip can replace the border with an inset shadow by setting `--ui-shadow-inset` and the four shadow geometry parts. Set `--ui-border-max: 0px` when that ring replaces the component border. Focusable components restore the clipped focus outline through `--ui-focus-inset`.
|
|
662
|
+
|
|
663
|
+
Declare `--ui-shadow-edge` when that ring _is_ the border. The ring then takes the element's edge color, which carries the edge axis as its alpha, so `.edged` draws it and `.edgeless` does not — the same answer a real border gives. Without it the ring paints on every component the aesthetic reaches and the edge classes have no effect. Use `--ui-shadow-ink` instead when the shadow is depth cast in the intent's own ink, such as a hard offset slab, which an `.edgeless` element still gets.
|
|
664
|
+
|
|
665
|
+
The token is read for its presence, not its value: declare it empty to switch the ring on, and `initial` to switch it back off in an aesthetic nested inside one that declares it.
|
|
666
|
+
|
|
667
|
+
```css
|
|
668
|
+
.stepped {
|
|
669
|
+
--ui-clip: polygon(/* structural silhouette */);
|
|
670
|
+
--ui-clip-tight: none;
|
|
671
|
+
--ui-border-max: 0px;
|
|
672
|
+
--ui-shadow-inset: inset;
|
|
673
|
+
--ui-shadow-edge: ;
|
|
674
|
+
--ui-shadow-x: 0px;
|
|
675
|
+
--ui-shadow-y: 0px;
|
|
676
|
+
--ui-shadow-blur: 0px;
|
|
677
|
+
--ui-shadow-spread: 4px;
|
|
678
|
+
--ui-focus-inset: var(--focus-ring-width);
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Variables prefixed `--_` are internal composition outputs — `--_bg`, `--_fg`, `--_edge`, `--_fill`, `--_shadow`, and the `--_d-*` resting slots a component states for `box` to read. Do not set or read them; only the `--ui-*` inputs above are public.
|
|
683
|
+
|
|
684
|
+
## Aesthetic tokens
|
|
685
|
+
|
|
686
|
+
The shipped [aesthetic classes](docs/usage/aesthetics.md) set material tokens for you. Only aesthetics with a token listed below expose an aesthetic-specific token. Tune all other behavior through the shared material tokens above. Listed tokens are available only where that aesthetic's stylesheet is imported.
|
|
687
|
+
|
|
688
|
+
| Token | Aesthetic | Purpose | Default |
|
|
689
|
+
| ------------------------ | --------------- | ----------------------------------------------------------- | --------------------------------- |
|
|
690
|
+
| `--neo-offset` | `.neobrutalism` | Hard shadow offset and distance the press travels. | `4px` |
|
|
691
|
+
| `--glass-radius` | `.glass` | Control corner radius. | `0.75rem` |
|
|
692
|
+
| `--glass-radius-surface` | `.glass` | Surface corner radius. | `1rem` |
|
|
693
|
+
| `--pixel-unit` | `.pixel` | One pixel of the grid. The corner cut and the ring are one. | `4px` |
|
|
694
|
+
| `--font-pixel` | `.pixel` | Consumer-supplied pixel font stack. | Falls back to the monospace stack |
|
|
695
|
+
| `--tile-radius` | `.chunky-tile` | Corner radius, on controls and surfaces alike. | `0.75rem` |
|
|
696
|
+
| `--tile-lift` | `.chunky-tile` | Depth of the seated bar, and how far a press travels. | `4px` |
|
|
697
|
+
| `--font-rounded` | `.chunky-tile` | Consumer-supplied rounded font stack. | Falls back to the page stack |
|
|
597
698
|
|
|
598
|
-
|
|
599
|
-
or `.dark` to the root or any ancestor to force token values for that subtree.
|
|
600
|
-
`.theme-light`, `.theme-dark`, and `data-theme="light|dark"` are aliases.
|
|
699
|
+
All four aesthetics reach the same way: each knob is _read_ with its default as a `var()` fallback rather than declared, so it resolves once into a private on the aesthetic's own class (`--_pixel-unit`, `--_neo-offset`, `--_tile-radius`, `--_tile-lift`, and so on). Set a knob on the element carrying the aesthetic class, or on any ancestor including `:root`:
|
|
601
700
|
|
|
602
701
|
```html
|
|
603
|
-
<
|
|
702
|
+
<section class="pixel" style="--pixel-unit: 6px">
|
|
703
|
+
<button class="btn primary">Chunkier</button>
|
|
704
|
+
</section>
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
```html
|
|
708
|
+
<html class="pixel" style="--pixel-unit: 6px">
|
|
604
709
|
<body>
|
|
605
|
-
<button class="btn primary">
|
|
710
|
+
<button class="btn primary">Also chunkier, set from the root</button>
|
|
606
711
|
</body>
|
|
607
712
|
</html>
|
|
608
713
|
```
|
|
609
714
|
|
|
610
|
-
|
|
611
|
-
selectors resolve and [Usage → Theming](docs/usage/theming.md) for the full color
|
|
612
|
-
token reference.
|
|
715
|
+
Knobs and material tokens reach in opposite directions, and telling them apart matters. A knob resolves once at the aesthetic's own class and inherits down already-resolved, so it reaches the aesthetic **from above**: setting it on the element or any ancestor works, but setting it on a _descendant_ of the aesthetic class changes nothing — the value has already been read. `--ui-radius` and the other material tokens above are read fresh by each component as it draws, so they reach **at or below** the element carrying them, which is how you round one card differently from its neighbours without touching the knob at all.
|
|
613
716
|
|
|
614
|
-
|
|
717
|
+
Shipped aesthetics set the shared material tokens above. `.neobrutalism` and `.pixel` set `--ui-ink` for their neutral outline; intent classes still override the intent slots on each component.
|
|
615
718
|
|
|
616
|
-
|
|
617
|
-
resolve package CSS imports. `/tw` entrypoints publish copied, uncompiled
|
|
618
|
-
source from `dist/tw`; a Tailwind v4 build must still process their `@theme`,
|
|
619
|
-
`@utility`, `@apply`, and related directives. Focused source component
|
|
620
|
-
entrypoints include theme tokens so their classes can work independently.
|
|
719
|
+
## Component internals
|
|
621
720
|
|
|
622
|
-
|
|
623
|
-
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
624
|
-
| `@codenhub/styles` | Compiled complete surface: Tailwind base/theme output, tokens, typography, utilities, components, and reset. Applies global reset, root theme, and focus rules. |
|
|
625
|
-
| `@codenhub/styles/theme` | Compiled Tailwind theme variables, Codenhub tokens, and the presentation classes. Sets root theme properties and system/explicit theme selectors; no reset or component classes. |
|
|
626
|
-
| `@codenhub/styles/components` | Compiled theme tokens, typography utilities, and every component class. Includes static reduced-motion loader masks; no reset, layout/content utilities, or native mappings. |
|
|
627
|
-
| `@codenhub/styles/native` | Compiled complete surface plus classless mappings for headings, text, content elements, form controls, and buttons. Applies the reset and broad element selectors. |
|
|
628
|
-
| `@codenhub/styles/aesthetics` | Compiled `.neobrutalism`, `.glass`, `.pixel`, and `.chunky-tile` classes. Adds no tokens or components of its own; import after the base stylesheet. |
|
|
629
|
-
| `@codenhub/styles/aesthetics/*` | One compiled aesthetic: `/neobrutalism`, `/glass`, `/pixel`, or `/chunky-tile`. Same rules and ordering as the combined file. |
|
|
630
|
-
| `@codenhub/styles/tw` | Source equivalent of the complete stylesheet. Imports Tailwind itself, theme, typography, utilities, components, and reset; applies the same global rules. |
|
|
631
|
-
| `@codenhub/styles/tw/theme` | Source theme, tokens, and presentation classes. Imports Tailwind's theme layer; emits root/system/explicit theme selectors and defines the package's custom `dark:` variant. |
|
|
632
|
-
| `@codenhub/styles/tw/components` | Published Tailwind source for theme, typography, buttons, feedback, forms, loaders, surfaces, and tooltips, including static reduced-motion loader masks. No reset, layout/content utilities, or native mappings. |
|
|
633
|
-
| `@codenhub/styles/tw/surface` | Source theme plus `.surface`, `.card`, and `.panel`. |
|
|
634
|
-
| `@codenhub/styles/tw/button` | Published Tailwind source for theme, activity indicators, and `.btn`; includes static reduced-motion masks because buttons compose `.loader-mask` for loading state. |
|
|
635
|
-
| `@codenhub/styles/tw/form` | Source theme plus field, public `text-control`, input, textarea, select, checkbox, radio, and switch utilities. |
|
|
636
|
-
| `@codenhub/styles/tw/feedback` | Source theme plus alert, badge, skeleton, and progress utilities and their keyframes. |
|
|
637
|
-
| `@codenhub/styles/tw/loader` | Published Tailwind source for activity indicators and loaders, including static reduced-motion masks. It has no theme or reset side effects and uses `currentColor`. |
|
|
638
|
-
| `@codenhub/styles/tw/tooltip` | Source theme plus tooltip utilities. |
|
|
639
|
-
| `@codenhub/styles/tw/reset` | Source theme, typography utilities, and global reset/accessibility rules. It changes root/body/elements and selection, motion, focus, scrollbar, and forced colors. |
|
|
640
|
-
| `@codenhub/styles/tw/native` | Source complete surface plus reset and classless native-element mappings; broadest global effects. |
|
|
641
|
-
| `@codenhub/styles/tw/typography` | Source theme plus typography utilities; sets theme selectors but no reset or classless mappings. |
|
|
642
|
-
| `@codenhub/styles/tw/utilities` | Source theme and typography composition plus layout and content utilities; safelists the package utility names. |
|
|
643
|
-
| `@codenhub/styles/tw/aesthetics` | Source aesthetics, combined. `/tw/aesthetics/neobrutalism`, `/glass`, `/pixel`, and `/chunky-tile` publish one apiece. Plain CSS with no theme or reset side effects. |
|
|
721
|
+
Component classes may define scoped implementation variables such as `--surface-padding`, `--field-gap`, `--input-group-pad`, `--switch-*`, `--table-*`, `--quote-*`, and `--tooltip-*`. These variables are internal wiring for class composition and are not the public token contract.
|
|
644
722
|
|
|
645
|
-
|
|
646
|
-
generated CSS is acceptable; pick the narrowest entrypoint that covers what a
|
|
647
|
-
page needs.
|
|
723
|
+
Consumers should customize broad behavior through color, foundation, motion, elevation, radius, layout, and focus tokens first. The only supported component-scoped input is `--progress-value` on the `.progress` element, because consumers must provide a value such as `64%`.
|
|
648
724
|
|
|
649
|
-
|
|
725
|
+
`--layout-gap` replaces the removed `--layout-stack-gap` and `--layout-cluster-gap` tokens. No compatibility aliases are provided.
|
|
650
726
|
|
|
651
|
-
|
|
652
|
-
- Chrome 123, Safari 17.5, or Firefox 121 and newer. Color tokens are declared
|
|
653
|
-
with `light-dark()` and selected by `color-scheme` (Firefox 120), and
|
|
654
|
-
`.input-group` propagates a nested control's invalid and disabled state with
|
|
655
|
-
`:has()` (Firefox 121).
|
|
656
|
-
- Tailwind CSS 4 or newer is required only for `/tw` source entrypoints.
|
|
657
|
-
- The package has no JavaScript runtime. Apps must provide semantic HTML,
|
|
658
|
-
ARIA, keyboard behavior, focus management, validation, and announcements;
|
|
659
|
-
see [Accessibility](docs/accessibility.md).
|
|
727
|
+
Next: [Aesthetics](docs/usage/aesthetics.md) covers each shipped aesthetic's complete look and its documented exceptions.
|
|
660
728
|
|
|
661
729
|
<!-- Source: docs/usage/aesthetics.md -->
|
|
662
730
|
|
|
663
731
|
# Aesthetics
|
|
664
732
|
|
|
665
|
-
An aesthetic decides what a component is _made of_: its radius, border
|
|
666
|
-
thickness, shadow, and shape. Aesthetics ship from opt-in entrypoints, so
|
|
667
|
-
importing the stylesheet is what makes the classes available:
|
|
733
|
+
An aesthetic decides what a component is _made of_: its radius, border thickness, shadow, and shape. Aesthetics ship from opt-in entrypoints, so importing the stylesheet is what makes the classes available:
|
|
668
734
|
|
|
669
735
|
```css
|
|
670
736
|
@import "@codenhub/styles";
|
|
671
737
|
@import "@codenhub/styles/aesthetics";
|
|
672
738
|
```
|
|
673
739
|
|
|
674
|
-
Import them after the base stylesheet. `.neobrutalism` and `.pixel` replace
|
|
675
|
-
the neutral border color, and they do so at zero specificity, so source order
|
|
676
|
-
is what lets them win.
|
|
740
|
+
Import them after the base stylesheet. `.neobrutalism` and `.pixel` replace the neutral border color, and they do so at zero specificity, so source order is what lets them win.
|
|
677
741
|
|
|
678
742
|
Like presentation, an aesthetic class cascades to any subtree:
|
|
679
743
|
|
|
@@ -691,33 +755,19 @@ Like presentation, an aesthetic class cascades to any subtree:
|
|
|
691
755
|
| `.pixel` | Corners cut by one grid unit, a chunky outline drawn as an inset ring, and the consumer-supplied `--font-pixel` over monospace. |
|
|
692
756
|
| `.chunky-tile` | Rounded slabs seated on a darker shade of themselves, with a press that moves the element down into its own bar. |
|
|
693
757
|
|
|
694
|
-
Aesthetics compose with every supported fill and edge pair. `.ghost.edgeless`
|
|
695
|
-
intentionally removes the visible material traits from components whose
|
|
696
|
-
aesthetic is expressed only through their fill, edge, or elevation.
|
|
758
|
+
Aesthetics compose with every supported fill and edge pair. `.ghost.edgeless` intentionally removes the visible material traits from components whose aesthetic is expressed only through their fill, edge, or elevation.
|
|
697
759
|
|
|
698
|
-
An explicit presentation on the element still wins over the aesthetic's
|
|
699
|
-
defaults. The aesthetic supplies edge thickness; presentation only decides
|
|
700
|
-
whether that edge is drawn.
|
|
760
|
+
An explicit presentation on the element still wins over the aesthetic's defaults. The aesthetic supplies edge thickness; presentation only decides whether that edge is drawn.
|
|
701
761
|
|
|
702
|
-
An aesthetic directly on a component likewise wins over an inherited
|
|
703
|
-
aesthetic. This includes tooltip pseudo-elements: `.tooltip.glass` gets the
|
|
704
|
-
complete glass bubble under a pixel ancestor, and `.tooltip.pixel` gets the
|
|
705
|
-
complete stepped bubble under a glass ancestor.
|
|
762
|
+
An aesthetic directly on a component likewise wins over an inherited aesthetic. This includes tooltip bubbles: `.tooltip-bubble.glass` gets the complete glass bubble under a pixel ancestor, and `.tooltip-bubble.pixel` gets the complete stepped bubble under a glass ancestor.
|
|
706
763
|
|
|
707
|
-
Each aesthetic that scales from one number publishes it as a knob — see
|
|
708
|
-
[Customizing → Aesthetic tokens](docs/usage/customizing.md#aesthetic-tokens) for how
|
|
709
|
-
knobs resolve and where they can be set. This page covers what each
|
|
710
|
-
aesthetic looks like and where it makes documented exceptions.
|
|
764
|
+
Each aesthetic that scales from one number publishes it as a knob — see [Customizing → Aesthetic tokens](docs/usage/customizing.md#aesthetic-tokens) for how knobs resolve and where they can be set. This page covers what each aesthetic looks like and where it makes documented exceptions.
|
|
711
765
|
|
|
712
766
|
## Neobrutalism
|
|
713
767
|
|
|
714
|
-
The shadow is cast in the component's own intent, so a success button throws
|
|
715
|
-
a green shadow and a destructive card a red one. With no intent, both the
|
|
716
|
-
outline and the shadow use the ink, which follows the theme rather than the
|
|
717
|
-
palette.
|
|
768
|
+
The shadow is cast in the component's own intent, so a success button throws a green shadow and a destructive card a red one. With no intent, both the outline and the shadow use the ink, which follows the theme rather than the palette.
|
|
718
769
|
|
|
719
|
-
`--neo-offset` is the knob: the shadow offset and the distance the press
|
|
720
|
-
travels are the same number, so scaling the look scales both at once.
|
|
770
|
+
`--neo-offset` is the knob: the shadow offset and the distance the press travels are the same number, so scaling the look scales both at once.
|
|
721
771
|
|
|
722
772
|
```html
|
|
723
773
|
<section class="neobrutalism" style="--neo-offset: 6px">
|
|
@@ -727,26 +777,15 @@ travels are the same number, so scaling the look scales both at once.
|
|
|
727
777
|
|
|
728
778
|
**Exceptions:**
|
|
729
779
|
|
|
730
|
-
- Content chips are left alone. A `.kbd`, `.code`, or `.pre` is content
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
- Only `.btn` and `.card.interactive` respond to hover and press. A card
|
|
734
|
-
becomes a control when it opts in with `.interactive`; a plain card, an
|
|
735
|
-
alert, or a panel is a container and stays put.
|
|
736
|
-
- An alert rests on the slab under this aesthetic and nowhere else. Here the
|
|
737
|
-
slab is a second ink line rather than depth, and an alert is the only
|
|
738
|
-
container the package rests flat, so without it a neobrutalist alert is
|
|
739
|
-
the one box on the page that reads as unfinished. It is a resting default,
|
|
740
|
-
so `.flat` still takes it off and `.floating` still doubles it.
|
|
780
|
+
- Content chips are left alone. A `.kbd`, `.code`, or `.pre` is content rather than structure, and an ink edge on one reads as a defect: a key cap keeps the quiet border color and a code chip keeps no border at all.
|
|
781
|
+
- Only `.btn` and an opted-in `.card` respond to hover and press. A card opts in with `.interactive`, or with one of its halves — `.hoverable` for the hover response alone, `.pressable` for the press alone; a plain card, an alert, or a panel is a container and stays put.
|
|
782
|
+
- An alert rests on the slab under this aesthetic and nowhere else. Here the slab is a second ink line rather than depth, and an alert is the only container the package rests flat, so without it a neobrutalist alert is the one box on the page that reads as unfinished. It is a resting default, so `.flat` still takes it off and `.floating` still doubles it.
|
|
741
783
|
|
|
742
784
|
## Glass
|
|
743
785
|
|
|
744
|
-
`.glass` needs something behind it to blur. On a flat page background it
|
|
745
|
-
renders as a plain translucent panel.
|
|
786
|
+
`.glass` needs something behind it to blur. On a flat page background it renders as a plain translucent panel.
|
|
746
787
|
|
|
747
|
-
`--glass-radius` and `--glass-radius-surface` are the corners, at `0.75rem`
|
|
748
|
-
and `1rem`. Glass is rounder than the base geometry because a translucent
|
|
749
|
-
panel with a tight corner reads as a cut-out rather than as a pane.
|
|
788
|
+
`--glass-radius` and `--glass-radius-surface` are the corners, at `0.75rem` and `1rem`. Glass is rounder than the base geometry because a translucent panel with a tight corner reads as a cut-out rather than as a pane.
|
|
750
789
|
|
|
751
790
|
```html
|
|
752
791
|
<section class="glass" style="--glass-radius-surface: 1.5rem">
|
|
@@ -756,18 +795,12 @@ panel with a tight corner reads as a cut-out rather than as a pane.
|
|
|
756
795
|
|
|
757
796
|
**Exceptions:**
|
|
758
797
|
|
|
759
|
-
- The blur applies to `.card`, `.panel`, `.alert`, and the tooltip bubble
|
|
760
|
-
|
|
761
|
-
of a dense cluster costs a composited layer apiece and reads as noise.
|
|
762
|
-
Controls still take the aesthetic's radius, border, and shadow.
|
|
763
|
-
- Under `prefers-reduced-transparency: reduce`, glass surfaces drop the blur
|
|
764
|
-
and become opaque.
|
|
798
|
+
- The blur applies to `.card`, `.panel`, `.alert`, and the tooltip bubble only. Controls stay solid and sit on the glass: a blur under every control of a dense cluster costs a composited layer apiece and reads as noise. Controls still take the aesthetic's radius, border, and shadow.
|
|
799
|
+
- Under `prefers-reduced-transparency: reduce`, glass surfaces drop the blur and become opaque.
|
|
765
800
|
|
|
766
801
|
## Pixel
|
|
767
802
|
|
|
768
|
-
`--pixel-unit` is one pixel of the imaginary low-resolution grid, `4px` by
|
|
769
|
-
default. One square unit is cut from each corner and the outline is one unit
|
|
770
|
-
thick, so the cut and what covers it are the same size.
|
|
803
|
+
`--pixel-unit` is one pixel of the imaginary low-resolution grid, `4px` by default. One square unit is cut from each corner and the outline is one unit thick, so the cut and what covers it are the same size.
|
|
771
804
|
|
|
772
805
|
```html
|
|
773
806
|
<section class="pixel" style="--pixel-unit: 6px">
|
|
@@ -777,44 +810,17 @@ thick, so the cut and what covers it are the same size.
|
|
|
777
810
|
|
|
778
811
|
**Exceptions:**
|
|
779
812
|
|
|
780
|
-
- Corners are one unit or nothing. Chips square instead of stepping —
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
table's `overflow: hidden` fights the clip and the corners square off
|
|
786
|
-
where the two meet, and progress and skeleton never read a clip at all —
|
|
787
|
-
a squaring aesthetic reaches them through `border-radius` alone. `.pre` is
|
|
788
|
-
the one exception that steps: it carries no clip override, so it inherits
|
|
789
|
-
the same polygon a button or a card gets, and with no border by default
|
|
790
|
-
the cut shows with no ring around it.
|
|
791
|
-
- The outline is an inset ring, because a clip removes a real border — and
|
|
792
|
-
the focus ring, for the same reason, is a second inset layer rather than
|
|
793
|
-
an outline. Both are the element's own edge rather than a shadow, so the
|
|
794
|
-
border answers `.edged` and `.edgeless` the way a border does: a
|
|
795
|
-
`.edgeless` badge and a `.solid` button draw none, a `.edged` card draws
|
|
796
|
-
one, and a field keeps one whatever a container asks for.
|
|
797
|
-
- The tooltip trigger and `.radio` are hardcoded past the clip rather than
|
|
798
|
-
reached by it. `.tooltip-icon` forces `clip-path: none`, since a stepped
|
|
799
|
-
badge would clip its own bubble away with it. `.radio` forces the same,
|
|
800
|
-
because the circle is the only thing telling it from a checkbox at a
|
|
801
|
-
glance, and a stepped polygon would square it.
|
|
802
|
-
- `--font-pixel` is yours to supply. The package ships no font binary, so the
|
|
803
|
-
aesthetic has no network side effect and falls back to the monospace
|
|
804
|
-
stack.
|
|
813
|
+
- Corners are one unit or nothing. Chips square instead of stepping — badges, key caps, code, checkboxes, and switches all read `--ui-clip-tight`, which this aesthetic sets to none, because one unit off each corner of a 24px badge is a bite rather than a corner. Tables, progress bars, and skeletons square too, each for its own reason: a table's `overflow: hidden` fights the clip and the corners square off where the two meet, and progress and skeleton never read a clip at all — a squaring aesthetic reaches them through `border-radius` alone. `.pre` is the one exception that steps: it carries no clip override, so it inherits the same polygon a button or a card gets, and with no border by default the cut shows with no ring around it.
|
|
814
|
+
- The outline is an inset ring, because a clip removes a real border — and the focus ring, for the same reason, is a second inset layer rather than an outline. Both are the element's own edge rather than a shadow, so the border answers `.edged` and `.edgeless` the way a border does: a `.edgeless` badge and a `.solid` button draw none, a `.edged` card draws one, and a field keeps one whatever a container asks for.
|
|
815
|
+
- The tooltip trigger and `.radio` are hardcoded past the clip rather than reached by it. `.tooltip-icon` forces `clip-path: none` because the docs call it a circular fixed identity, the same reason `.radio` forces it: the circle is the only thing telling it from a checkbox at a glance, and a stepped polygon would square either one.
|
|
816
|
+
- `--font-pixel` is yours to supply. The package ships no font binary, so the aesthetic has no network side effect and falls back to the monospace stack.
|
|
817
|
+
- Numeral legibility depends on the face you pick. At UI sizes -- pagination, counters, small badges -- some pixel/bitmap faces render lookalike digits such as `2`/`8` or `3`/`9` closely enough to misread at a glance; this is a property of the font, not something the aesthetic's CSS can correct. Prefer a face with clearly distinct digit shapes wherever small numeric text matters.
|
|
805
818
|
|
|
806
819
|
## Chunky tile
|
|
807
820
|
|
|
808
|
-
Rounded slabs sitting on a darker shade of themselves, pressed flat on
|
|
809
|
-
click. The look mobile learning and game apps use; it is named for what it
|
|
810
|
-
is made of rather than for any one product.
|
|
821
|
+
Rounded slabs sitting on a darker shade of themselves, pressed flat on click. The look mobile learning and game apps use; it is named for what it is made of rather than for any one product.
|
|
811
822
|
|
|
812
|
-
`--tile-lift` is how deep the bar is and how far a press travels, `4px` by
|
|
813
|
-
default. One number drives both, so the top edge drops by exactly the bar's
|
|
814
|
-
depth and the bottom edge does not move. `--tile-radius` is the corner,
|
|
815
|
-
`0.75rem`, and it is one value rather than a control/surface pair on
|
|
816
|
-
purpose — the shared corner is what makes a button and the card it sits in
|
|
817
|
-
read as the same object.
|
|
823
|
+
`--tile-lift` is how deep the bar is and how far a press travels, `4px` by default. One number drives both, so the top edge drops by exactly the bar's depth and the bottom edge does not move. `--tile-radius` is the corner, `0.75rem`, and it is one value rather than a control/surface pair on purpose — the shared corner is what makes a button and the card it sits in read as the same object.
|
|
818
824
|
|
|
819
825
|
```html
|
|
820
826
|
<section class="chunky-tile" style="--tile-lift: 6px; --tile-radius: 0.5rem">
|
|
@@ -822,49 +828,29 @@ read as the same object.
|
|
|
822
828
|
</section>
|
|
823
829
|
```
|
|
824
830
|
|
|
825
|
-
The bar is a darker shade of the element's own colour rather than a shadow
|
|
826
|
-
under it, so it follows every intent: a success button sits on a dark green
|
|
827
|
-
edge, a destructive one on a dark red edge, and a plain card on a grey edge
|
|
828
|
-
that matches the line it already draws. Measured on the light palette,
|
|
829
|
-
`.btn.success` is `#007a55` on `#004c34`. On a dark page a near-black
|
|
830
|
-
surface cannot go darker, so its bar lands lighter and reads as a rim.
|
|
831
|
+
The bar is a darker shade of the element's own colour rather than a shadow under it, so it follows every intent: a success button sits on a dark green edge, a destructive one on a dark red edge, and a plain card on a grey edge that matches the line it already draws. Measured on the light palette, `.btn.success` is `#007a55` on `#004c34`. On a dark page a near-black surface cannot go darker, so its bar lands lighter and reads as a rim.
|
|
831
832
|
|
|
832
833
|
**Exceptions:**
|
|
833
834
|
|
|
834
|
-
- Only what the registry rests above zero sits on a bar — buttons and cards.
|
|
835
|
-
A badge, chip, field, or tooltip is flat until it asks for depth with
|
|
836
|
-
`.raised`, and `.flat` takes the bar off anything:
|
|
835
|
+
- Only what the registry rests above zero sits on a bar — buttons and cards. A badge, chip, field, or tooltip is flat until it asks for depth with `.raised`, and `.flat` takes the bar off anything:
|
|
837
836
|
|
|
838
837
|
```html
|
|
839
838
|
<div class="card soft edgeless info flat">Promo panel, deliberately flat</div>
|
|
840
839
|
```
|
|
841
840
|
|
|
842
|
-
- `--font-rounded` is yours to supply. A heavy rounded grotesque is most of
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
-
|
|
846
|
-
|
|
847
|
-
uppercase buttons, write that rule in your own stylesheet.
|
|
848
|
-
- `.card.interactive` presses like a `.btn`, so a chunky answer tile drops
|
|
849
|
-
onto its bar on click. A plain `.card` stays put.
|
|
850
|
-
- The shipped `.primary` is a near-black monochrome, which leaves a primary
|
|
851
|
-
button's bar almost invisible against its own plate; give `.primary` a hue
|
|
852
|
-
if it is the call to action.
|
|
853
|
-
- Depth in this aesthetic is the bar, never a blurred drop shadow. Anything
|
|
854
|
-
the registry rests flat sits flush on the page.
|
|
841
|
+
- `--font-rounded` is yours to supply. A heavy rounded grotesque is most of this look and the package ships no font binary, so with nothing supplied the page's own stack is used.
|
|
842
|
+
- Action labels are drawn heavier and slightly tracked. Casing is left alone: how a label is worded is the application's decision, so if you want uppercase buttons, write that rule in your own stylesheet.
|
|
843
|
+
- `.card.interactive` presses like a `.btn`, so a chunky answer tile drops onto its bar on click; `.card.pressable` gets the same press on its own. A plain `.card` stays put.
|
|
844
|
+
- The shipped `.primary` is a near-black-or-near-white monochrome depending on theme, and the bar mixes toward a fixed black regardless of theme. In light theme this leaves a primary button's bar almost invisible against its own near-black plate (measured `1.04:1`, 10 units of sRGB distance); in dark theme the near-white plate against the same black-anchored bar reads fine (`2.48:1`, 154 units). Give `.primary` a hue of its own if it is the call to action and needs the depth cue in both themes.
|
|
845
|
+
- Depth in this aesthetic is the bar, never a blurred drop shadow. Anything the registry rests flat sits flush on the page.
|
|
855
846
|
|
|
856
847
|
<!-- Source: docs/usage/buttons.md -->
|
|
857
848
|
|
|
858
849
|
# Buttons
|
|
859
850
|
|
|
860
|
-
Use `.btn` with one optional intent class, one optional fill class, one
|
|
861
|
-
optional edge class, optional size class, and optional state. See
|
|
862
|
-
[Composing](docs/usage/composing.md) for how the intent, presentation, and aesthetic
|
|
863
|
-
axes combine on any component.
|
|
851
|
+
Use `.btn` with one optional intent class, one optional fill class, one optional edge class, optional size class, and optional state. See [Composing](docs/usage/composing.md) for how the intent, presentation, and aesthetic axes combine on any component.
|
|
864
852
|
|
|
865
|
-
Intent classes map color tokens into button tone slots.
|
|
866
|
-
[Presentation](docs/usage/composing.md#presentation) decides how much of that intent
|
|
867
|
-
reaches background, text, border, and hover.
|
|
853
|
+
Intent classes map color tokens into button tone slots. [Presentation](docs/usage/composing.md#presentation) decides how much of that intent reaches background, text, border, and hover.
|
|
868
854
|
|
|
869
855
|
Intent classes:
|
|
870
856
|
|
|
@@ -878,29 +864,22 @@ Intent classes:
|
|
|
878
864
|
| `.destructive`, `.danger`, `.error` | Destructive or error action. |
|
|
879
865
|
| `.info` | Informational action. |
|
|
880
866
|
|
|
881
|
-
Every component that supports intent accepts the same list and reads the
|
|
882
|
-
same [intent tokens](docs/usage/theming.md#intent-tokens), so a custom intent class
|
|
883
|
-
works everywhere without touching a component.
|
|
867
|
+
Every component that supports intent accepts the same list and reads the same [intent tokens](docs/usage/theming.md#intent-tokens), so a custom intent class works everywhere without touching a component.
|
|
884
868
|
|
|
885
|
-
`.neutral` is the only intent that does not fill all the way. Its color is
|
|
886
|
-
the page's own text color, so a full fill of it would be a black or white
|
|
887
|
-
slab — the loudest element on the page, for the intent that means nothing in
|
|
888
|
-
particular. It stops at `--intent-fill-max`, which is why a `.solid` neutral
|
|
889
|
-
is a quiet plate a step past `.soft` rather than a block of ink. Set the slot
|
|
890
|
-
on your own intent classes; see [Theming → Intent tokens](docs/usage/theming.md#intent-tokens).
|
|
869
|
+
`.neutral` is the only intent that does not fill all the way. Its color is the page's own text color, so a full fill of it would be a black or white slab — the loudest element on the page, for the intent that means nothing in particular. It stops at `--intent-fill-max`, which is why a `.solid` neutral is a quiet plate a step past `.soft` rather than a block of ink. Set the slot on your own intent classes; see [Theming → Intent tokens](docs/usage/theming.md#intent-tokens).
|
|
891
870
|
|
|
892
871
|
Size and shape classes:
|
|
893
872
|
|
|
894
|
-
| Class | Purpose
|
|
895
|
-
| ---------------------------------------------------------------------- |
|
|
896
|
-
| `.pill` | Fully rounded button corners (`border-radius: 9999px`).
|
|
897
|
-
| `.sm` | Smaller button.
|
|
898
|
-
| `.lg` | Larger button.
|
|
899
|
-
| `.p-
|
|
900
|
-
| `.p-
|
|
901
|
-
| `.
|
|
902
|
-
| `.
|
|
903
|
-
| `.disabled`, `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` | Disabled styling.
|
|
873
|
+
| Class | Purpose |
|
|
874
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
875
|
+
| `.pill` | Fully rounded button corners (`border-radius: 9999px`). |
|
|
876
|
+
| `.sm` | Smaller button. |
|
|
877
|
+
| `.lg` | Larger button. |
|
|
878
|
+
| `.p-xs`, `.dense` | Tightest padding modifier (`px-2 py-0.5`; icon `p-0.5`). Also drops the `--control-height` minimum so a chip-sized button fits a table cell or a dense toolbar; every other size and padding class keeps that floor for a labeled button. |
|
|
879
|
+
| `.p-sm`, `.compact` | Compact padding modifier (`px-2.5 py-1`; icon `p-1`). |
|
|
880
|
+
| `.p-lg`, `.spacious` | Spacious padding modifier (`px-6 py-3`; icon `p-3`). |
|
|
881
|
+
| `.icon` | Square icon button, sized to its own padding rather than to `--control-height`. Use an accessible name in HTML. |
|
|
882
|
+
| `.disabled`, `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` | Disabled styling. |
|
|
904
883
|
|
|
905
884
|
Examples:
|
|
906
885
|
|
|
@@ -910,236 +889,306 @@ Examples:
|
|
|
910
889
|
<button class="btn warning soft">Warning soft</button>
|
|
911
890
|
<button class="btn destructive ghost edgeless">Danger ghost</button>
|
|
912
891
|
<button class="btn icon primary" aria-label="Create">+</button>
|
|
913
|
-
<button class="btn primary
|
|
892
|
+
<button class="btn primary" disabled>
|
|
893
|
+
<span class="loader sm" aria-hidden="true"></span>
|
|
894
|
+
Saving
|
|
895
|
+
</button>
|
|
914
896
|
```
|
|
915
897
|
|
|
916
|
-
|
|
917
|
-
it with disabled behavior so users cannot trigger duplicate work. Loading
|
|
918
|
-
buttons keep only opacity and transform transitions active so spinner and
|
|
919
|
-
surface colors stay synchronized when theme tokens change.
|
|
898
|
+
## Loading
|
|
920
899
|
|
|
921
|
-
|
|
922
|
-
(`--ui-active-transform`, `scale(0.97)`). [Aesthetics](docs/usage/aesthetics.md) swap
|
|
923
|
-
in their own press gesture, and `prefers-reduced-motion` turns it off.
|
|
900
|
+
The package ships no loading state of its own: a loading button is a disabled `.btn` composed with a real [`.loader`](docs/usage/feedback.md) child, the same activity indicator every other component uses. Disable the button so users cannot trigger duplicate work, and drop a `.loader` in beside the label — any of its variants (`.dots-wave`, `.bars-wave`, and so on) work the same way. Whether to also hide the label is your call; the package has no built-in mechanism for that.
|
|
924
901
|
|
|
925
|
-
|
|
926
|
-
[Accessibility](docs/accessibility.md).
|
|
902
|
+
On the base look a press scales the button down a little (`--ui-active-transform`, `scale(0.97)`). [Aesthetics](docs/usage/aesthetics.md) swap in their own press gesture, and `prefers-reduced-motion` turns it off.
|
|
927
903
|
|
|
928
|
-
|
|
904
|
+
Use `<button>` for actions or an accessible link for navigation; see [Accessibility](docs/accessibility.md).
|
|
929
905
|
|
|
930
|
-
|
|
906
|
+
<!-- Source: docs/usage/forms.md -->
|
|
907
|
+
|
|
908
|
+
# Forms
|
|
931
909
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
910
|
+
| Class or Selector | Purpose |
|
|
911
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
912
|
+
| `.field` | Vertical field wrapper. |
|
|
913
|
+
| `.label` | Form label text. |
|
|
914
|
+
| `.hint` | Secondary helper text. |
|
|
915
|
+
| `.hint.error` | Helper text with destructive intent. |
|
|
916
|
+
| `.surface` | Shared public container composition utility. |
|
|
917
|
+
| `.text-control` | Shared public text-control composition utility. |
|
|
918
|
+
| `.ipt` | Input control styling. |
|
|
919
|
+
| `.input-group` | Wrapper that owns the field box so a control can carry an icon or affix. |
|
|
920
|
+
| `.textarea` | Textarea control styling. |
|
|
921
|
+
| `.select` | Select control styling. |
|
|
922
|
+
| `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
|
|
923
|
+
| `input[type="radio"].radio` | Custom radio control styling. |
|
|
924
|
+
| `input[type="checkbox"].switch` | Custom switch control styling. |
|
|
925
|
+
| `[aria-invalid="true"]` on controls | Destructive border and focus color. |
|
|
926
|
+
| `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
|
|
935
927
|
|
|
936
|
-
##
|
|
928
|
+
## Icons
|
|
929
|
+
|
|
930
|
+
No icon is shipped for text inputs — that's what this section covers. It used to paint one per input type as a `background-image` — a `data:` URI, which cannot read `currentColor` or a custom property, so every glyph shipped a light and a dark copy and a theme selector chose between them. That put icon artwork, and the package's only theme-by-selector branch, inside a CSS-only design system whose glyphs did not fit every aesthetic. Icons for `.input-group` are the consumer's to choose now.
|
|
931
|
+
|
|
932
|
+
This is separate from the toggle marks below, which stay small, fixed, built-in glyphs. `.alert`'s icon followed the same path since — see [Feedback → Icon](docs/usage/feedback.md#example) — the package no longer paints one there either.
|
|
933
|
+
|
|
934
|
+
`.input-group` is the wrapper for a control that carries one. It owns the field box — border, radius, fill, focus ring, invalid and disabled state — and the control inside it goes flush, so the boundary is drawn once. Any icon element works, placed before the control for a leading icon or after it for a trailing one: an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons`.
|
|
937
935
|
|
|
938
936
|
```html
|
|
939
|
-
<
|
|
940
|
-
<
|
|
941
|
-
<
|
|
942
|
-
|
|
937
|
+
<label class="field">
|
|
938
|
+
<span class="label">Email</span>
|
|
939
|
+
<div class="input-group">
|
|
940
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" aria-hidden="true">
|
|
941
|
+
<rect width="20" height="16" x="2" y="4" rx="2" />
|
|
942
|
+
<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
|
|
943
|
+
</svg>
|
|
944
|
+
<input class="ipt" type="email" />
|
|
945
|
+
</div>
|
|
946
|
+
</label>
|
|
947
|
+
<label class="field">
|
|
948
|
+
<span class="label">Search</span>
|
|
949
|
+
<div class="input-group">
|
|
950
|
+
<input class="ipt" type="search" />
|
|
951
|
+
<i class="ic-search" aria-hidden="true"></i>
|
|
952
|
+
</div>
|
|
953
|
+
</label>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
The group reads intent and both [presentation](docs/usage/composing.md#presentation) axes the same way a lone `.ipt` does — `.input-group.soft.edgeless` is the sunk variant, `.input-group.primary` colors the boundary — and follows whichever [aesthetic](docs/usage/aesthetics.md) is in scope. `aria-invalid` or `disabled` on the control inside propagates to the group; so does either on the group itself. Focus is shown with `:focus-within`, so unlike a lone field the ring also appears on a mouse click.
|
|
957
|
+
|
|
958
|
+
Non-icon input types keep the browser's native `date` and `datetime-local` pickers. WebKit's native `search` decorations are still suppressed on `.text-control` to keep them from overlapping the value.
|
|
959
|
+
|
|
960
|
+
The slot works the same way for a text affix — a URL host, a currency symbol, a unit — not just an icon:
|
|
961
|
+
|
|
962
|
+
```html
|
|
963
|
+
<div class="input-group">
|
|
964
|
+
<span>example.com/</span>
|
|
965
|
+
<input class="ipt" type="text" />
|
|
943
966
|
</div>
|
|
944
967
|
```
|
|
945
968
|
|
|
946
|
-
|
|
947
|
-
|
|
969
|
+
The gap between the control and its adornment is `--input-group-gap`, `0.5rem` by default: the right amount of breathing room for a distinct icon glyph, and too much for text meant to read as glued to the value — `example.com/` followed by a gap reads as a stray space breaking the path apart. Set `--input-group-gap: 0` on the group for a flush affix; icon compositions keep the default unchanged.
|
|
970
|
+
|
|
971
|
+
## Toggles
|
|
948
972
|
|
|
949
|
-
|
|
950
|
-
element that shows the intent, because a container silently recoloring every
|
|
951
|
-
descendant is a trap rather than a feature — see
|
|
952
|
-
[Theming → Intent tokens](docs/usage/theming.md#intent-tokens). Use `.neutral` to opt
|
|
953
|
-
an element back out of an inherited intent context. Presentation and
|
|
954
|
-
[material tokens](docs/usage/customizing.md#material-tokens) do cascade, so a
|
|
955
|
-
container can set the look of its whole subtree while any element overrides
|
|
956
|
-
it.
|
|
973
|
+
`.checkbox`, `.radio`, and `.switch` accept the same intent classes as buttons to set the checked color:
|
|
957
974
|
|
|
958
|
-
|
|
975
|
+
| Class | Meaning |
|
|
976
|
+
| ----------------------------------- | ----------------------- |
|
|
977
|
+
| `.neutral` _(default)_ | Text color, capped. |
|
|
978
|
+
| `.primary` | Primary color. |
|
|
979
|
+
| `.secondary` | Secondary/accent color. |
|
|
980
|
+
| `.success` | Success color. |
|
|
981
|
+
| `.warning` | Warning color. |
|
|
982
|
+
| `.destructive`, `.danger`, `.error` | Destructive color. |
|
|
983
|
+
| `.info` | Info color. |
|
|
959
984
|
|
|
960
|
-
|
|
961
|
-
decide how much intent fills a component; `.edged` and `.edgeless` decide
|
|
962
|
-
whether it draws a boundary. They set
|
|
963
|
-
[presentation tokens](docs/usage/customizing.md#presentation-tokens) and nothing
|
|
964
|
-
else, so they work on the component itself or on any ancestor.
|
|
985
|
+
All three toggles rest at `.soft`. Presentation decides the _unchecked_ plate; checked is pinned to one look regardless of which class is on the element, so a checkbox, a radio, and a switch each have a single "on" identity rather than one per presentation:
|
|
965
986
|
|
|
966
|
-
|
|
|
967
|
-
|
|
|
968
|
-
| `.
|
|
969
|
-
| `.soft
|
|
970
|
-
| `.
|
|
971
|
-
| `.edged` | Intent-colored boundary at the aesthetic's material width. |
|
|
972
|
-
| `.edgeless` | No boundary. |
|
|
987
|
+
| Toggle | unchecked | checked |
|
|
988
|
+
| ---------------- | --------- | ---------------------------------------- |
|
|
989
|
+
| `.ghost` | 0% | 100%, mark in the contrast tone (pinned) |
|
|
990
|
+
| `.soft`, default | 12% | 100%, mark in the contrast tone (pinned) |
|
|
991
|
+
| `.solid` | 40% | 100%, mark in the contrast tone (pinned) |
|
|
973
992
|
|
|
974
|
-
|
|
993
|
+
A checked checkbox and a checked switch cut their mark out of that pinned plate. A checked radio thickens its ring to twice the resting line and takes the intent whole on it, and the dot follows the same pinned plate, so `.radio.soft` and `.radio.solid` render the same filled circle once checked — presentation only changes what the radio looks like before it's picked.
|
|
975
994
|
|
|
976
|
-
|
|
977
|
-
| ----------- | ----------- | ----------------- | ------------- |
|
|
978
|
-
| `.solid` | `100%` | `100%` | |
|
|
979
|
-
| `.soft` | `12%` | `0%` | |
|
|
980
|
-
| `.ghost` | `0%` | `0%` | |
|
|
981
|
-
| `.edged` | | | `100%` |
|
|
982
|
-
| `.edgeless` | | | `0%` |
|
|
995
|
+
## Text controls
|
|
983
996
|
|
|
984
|
-
|
|
997
|
+
Text controls also take intent, which colors the resting border and the focus-visible border, and both [presentation](docs/usage/composing.md#presentation) axes. `.ghost` fills nothing, `.soft` takes `12%`, and `.solid` takes `20%` — quiet enough that typed text still reads on it, and ordered so the louder name draws the stronger tint. None of them touches the line.
|
|
998
|
+
|
|
999
|
+
The boundary is the edge axis's, and it splits the same way. A container's `.edgeless` is floored, so a toolbar cannot leave a field with no mark of where typing goes. `.edgeless` on the control itself is honoured, which is how the borderless field is spelled — it does not meet WCAG 1.4.11 at rest, so it is opt-in and never a default, and the focus ring still shows.
|
|
985
1000
|
|
|
986
1001
|
```html
|
|
987
|
-
<
|
|
988
|
-
<
|
|
989
|
-
<
|
|
990
|
-
<
|
|
991
|
-
<
|
|
992
|
-
|
|
1002
|
+
<input class="ipt success" placeholder="Valid" />
|
|
1003
|
+
<input class="ipt ghost edged" placeholder="Outlined" />
|
|
1004
|
+
<input class="ipt soft edged" placeholder="Tinted with a border" />
|
|
1005
|
+
<input class="ipt soft edgeless" placeholder="Sunk into the page" />
|
|
1006
|
+
<div class="soft">
|
|
1007
|
+
<input class="ipt" placeholder="Tinted from the container, capped at 6%" />
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="edgeless">
|
|
1010
|
+
<input class="ipt" placeholder="Keeps its line; the floor holds" />
|
|
1011
|
+
</div>
|
|
993
1012
|
```
|
|
994
1013
|
|
|
995
|
-
|
|
996
|
-
Use `.solid.edgeless` for a filled box with no line — and prefer it on a
|
|
997
|
-
neutral one, because a capped fill leaves the box translucent and the edge
|
|
998
|
-
blend then paints a second coat of the same tint rather than disappearing
|
|
999
|
-
into it (measured 1.53:1 against its own plate in light, 1.82:1 in dark).
|
|
1014
|
+
`.surface` is a public low-level utility. It is the box every container in the package composes: ghost, edged, surface radius, and the two slots only a surface resolves — the backdrop filter and the complete surface shadow. Use it to paint a container the package does not ship, under whichever aesthetic is in scope. Prefer `.card` or `.panel` when they fit.
|
|
1000
1015
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1016
|
+
```html
|
|
1017
|
+
<div class="surface glass">Blurred, translucent, glass corners</div>
|
|
1018
|
+
<div class="surface pixel">Cut corners and an inset ring</div>
|
|
1019
|
+
<div class="surface primary solid">Any intent, any presentation</div>
|
|
1020
|
+
```
|
|
1006
1021
|
|
|
1007
|
-
|
|
1008
|
-
buttons are filled, alerts and badges are tinted, surfaces and controls are
|
|
1009
|
-
neutral and bordered.
|
|
1022
|
+
`.text-control` is a public low-level utility from the form entrypoint. It provides the shared control dimensions, border, placeholder, focus-visible, invalid, and disabled styles composed by `ipt`, `textarea`, and `select`. Use it for custom text-like controls; prefer those higher-level utilities when they fit.
|
|
1010
1023
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1024
|
+
## Example
|
|
1025
|
+
|
|
1026
|
+
```html
|
|
1027
|
+
<label class="field">
|
|
1028
|
+
<span class="label">Email</span>
|
|
1029
|
+
<input class="ipt" type="email" aria-invalid="true" aria-describedby="email-error" />
|
|
1030
|
+
<span class="hint error" id="email-error">Enter a valid email.</span>
|
|
1031
|
+
</label>
|
|
1032
|
+
<label style="display: flex; gap: 0.5rem; align-items: center">
|
|
1033
|
+
<input type="checkbox" class="checkbox success" />
|
|
1034
|
+
<span>Accept terms</span>
|
|
1035
|
+
</label>
|
|
1036
|
+
<label style="display: flex; gap: 0.5rem; align-items: center">
|
|
1037
|
+
<input type="radio" class="radio secondary" name="plan" />
|
|
1038
|
+
<span>Standard plan</span>
|
|
1039
|
+
</label>
|
|
1040
|
+
<label style="display: flex; gap: 0.5rem; align-items: center">
|
|
1041
|
+
<input type="checkbox" class="switch destructive" />
|
|
1042
|
+
<span>Enable</span>
|
|
1043
|
+
</label>
|
|
1044
|
+
```
|
|
1023
1045
|
|
|
1024
|
-
|
|
1025
|
-
text legible when a `.solid` container reaches a field nobody classed. A
|
|
1026
|
-
fill class written on the control itself names its own cap instead, because
|
|
1027
|
-
that is a consumer describing what they want: `.soft` takes `12%` and
|
|
1028
|
-
`.solid` takes `20%`. A switch caps at `40%`, where its three fills separate
|
|
1029
|
-
far enough to read on a track.
|
|
1046
|
+
Use labels, `type`, validation logic, `aria-describedby`, and error message relationships; see [Accessibility](docs/accessibility.md).
|
|
1030
1047
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
|
1036
|
-
|
|
|
1048
|
+
<!-- Source: docs/usage/feedback.md -->
|
|
1049
|
+
|
|
1050
|
+
# Feedback
|
|
1051
|
+
|
|
1052
|
+
| Class | Purpose |
|
|
1053
|
+
| ---------------- | ---------------------------------------------------------------------------- |
|
|
1054
|
+
| `.alert` | Inline feedback surface. |
|
|
1055
|
+
| `.alert-icon` | Sizing and alignment for an icon element dropped in as a child of `.alert`. |
|
|
1056
|
+
| `.badge` | Compact status pill. |
|
|
1057
|
+
| `.loader-mask` | Low-level activity indicator composition utility. Applies the spinner mask. |
|
|
1058
|
+
| `.loader` | Standalone inline loader artwork. |
|
|
1059
|
+
| `.skeleton` | Ambient loading placeholder. |
|
|
1060
|
+
| `.progress` | Progress track. Uses `--progress-value` variable. |
|
|
1061
|
+
| `.active` | Optional on `.progress` to add a track shimmer. |
|
|
1062
|
+
| `.indeterminate` | Optional on `.progress` to animate a moving fill without `--progress-value`. |
|
|
1063
|
+
|
|
1064
|
+
Activity indicator modifier classes compose with `.loader`:
|
|
1065
|
+
|
|
1066
|
+
| Class | Animation style |
|
|
1067
|
+
| ---------------------- | --------------------------------------- |
|
|
1068
|
+
| _(default)_ | Circular spinner (rotating arc). |
|
|
1069
|
+
| `.dots-wave` | Three dots bouncing up/down in a wave. |
|
|
1070
|
+
| `.dots-fade` | Three dots fading in and out. |
|
|
1071
|
+
| `.dots-queue` | Dot queuing from left to right. |
|
|
1072
|
+
| `.dots-rotate` | Side dots rotating around a center dot. |
|
|
1073
|
+
| `.dots-grow` | Three dots growing and shrinking. |
|
|
1074
|
+
| `.dots-grow-alternate` | Outer dots small, center dot pulses. |
|
|
1075
|
+
| `.dot-bounce` | Single dot bouncing with squash effect. |
|
|
1076
|
+
| `.bars-wave` | Three vertical bars scaling in a wave. |
|
|
1077
|
+
| `.pulse-ring` | Two concentric rings pulsing outward. |
|
|
1078
|
+
|
|
1079
|
+
These variants retain their embedded SVG animations normally. Under `prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding static mask. This fallback is part of focused loader, button, and components imports; it does not depend on the global reset. See [Accessibility](docs/accessibility.md).
|
|
1080
|
+
|
|
1081
|
+
Loader size modifiers:
|
|
1082
|
+
|
|
1083
|
+
| Class | Size |
|
|
1084
|
+
| -------- | -------------------- |
|
|
1085
|
+
| _(none)_ | Default (`1.75rem`). |
|
|
1086
|
+
| `.sm` | Small (`1.25rem`). |
|
|
1087
|
+
| `.lg` | Large (`2.25rem`). |
|
|
1088
|
+
|
|
1089
|
+
Alerts, badges, progress bars, skeletons, and loaders accept `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.danger`, `.error`, and `.info`. Without an intent, they use the text palette, except `.loader`, which keeps `currentColor` so it matches whatever content surrounds it.
|
|
1090
|
+
|
|
1091
|
+
Skeletons, loaders, progress bars, and dividers are indicators. They read intent but ignore fill and edge [presentation](docs/usage/composing.md#presentation).
|
|
1037
1092
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
control reference.
|
|
1042
|
-
|
|
1043
|
-
A tooltip bubble is filled, over a tinted ground. An intent fills it with
|
|
1044
|
-
that intent's own color: `.tooltip.primary` is the primary color, black on a
|
|
1045
|
-
light page and white on a dark one, and `.tooltip.destructive` is a red
|
|
1046
|
-
bubble.
|
|
1047
|
-
|
|
1048
|
-
With no intent the bubble is `--color-tooltip`, the one plate in the package
|
|
1049
|
-
chosen per theme rather than derived from an intent. Composed like
|
|
1050
|
-
everything else it was `20%` of the page's ink over the surface tone, which
|
|
1051
|
-
steps _lighter_ than the surface on a dark page and _darker_ on a light
|
|
1052
|
-
one — so the dark bubble read as lifted and the light one as a mid-grey
|
|
1053
|
-
slab two steps darker than every card on the screen. The token states each
|
|
1054
|
-
direction instead: near-white over a light page, a mid grey over a dark
|
|
1055
|
-
one. `--color-tooltip-contrast` is its ink.
|
|
1056
|
-
|
|
1057
|
-
Near-white is only 1.04:1 against a light page, so the bubble draws a
|
|
1058
|
-
hairline as well, and floors it: a container's `.solid` or `.edgeless`
|
|
1059
|
-
cannot take the boundary away from a bubble nobody classed, while
|
|
1060
|
-
`.tooltip.edgeless` still can. The ground keeps the bubble opaque at every
|
|
1061
|
-
fill, so a cascaded presentation leaves it readable over whatever it floats
|
|
1062
|
-
above. It reads the material tokens too, so an [aesthetic](docs/usage/aesthetics.md)
|
|
1063
|
-
in scope shapes the bubble like any other component. See
|
|
1064
|
-
[Tooltips](docs/usage/tooltips.md) for the full tooltip reference.
|
|
1093
|
+
```html
|
|
1094
|
+
<span class="loader success" aria-hidden="true"></span> <span class="skeleton info"></span>
|
|
1095
|
+
```
|
|
1065
1096
|
|
|
1066
|
-
|
|
1097
|
+
The package ships no icon of its own: `.alert` is already `flex`, so an icon dropped in as a child is spaced from the message by its `gap-3` with nothing to trigger. `.alert-icon` only sizes and aligns whatever icon element you provide — an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons` — the same contract [`.input-group`](docs/usage/forms.md#icons) documents.
|
|
1067
1098
|
|
|
1068
|
-
|
|
1069
|
-
means the component reads applicable radius, border-width, shadow, hover, or
|
|
1070
|
-
clip/edge material tokens; an aesthetic may intentionally affect only the
|
|
1071
|
-
materials that component draws.
|
|
1099
|
+
Alerts and badges read the shared [presentation](docs/usage/composing.md#presentation) classes. Without one they use a tinted surface, intent-colored text, and a mixed intent border -- except a neutral `.alert` (no named intent), which rests untinted instead, the same no-named-intent carve-out `.card.soft` and `.panel` use: a plain `--color-foreground` plate rather than 12% of near-black ink over the page. A named intent (`.alert.success`, `.alert.destructive`, and so on) keeps its ordinary tint. `.badge` is unaffected either way -- it has no border to fall back on at rest, so untinting it would make a neutral badge disappear rather than read quieter.
|
|
1072
1100
|
|
|
1073
|
-
|
|
1074
|
-
the class changes what the component computes, which
|
|
1075
|
-
`tests/browser/axes.spec.ts` asserts against `registry.json` in both
|
|
1076
|
-
directions: an axis marked `Yes` must move, and an axis marked `No` must not.
|
|
1101
|
+
## Example
|
|
1077
1102
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1103
|
+
```html
|
|
1104
|
+
<div class="alert success" role="status">Saved successfully.</div>
|
|
1105
|
+
<div class="alert primary solid" role="status">Deployment started.</div>
|
|
1106
|
+
<div class="alert warning soft" role="status">
|
|
1107
|
+
<svg class="alert-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
1108
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
|
1109
|
+
<path d="M12 9v4" />
|
|
1110
|
+
<path d="M12 17h.01" />
|
|
1111
|
+
</svg>
|
|
1112
|
+
Review required.
|
|
1113
|
+
</div>
|
|
1114
|
+
<div class="alert destructive ghost edged" role="alert">Deployment failed.</div>
|
|
1115
|
+
<span class="badge warning">Queued</span>
|
|
1116
|
+
<span class="badge success solid">Live</span>
|
|
1117
|
+
<span class="badge info soft">Draft</span>
|
|
1118
|
+
<span class="loader" aria-hidden="true"></span>
|
|
1119
|
+
<span class="loader dots-wave" aria-hidden="true"></span>
|
|
1120
|
+
<span class="loader dots-fade" aria-hidden="true"></span>
|
|
1121
|
+
<span class="loader bars-wave" aria-hidden="true"></span>
|
|
1122
|
+
<span class="loader pulse-ring sm" aria-hidden="true"></span>
|
|
1123
|
+
<span class="loader dots-grow lg" aria-hidden="true"></span>
|
|
1124
|
+
<div class="progress" role="progressbar" aria-label="Upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="64" style="--progress-value: 64%"></div>
|
|
1125
|
+
<div class="progress secondary active" role="progressbar" aria-label="Upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="64" style="--progress-value: 64%"></div>
|
|
1126
|
+
<div class="progress info indeterminate" role="progressbar" aria-label="Loading" aria-valuemin="0" aria-valuemax="100"></div>
|
|
1127
|
+
```
|
|
1094
1128
|
|
|
1095
|
-
|
|
1096
|
-
the other. Their line is the only thing marking an unchecked box, so it is
|
|
1097
|
-
drawn whatever the edge class says, from a container or from the element
|
|
1098
|
-
itself. `.edgeless` on either is unsupported rather than merely discouraged.
|
|
1129
|
+
Add `role="status"` or `role="alert"` to alerts based on announcement urgency, mark decorative loading visuals with `aria-hidden="true"`, and use semantic progress elements or ARIA values when numeric progress must be announced; see [Accessibility](docs/accessibility.md).
|
|
1099
1130
|
|
|
1100
|
-
|
|
1101
|
-
a `.edgeless` toolbar will not erase the line of a field nobody classed, while
|
|
1102
|
-
`.ipt.edgeless` is a consumer describing what they want and gets it.
|
|
1131
|
+
<!-- Source: docs/usage/tooltips.md -->
|
|
1103
1132
|
|
|
1104
|
-
|
|
1105
|
-
`.destructive`; they do not add component behavior. State and modifier
|
|
1106
|
-
classes such as `.interactive`, `.loading`, `.invalid`, `.compact`, and
|
|
1107
|
-
`.vertical` sit above this map and are documented with their component.
|
|
1133
|
+
# Tooltips
|
|
1108
1134
|
|
|
1109
|
-
|
|
1135
|
+
| Class or Attribute | Purpose |
|
|
1136
|
+
| -------------------------------------- | ------------------------------------------------------------------------ |
|
|
1137
|
+
| `.tooltip` | Wrapper around a trigger and a bubble. Paints nothing itself. |
|
|
1138
|
+
| `.tooltip-bubble` | The message. A real element, styled like any other surface. |
|
|
1139
|
+
| `.tooltip-icon` | Circular icon-style trigger, for a message with nothing else to hang on. |
|
|
1140
|
+
| `[data-tooltip-position="top"]` | Positions the bubble above the trigger, on `.tooltip`. |
|
|
1141
|
+
| `[data-tooltip-position="bottom"]` | Positions the bubble below the trigger, on `.tooltip`. |
|
|
1142
|
+
| `[data-tooltip-position="left"]` | Positions the bubble left of the trigger, on `.tooltip`. |
|
|
1143
|
+
| `[data-tooltip-position="right"]` | Positions the bubble right of the trigger, on `.tooltip`. |
|
|
1144
|
+
| No `data-tooltip-position` | Defaults to top placement. |
|
|
1145
|
+
| `[data-state="open"]` | Shows the bubble without hover, on `.tooltip`. |
|
|
1146
|
+
| `.tooltip-bubble.raised` / `.floating` | Adds one or two units of depth. The bubble is flat by default. |
|
|
1110
1147
|
|
|
1111
|
-
|
|
1112
|
-
[aesthetic](docs/usage/aesthetics.md) draws depth or one of these classes asks for it.
|
|
1148
|
+
A tooltip is composition, not a single class: `.tooltip` wraps a trigger element and a `.tooltip-bubble`, and shows the bubble on hover or focus within the wrapper. This replaced a `::after` pseudo-element that carried the message as `content: attr(data-tooltip)` — CSS generated content is not reliably exposed to assistive tech, and cannot be the target of an `aria-describedby`, since that needs a real element with a real `id`. A real bubble can be. Wire the association yourself: give the bubble an `id`, give the trigger `aria-describedby` pointing at it, and put `role="tooltip"` on the bubble.
|
|
1113
1149
|
|
|
1114
|
-
|
|
1115
|
-
| ----------------- | ----------- | ------------------------------------------------------------------------ |
|
|
1116
|
-
| `.elevation-none` | `.flat` | Removes part-based elevation; glass surfaces keep their complete shadow. |
|
|
1117
|
-
| `.elevation-sm` | `.raised` | One unit of the depth in scope. Cards, tiles, popovers that sit close. |
|
|
1118
|
-
| `.elevation-md` | `.floating` | Twice it, for menus and popovers. |
|
|
1150
|
+
A tooltip bubble's fill, ground color, and floor behavior are covered in [Composing → Presentation](docs/usage/composing.md#presentation), and its material follows whichever [aesthetic](docs/usage/aesthetics.md) is in scope. Intent, presentation, aesthetic, and elevation classes go on `.tooltip-bubble` — the element that actually paints — not on `.tooltip`, the same rule every component in this package follows: put the class on the element that shows it.
|
|
1119
1151
|
|
|
1120
|
-
|
|
1121
|
-
and `.raised` are aliases, not a canonical form and a deprecated one, the way
|
|
1122
|
-
`.destructive` and `.danger` are on the intent axis. The `none`/`sm`/`md`
|
|
1123
|
-
naming matches the raw `--elevation-*` tokens in
|
|
1124
|
-
[Theming](docs/usage/theming.md#foundation-tokens) for the same weight of depth, even
|
|
1125
|
-
though the two are read from different places.
|
|
1152
|
+
The bubble rests flat. It draws no shadow of its own on a plain page, and zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the same way `.flat` does elsewhere. Add [`.raised` or `.floating`](docs/usage/composing.md#elevation) to lift it, on the bubble itself — `.tooltip-bubble.floating` is the pre-0.1.0 look. An aesthetic whose shadow is a complete value, like `.glass`, still reaches the bubble as it reaches any surface.
|
|
1126
1153
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
class decides how much of it this element takes.
|
|
1154
|
+
Intent works on a tooltip bubble the same way it works on a filled component: `.tooltip-bubble.primary` fills the bubble with the primary color, black text on a light page and white on a dark one; `.tooltip-bubble.destructive` is a red bubble with the same logic. With no intent, the bubble uses `--color-tooltip`, a plate chosen per theme rather than derived from an intent; see [Composing → Presentation](docs/usage/composing.md#presentation) for why.
|
|
1155
|
+
|
|
1156
|
+
## Example
|
|
1131
1157
|
|
|
1132
1158
|
```html
|
|
1133
|
-
<
|
|
1134
|
-
<
|
|
1135
|
-
<
|
|
1159
|
+
<span class="tooltip">
|
|
1160
|
+
<button aria-describedby="status-bubble">Status</button>
|
|
1161
|
+
<span class="tooltip-bubble" role="tooltip" id="status-bubble">Saved 2 minutes ago</span>
|
|
1162
|
+
</span>
|
|
1163
|
+
|
|
1164
|
+
<span class="tooltip" data-tooltip-position="bottom">
|
|
1165
|
+
<button aria-describedby="docs-bubble">Docs</button>
|
|
1166
|
+
<span class="tooltip-bubble" role="tooltip" id="docs-bubble">Opens in a new tab</span>
|
|
1167
|
+
</span>
|
|
1168
|
+
|
|
1169
|
+
<span class="tooltip">
|
|
1170
|
+
<button class="btn primary" aria-describedby="deploy-bubble">Deploy</button>
|
|
1171
|
+
<span class="tooltip-bubble raised primary" role="tooltip" id="deploy-bubble">Primary intent, lifted a step</span>
|
|
1172
|
+
</span>
|
|
1173
|
+
|
|
1174
|
+
<span class="tooltip" data-state="open">
|
|
1175
|
+
<span aria-describedby="pinned-bubble">Pinned</span>
|
|
1176
|
+
<span class="tooltip-bubble" role="tooltip" id="pinned-bubble">Always visible, no hover needed</span>
|
|
1177
|
+
</span>
|
|
1178
|
+
|
|
1179
|
+
<span class="tooltip" data-tooltip-position="right">
|
|
1180
|
+
<span class="tooltip-icon" tabindex="0" aria-label="More details" aria-describedby="icon-bubble">?</span>
|
|
1181
|
+
<span class="tooltip-bubble" role="tooltip" id="icon-bubble">More details</span>
|
|
1182
|
+
</span>
|
|
1136
1183
|
```
|
|
1137
1184
|
|
|
1138
|
-
The
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1185
|
+
The first two show plain triggers with default (top) and explicit bottom placement. The third combines an intent with `.raised` on the bubble to show they compose freely — the intent decides the bubble's fill, `.raised` decides whether it casts depth. The fourth uses `data-state="open"` on the wrapper to keep a tooltip visible without a hover or focus interaction, useful for a tooltip that should be demonstrated or tested without simulating pointer input. The fifth uses `.tooltip-icon` as the trigger, for a message with no other element to hang it on.
|
|
1186
|
+
|
|
1187
|
+
`.tooltip` composes with any trigger: wrap an existing `.btn` or `.badge` in it and both keep their own look, gaining a bubble beside them. `.tooltip-icon` is only the circular chip for the case where there is nothing else to be the trigger.
|
|
1188
|
+
|
|
1189
|
+
Showing and hiding the bubble is CSS-only — `:hover` and `:focus-within` on `.tooltip` — but that is presentational. It is not a complete accessible tooltip implementation by itself: provide the `id`/`aria-describedby`/`role="tooltip"` association outside the CSS; see [Accessibility](docs/accessibility.md).
|
|
1190
|
+
|
|
1191
|
+
Next: [Content and layout](docs/usage/content-and-layout.md) covers layout helpers, content chips and tables, surfaces, and typography utilities.
|
|
1143
1192
|
|
|
1144
1193
|
<!-- Source: docs/usage/content-and-layout.md -->
|
|
1145
1194
|
|
|
@@ -1147,26 +1196,19 @@ would erase the edge.
|
|
|
1147
1196
|
|
|
1148
1197
|
## Layout
|
|
1149
1198
|
|
|
1150
|
-
Layout helpers use the shared `--layout-gap` token. `.tight` sets it to
|
|
1151
|
-
`0.5rem` and `.loose` sets it to `1.5rem` within a view, stack, cluster, or
|
|
1152
|
-
auto-grid.
|
|
1199
|
+
Layout helpers use the shared `--layout-gap` token. `.tight` sets it to `0.5rem` and `.loose` sets it to `1.5rem` within a view, stack, cluster, or auto-grid.
|
|
1153
1200
|
|
|
1154
|
-
- `.view` is a flex container; `.vertical` and `.horizontal` set its
|
|
1155
|
-
direction, and horizontal views wrap with centered cross-axis alignment.
|
|
1201
|
+
- `.view` is a flex container; `.vertical` and `.horizontal` set its direction, and horizontal views wrap with centered cross-axis alignment.
|
|
1156
1202
|
- `.stack` is a vertical flex stack.
|
|
1157
|
-
- `.cluster` is a wrapping horizontal flex row; `.between` adds
|
|
1158
|
-
`space-between` alignment.
|
|
1203
|
+
- `.cluster` is a wrapping horizontal flex row; `.between` adds `space-between` alignment.
|
|
1159
1204
|
- `.auto-grid` is a responsive auto-fit grid using `--layout-grid-min`.
|
|
1160
|
-
- `.tight` and `.loose` set the shared gap to `0.5rem` or `1.5rem` on views,
|
|
1161
|
-
stacks, clusters, and auto-grids.
|
|
1205
|
+
- `.tight` and `.loose` set the shared gap to `0.5rem` or `1.5rem` on views, stacks, clusters, and auto-grids.
|
|
1162
1206
|
- `.section` adds responsive block padding and an inline gutter.
|
|
1163
|
-
- `.section-content` centers content at `--container-max`; `.narrow` and
|
|
1164
|
-
|
|
1165
|
-
- `.
|
|
1166
|
-
takes intent, but presentation classes do not affect it.
|
|
1207
|
+
- `.section-content` centers content at `--container-max`; `.narrow` and `.wide` select the corresponding container tokens.
|
|
1208
|
+
- `.divider` is horizontal; `.vertical` makes it self-stretch vertically. It takes intent, but presentation classes do not affect it.
|
|
1209
|
+
- `.visually-hidden` hides content visually while keeping it in the accessibility tree, using the standard clip-based recipe. Not named `.sr-only`: on a `/tw` entry the consumer's own Tailwind build already ships a real `.sr-only`, and a same-named utility of this package's own would silently collide with it in the cascade — the same reason `.data-table` is not named `.table`.
|
|
1167
1210
|
|
|
1168
|
-
The removed `--layout-stack-gap` and `--layout-cluster-gap` tokens have no
|
|
1169
|
-
compatibility aliases.
|
|
1211
|
+
The removed `--layout-stack-gap` and `--layout-cluster-gap` tokens have no compatibility aliases.
|
|
1170
1212
|
|
|
1171
1213
|
## Content
|
|
1172
1214
|
|
|
@@ -1184,8 +1226,7 @@ compatibility aliases.
|
|
|
1184
1226
|
|
|
1185
1227
|
### Table rules
|
|
1186
1228
|
|
|
1187
|
-
A table draws three kinds of line, and `.ruled` and `.ruleless` move all of
|
|
1188
|
-
them together:
|
|
1229
|
+
A table draws three kinds of line, and `.ruled` and `.ruleless` move all of them together:
|
|
1189
1230
|
|
|
1190
1231
|
| Class | Head and foot boundaries | Rules between body rows |
|
|
1191
1232
|
| ----------- | ------------------------ | ----------------------- |
|
|
@@ -1194,36 +1235,32 @@ them together:
|
|
|
1194
1235
|
| `.ruled` | Yes | Yes |
|
|
1195
1236
|
|
|
1196
1237
|
```html
|
|
1197
|
-
<table class="data-table">
|
|
1198
|
-
|
|
1199
|
-
|
|
1238
|
+
<table class="data-table">
|
|
1239
|
+
Head and foot boundaries only
|
|
1240
|
+
</table>
|
|
1241
|
+
<table class="data-table ruled">
|
|
1242
|
+
Every row separated
|
|
1243
|
+
</table>
|
|
1244
|
+
<table class="data-table ruleless">
|
|
1245
|
+
No lines inside the table at all
|
|
1246
|
+
</table>
|
|
1200
1247
|
```
|
|
1201
1248
|
|
|
1202
|
-
The boundaries under a head and above a foot separate the parts of a table
|
|
1203
|
-
from each other, so they are drawn by default where the row rules are not.
|
|
1204
|
-
Rules between body rows used to arrive on their own, because the
|
|
1205
|
-
component's published edge default is `edged` and the edge axis wrote the
|
|
1206
|
-
rules along with the boundary — so whether you got them depended on a
|
|
1207
|
-
registry default rather than on anything in your markup.
|
|
1249
|
+
The boundaries under a head and above a foot separate the parts of a table from each other, so they are drawn by default where the row rules are not. Rules between body rows used to arrive on their own, because the component's published edge default is `edged` and the edge axis wrote the rules along with the boundary — so whether you got them depended on a registry default rather than on anything in your markup.
|
|
1208
1250
|
|
|
1209
|
-
`--ui-rule` is the token behind both classes, so a container can set the
|
|
1210
|
-
answer for a whole region without classing each table:
|
|
1251
|
+
`--ui-rule` is the token behind both classes, so a container can set the answer for a whole region without classing each table:
|
|
1211
1252
|
|
|
1212
1253
|
```html
|
|
1213
1254
|
<section style="--ui-rule: 100%">
|
|
1214
|
-
<table class="data-table">
|
|
1255
|
+
<table class="data-table">
|
|
1256
|
+
Ruled by the region
|
|
1257
|
+
</table>
|
|
1215
1258
|
</section>
|
|
1216
1259
|
```
|
|
1217
1260
|
|
|
1218
|
-
The frame around the table is the edge axis and is asked for the same way:
|
|
1219
|
-
`.data-table` publishes `edgeless`, so `.edged` draws one. None of the
|
|
1220
|
-
three switches reaches the others — `.data-table.ruleless.edged` is a
|
|
1221
|
-
framed table with no lines inside it, and `.data-table.ruled` is a
|
|
1222
|
-
frameless one with every row separated.
|
|
1261
|
+
The frame around the table is the edge axis and is asked for the same way: `.data-table` publishes `edgeless`, so `.edged` draws one. None of the three switches reaches the others — `.data-table.ruleless.edged` is a framed table with no lines inside it, and `.data-table.ruled` is a frameless one with every row separated.
|
|
1223
1262
|
|
|
1224
|
-
A table's rows deliberately inherit its intent instead of resetting it, so
|
|
1225
|
-
`.data-table.success` tints throughout. A row carrying its own intent still
|
|
1226
|
-
wins:
|
|
1263
|
+
A table's rows deliberately inherit its intent instead of resetting it, so `.data-table.success` tints throughout. A row carrying its own intent still wins:
|
|
1227
1264
|
|
|
1228
1265
|
```html
|
|
1229
1266
|
<table class="data-table success">
|
|
@@ -1238,37 +1275,30 @@ wins:
|
|
|
1238
1275
|
</table>
|
|
1239
1276
|
```
|
|
1240
1277
|
|
|
1241
|
-
This is the one place intent cascades, because a table's rows are parts of
|
|
1242
|
-
the table rather than independent components.
|
|
1278
|
+
This is the one place intent cascades, because a table's rows are parts of the table rather than independent components.
|
|
1243
1279
|
|
|
1244
|
-
`.data-table`, `.kbd`, `.code`, and `.pre` also read
|
|
1245
|
-
[presentation](docs/usage/composing.md#presentation). A table applies it to its
|
|
1246
|
-
header, its rules and its border; a chip to its whole plate:
|
|
1280
|
+
`.data-table`, `.kbd`, `.code`, and `.pre` also read [presentation](docs/usage/composing.md#presentation). A table applies it to its header, its rules and its border; a chip to its whole plate:
|
|
1247
1281
|
|
|
1248
1282
|
```html
|
|
1249
|
-
<table class="data-table success soft edged"
|
|
1250
|
-
|
|
1283
|
+
<table class="data-table success soft edged">
|
|
1284
|
+
...
|
|
1285
|
+
</table>
|
|
1286
|
+
<table class="data-table">
|
|
1287
|
+
A ghost table: rules and type, no header plate
|
|
1288
|
+
</table>
|
|
1251
1289
|
<kbd class="kbd primary solid">Ctrl</kbd>
|
|
1252
1290
|
<kbd class="kbd primary soft edged">Shift</kbd>
|
|
1253
1291
|
```
|
|
1254
1292
|
|
|
1255
|
-
`.ghost` means two different things on these components, and both are worth
|
|
1256
|
-
|
|
1293
|
+
`.ghost` means two different things on these components, and both are worth knowing before you reach for it.
|
|
1294
|
+
|
|
1295
|
+
On `.data-table` it removes the header plate along with the body tint, so `.data-table.ghost` is boundaries and type alone. The component rests at `soft`, so that is something to ask for rather than the default.
|
|
1257
1296
|
|
|
1258
|
-
|
|
1259
|
-
`.data-table.ghost` is boundaries and type alone. The component rests at
|
|
1260
|
-
`soft`, so that is something to ask for rather than the default.
|
|
1297
|
+
`.soft` rests untinted throughout -- `--color-foreground`, no intent color mixed in, for every intent, including the resting default, on the head plate and the table's own base fill alike. A table has no hover or interaction state to reveal a tint into the way `.card.soft` does on `.interactive`/`.hoverable`, so unlike a card there is no state where a `.data-table.success.soft` reveals green; only the boundary and rule lines still read the intent.
|
|
1261
1298
|
|
|
1262
|
-
On `.kbd`, `.code`, and `.pre` it is **not supported**. All three rest on
|
|
1263
|
-
`--intent-subtle`, and that ground draws the plate whatever the fill says —
|
|
1264
|
-
so `.ghost` took the fill away and changed nothing visible. Worse, the
|
|
1265
|
-
ground alone is a near-page tint: a ghost chip measured the same `#e5e5e5`
|
|
1266
|
-
for neutral and primary on the light page, and `#f5f5f5` at 1.04:1 against
|
|
1267
|
-
it for secondary. All three rest at `.soft` instead, which puts a real `12%`
|
|
1268
|
-
of the intent over the ground and separates them.
|
|
1299
|
+
On `.kbd`, `.code`, and `.pre` it is **not supported**. All three rest on `--intent-subtle`, and that ground draws the plate whatever the fill says — so `.ghost` took the fill away and changed nothing visible. Worse, the ground alone is a near-page tint: a ghost chip measured the same `#e5e5e5` for neutral and primary on the light page, and `#f5f5f5` at 1.04:1 against it for secondary. A named intent rests at `.soft` instead, which puts a real `12%` of the intent over the ground and separates it from the others; the no-named-intent case reads no differently from `.ghost` either way, so it rests at the ground alone rather than adding an unmotivated wash of its own.
|
|
1269
1300
|
|
|
1270
|
-
Use `.table-wrap` around `.data-table` when table width may exceed its
|
|
1271
|
-
container:
|
|
1301
|
+
Use `.table-wrap` around `.data-table` when table width may exceed its container:
|
|
1272
1302
|
|
|
1273
1303
|
```html
|
|
1274
1304
|
<div class="table-wrap">
|
|
@@ -1291,38 +1321,38 @@ container:
|
|
|
1291
1321
|
|
|
1292
1322
|
## Surfaces
|
|
1293
1323
|
|
|
1294
|
-
| Class | Purpose
|
|
1295
|
-
| -------------------- |
|
|
1296
|
-
| `.card` | Bordered container, surface radius, padded. Rests at elevation `1`.
|
|
1297
|
-
| `.panel` | Flush container for sidebars, toolbars, and wells. No elevation.
|
|
1298
|
-
| `.interactive` | On `.card`. Adds pointer cursor, hover and press response, and a focus ring.
|
|
1299
|
-
| `.
|
|
1300
|
-
| `.
|
|
1301
|
-
| `.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
at elevation `1` is not a visible shadow by itself — nothing draws depth
|
|
1307
|
-
|
|
1308
|
-
`.
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
(`--ui-active-transform`, `scale(0.97)`); an [aesthetic](docs/usage/aesthetics.md)
|
|
1313
|
-
|
|
1324
|
+
| Class | Purpose |
|
|
1325
|
+
| -------------------- | ------------------------------------------------------------------------------------- |
|
|
1326
|
+
| `.card` | Bordered container, surface radius, padded. Rests at elevation `1`. |
|
|
1327
|
+
| `.panel` | Flush container for sidebars, toolbars, and wells. No elevation. |
|
|
1328
|
+
| `.interactive` | On `.card`. Adds pointer cursor, hover and press response, and a focus ring. |
|
|
1329
|
+
| `.hoverable` | On `.card`. The hover half of `.interactive`: hover response only, no pointer cursor. |
|
|
1330
|
+
| `.pressable` | On `.card`. The press half of `.interactive`: press response and pointer cursor. |
|
|
1331
|
+
| `.p-xs`, `.dense` | On `.card` or `.panel`. Reduces padding further than `.compact`. |
|
|
1332
|
+
| `.p-sm`, `.compact` | On `.card` or `.panel`. Reduces padding. |
|
|
1333
|
+
| `.p-lg`, `.spacious` | On `.card` or `.panel`. Increases padding. |
|
|
1334
|
+
| `.flush` | On `.card` or `.panel`. Removes padding, for edge-to-edge content. |
|
|
1335
|
+
|
|
1336
|
+
Both read intent, [presentation](docs/usage/composing.md#presentation), and [material tokens](docs/usage/customizing.md#material-tokens). A plain `.card` is a neutral bordered container; only an explicit presentation tints it. Resting at elevation `1` is not a visible shadow by itself — nothing draws depth until an [aesthetic](docs/usage/aesthetics.md) supplies shadow geometry or `.elevation-sm`/`.elevation-md` asks for it; see [Composing → Elevation](docs/usage/composing.md#elevation).
|
|
1337
|
+
|
|
1338
|
+
`.card.soft` with no named intent is untinted at rest — a plain `--color-foreground` plate, no ink mixed in, marking a different container without drawing attention to it. The intent tint and a `--color-surface` ground only appear together, on `:hover`, and only on `.card.interactive`/`.card.hoverable`: a plain `.card.soft` with neither has no hover state to reveal into and stays at the quiet rest. A named intent (`.card.soft.success`, `.card.soft.destructive`, and so on) keeps its ordinary tint at rest instead, and deepens further on hover the same way any other filled surface does — only the no-intent case is untinted, since removing an intent's own color would silence it rather than quiet it. `.solid` and `.ghost` are unaffected either way.
|
|
1339
|
+
|
|
1340
|
+
`.panel` with no named intent behaves the same way, permanently rather than on hover: a plain `.panel` rests at the same untinted `--color-foreground` plate a neutral `.card.soft` does, because a panel has no hover state to reveal a tint into. `.panel.primary`, `.panel.destructive`, and every other named intent keep their ordinary tint. `.kbd`, `.code`, and `.pre` follow the same no-named-intent rule for their own fill, without moving off their `--intent-subtle` ground.
|
|
1341
|
+
|
|
1342
|
+
On the base look, `.card.interactive` scales down slightly on press (`--ui-active-transform`, `scale(0.97)`); an [aesthetic](docs/usage/aesthetics.md) substitutes its own press gesture, and `prefers-reduced-motion` turns it off.
|
|
1343
|
+
|
|
1344
|
+
`.interactive` is the pair `.hoverable` (hover response only, no pointer cursor) and `.pressable` (press response and pointer cursor) applied together. Take one half for a card that answers a single gesture: a card that warms on hover but holds its own action in a nested button takes `.hoverable`, so pressing that button does not dip the whole card.
|
|
1314
1345
|
|
|
1315
1346
|
```html
|
|
1316
1347
|
<article class="card">Neutral card</article>
|
|
1317
|
-
<article class="card success
|
|
1348
|
+
<article class="card success solid">Filled success card</article>
|
|
1318
1349
|
<article class="card primary ghost edged">Intent border, no fill</article>
|
|
1319
|
-
<a class="card interactive" href="/package">
|
|
1350
|
+
<a class="card success soft interactive" href="/package">Success-tinted at rest; deepens on hover</a>
|
|
1351
|
+
<article class="card hoverable">Warms on hover; the action is a nested button</article>
|
|
1320
1352
|
<aside class="panel">Flush panel</aside>
|
|
1321
1353
|
```
|
|
1322
1354
|
|
|
1323
|
-
`.interactive`
|
|
1324
|
-
an accessible name; a `<div class="card interactive">` is not focusable or
|
|
1325
|
-
operable by keyboard.
|
|
1355
|
+
`.interactive` and `.pressable` are styling only. Use a real interactive element and give it an accessible name; a `<div class="card interactive">` is not focusable or operable by keyboard.
|
|
1326
1356
|
|
|
1327
1357
|
## Typography utilities
|
|
1328
1358
|
|
|
@@ -1339,784 +1369,437 @@ All typography classes and `.selection-contrast` are `@utility` classes.
|
|
|
1339
1369
|
| `.text-body` | Default body copy. |
|
|
1340
1370
|
| `.selection-contrast` | Inverts `::selection` colors to primary-contrast background. |
|
|
1341
1371
|
|
|
1342
|
-
<!-- Source: docs/
|
|
1372
|
+
<!-- Source: docs/integrating/index.md -->
|
|
1343
1373
|
|
|
1344
|
-
#
|
|
1374
|
+
# Integrating
|
|
1345
1375
|
|
|
1346
|
-
|
|
1347
|
-
decide what a component is made of. Aesthetic tokens are the small set of
|
|
1348
|
-
named knobs a shipped aesthetic exposes to scale its own look.
|
|
1376
|
+
This package has no JavaScript and no framework bindings; see [Introduction](docs/index.md#what-it-does-not-do). Integrating it into a framework is entirely about where that framework lets you load a global stylesheet, and whether the project runs Tailwind CSS v4.
|
|
1349
1377
|
|
|
1350
|
-
##
|
|
1378
|
+
## The shared model
|
|
1351
1379
|
|
|
1352
|
-
|
|
1353
|
-
never _which_ intent it shows. They are unitless numbers and percentages, so
|
|
1354
|
-
they carry no color and inherit safely: a container sets them once and every
|
|
1355
|
-
component below resolves them against its own intent.
|
|
1380
|
+
Every stack does the same two imports, from the same kind of place — a root layout, an app entry file, or a global CSS file, never a page or a component further down the tree:
|
|
1356
1381
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
| `--ui-fill` | Percentage of the intent color mixed into the background. |
|
|
1360
|
-
| `--ui-fg-on-fill` | Percentage blended from the intent readable tone toward its contrast tone. |
|
|
1361
|
-
| `--ui-border` | Percentage of the intent color mixed into the border color. |
|
|
1382
|
+
1. One [entrypoint](docs/setup.md#import-paths) — the compiled `@codenhub/styles` for any build, or `@codenhub/styles/tw` when the project already runs Tailwind CSS v4.
|
|
1383
|
+
2. `@codenhub/styles/aesthetics` (or `/tw/aesthetics`) after it, only if the project uses an [aesthetic](docs/usage/aesthetics.md).
|
|
1362
1384
|
|
|
1363
|
-
|
|
1364
|
-
is derived by adding `--hover-step` to the resting fill and using
|
|
1365
|
-
`--intent-hover`; there are no per-presentation hover tokens.
|
|
1385
|
+
Importing from one place matters for two reasons. Some frameworks only _allow_ global CSS from a designated root file and fail the build otherwise — Next.js's Pages Router is one, see [Next.js → Troubleshooting](docs/integrating/nextjs.md#troubleshooting). Where a framework allows it more broadly, as Next.js's App Router does, importing from the root remains the recommended choice for predictable CSS ordering — and importing per-component instead would load the same reset and token rules once per component rather than once per app.
|
|
1366
1386
|
|
|
1367
|
-
|
|
1368
|
-
filled box with no line is `.solid.edgeless`, which is worth reaching for on
|
|
1369
|
-
a neutral component: neutral caps its fill, so the box stays translucent and
|
|
1370
|
-
a border over it paints a second coat of the same tint instead of blending
|
|
1371
|
-
into it.
|
|
1387
|
+
## Which guide to follow
|
|
1372
1388
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1389
|
+
| Stack | Guide |
|
|
1390
|
+
| ---------------------------------------- | -------------------------------- |
|
|
1391
|
+
| A Tailwind CSS v4 project on any bundler | [Tailwind CSS v4](docs/integrating/tailwind.md) |
|
|
1392
|
+
| Next.js / React | [Next.js](docs/integrating/nextjs.md) |
|
|
1393
|
+
| Vue | [Vue](docs/integrating/vue.md) |
|
|
1394
|
+
| Svelte / SvelteKit | [Svelte](docs/integrating/svelte.md) |
|
|
1395
|
+
| Astro | [Astro](docs/integrating/astro.md) |
|
|
1396
|
+
|
|
1397
|
+
A project with no framework — a plain Vite, webpack, or Parcel build — needs nothing beyond [Setup → Quick start](docs/setup.md#quick-start): import the compiled stylesheet from the project's existing global CSS entry.
|
|
1398
|
+
|
|
1399
|
+
## Verifying it worked
|
|
1400
|
+
|
|
1401
|
+
After the import, render one styled element and confirm it picked up the package's tokens rather than the browser default:
|
|
1402
|
+
|
|
1403
|
+
```html
|
|
1404
|
+
<button class="btn primary">Continue</button>
|
|
1375
1405
|
```
|
|
1376
1406
|
|
|
1377
|
-
|
|
1378
|
-
background token lets a tinted surface adapt to whatever it is placed on.
|
|
1379
|
-
Mixing in `oklab` keeps the wide-gamut token palette intact and blends
|
|
1380
|
-
perceptually, where `srgb` would clip it.
|
|
1407
|
+
A rounded, filled button in the primary color means the stylesheet loaded. Unstyled browser-default text most often means the import specifier didn't resolve — check the [import paths table](docs/setup.md#import-paths) for the exact one your entrypoint needs, and confirm your build tooling resolves package CSS imports at all, see [Setup → Requirements](docs/setup.md#requirements).
|
|
1381
1408
|
|
|
1382
|
-
|
|
1383
|
-
rather than the token's own `oklch` syntax. It is the same color; code that
|
|
1384
|
-
compares computed color strings should compare colors instead.
|
|
1409
|
+
<!-- Source: docs/integrating/tailwind.md -->
|
|
1385
1410
|
|
|
1386
|
-
|
|
1387
|
-
> overrides set these tokens; components only read them. Setting a value
|
|
1388
|
-
> that no presentation class produces is supported but unvalidated. See
|
|
1389
|
-
> [Composing](docs/usage/composing.md) for the classes that ship these values.
|
|
1411
|
+
# Tailwind CSS v4
|
|
1390
1412
|
|
|
1391
|
-
|
|
1413
|
+
A project that already runs Tailwind CSS v4 can process this package's source instead of its compiled output, so both sets of classes compile through the same Tailwind build.
|
|
1392
1414
|
|
|
1393
|
-
|
|
1394
|
-
intent it shows or how strongly. They control shape, borders, shadows,
|
|
1395
|
-
translucency, filters, and transforms.
|
|
1415
|
+
## Why source instead of compiled
|
|
1396
1416
|
|
|
1397
|
-
|
|
1398
|
-
default look. Setting them on a container restyles the whole subtree. Only
|
|
1399
|
-
shadow geometry is split into colorless parts that inherit safely;
|
|
1400
|
-
color-capable inputs such as `--ui-ink` and `--ui-surface-shadow` may
|
|
1401
|
-
include their own colors.
|
|
1417
|
+
`/tw` entrypoints publish copied, uncompiled source from `dist/tw`; your own Tailwind build compiles it alongside your app, so its `@theme`, `@utility`, and `@apply` directives resolve together with yours. The compiled entrypoints (`@codenhub/styles`) still work in a Tailwind project, but then two separate CSS pipelines run side by side instead of one — use `/tw` unless a specific reason rules it out.
|
|
1402
1418
|
|
|
1403
|
-
|
|
1404
|
-
| ----------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
1405
|
-
| `--ui-radius` | Corner radius for controls. | `--radius-control` |
|
|
1406
|
-
| `--ui-radius-surface` | Corner radius for surfaces. | `--radius-surface` |
|
|
1407
|
-
| `--ui-border-width` | Edge thickness. | `--border-width` |
|
|
1408
|
-
| `--ui-border-max` | Ceiling on computed edge width. | `100px` |
|
|
1409
|
-
| `--ui-ink` | Neutral line color when no intent is set. | `--color-border` |
|
|
1410
|
-
| `--ui-rule` | Lines inside a table. Unset draws the head and foot boundaries only; `0%` draws none; `100%` rules every row. | unset |
|
|
1411
|
-
| `--ui-shadow-x` | Shadow horizontal offset. | `0px` |
|
|
1412
|
-
| `--ui-shadow-y` | Shadow vertical offset. | `0px` |
|
|
1413
|
-
| `--ui-shadow-blur` | Shadow blur radius. | `0px` |
|
|
1414
|
-
| `--ui-shadow-spread` | Shadow spread radius. | `0px` |
|
|
1415
|
-
| `--ui-shadow-inset` | The `inset` keyword for an inner ring. | Empty |
|
|
1416
|
-
| `--ui-hover-shadow-x` | Shadow horizontal offset while hovered. | `--ui-shadow-x` |
|
|
1417
|
-
| `--ui-hover-shadow-y` | Shadow vertical offset while hovered. | `--ui-shadow-y` |
|
|
1418
|
-
| `--ui-active-shadow-x` | Shadow horizontal offset while pressed. | `--ui-shadow-x` |
|
|
1419
|
-
| `--ui-active-shadow-y` | Shadow vertical offset while pressed. | `--ui-shadow-y` |
|
|
1420
|
-
| `--ui-active-transform` | Transform while pressed. `none` under `prefers-reduced-motion`. | `scale(0.97)` |
|
|
1421
|
-
| `--ui-shadow-ink` | Percentage of shadow color taken from intent ink. | `0%` |
|
|
1422
|
-
| `--ui-shadow-edge` | Declared, even empty, when the shadow is the element's edge rather than its depth. Read for presence. | Unset |
|
|
1423
|
-
| `--ui-elevation` | Unitless multiplier over shadow geometry. | `1` |
|
|
1424
|
-
| `--ui-surface-shadow` | Complete multi-layer shadow accepted by surfaces only. | Unset |
|
|
1425
|
-
| `--ui-surface-ground` | Ground a surface sits on. | `--color-background` |
|
|
1426
|
-
| `--ui-bg-alpha` | Multiplier over fill for translucent materials. | `1` |
|
|
1427
|
-
| `--ui-backdrop` | Backdrop filter accepted by surfaces only. | `none` |
|
|
1428
|
-
| `--ui-hover-transform` | Transform while an interactive element is hovered. | `none` |
|
|
1429
|
-
| `--ui-clip` | Structural component silhouette. | `none` |
|
|
1430
|
-
| `--ui-clip-tight` | Compact component silhouette. | `--ui-clip` |
|
|
1431
|
-
| `--ui-focus-inset` | Inset focus layer width. | Unset |
|
|
1419
|
+
## Import the source entrypoint
|
|
1432
1420
|
|
|
1433
|
-
```
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1421
|
+
```css
|
|
1422
|
+
@import "tailwindcss";
|
|
1423
|
+
@import "@codenhub/styles/tw";
|
|
1424
|
+
|
|
1425
|
+
@source "./src";
|
|
1438
1426
|
```
|
|
1439
1427
|
|
|
1440
|
-
|
|
1441
|
-
> consumer overrides set them.
|
|
1428
|
+
Tailwind CSS v4 scans your project automatically — most projects need nothing beyond the two imports above. `@source` paths resolve relative to the CSS file that declares them, not your project root, so `./src` here means "the `src/` next to this stylesheet"; adjust it to match where your own global stylesheet actually lives. Add `@source` only for markup Tailwind's automatic scan won't reach on its own: a path excluded by default (`.gitignore`d, or inside `node_modules` — an external component library, for instance) or, in a monorepo, one outside this file's own directory tree. Point it at your app's markup, not at this package — `/tw` already ships every class definition it publishes, so scanning it too only slows the build without adding classes.
|
|
1442
1429
|
|
|
1443
|
-
|
|
1430
|
+
## Add aesthetics
|
|
1444
1431
|
|
|
1445
|
-
|
|
1446
|
-
a clip can replace the border with an inset shadow by setting
|
|
1447
|
-
`--ui-shadow-inset` and the four shadow geometry parts. Set
|
|
1448
|
-
`--ui-border-max: 0px` when that ring replaces the component border.
|
|
1449
|
-
Focusable components restore the clipped focus outline through
|
|
1450
|
-
`--ui-focus-inset`.
|
|
1451
|
-
|
|
1452
|
-
Declare `--ui-shadow-edge` when that ring _is_ the border. The ring then
|
|
1453
|
-
takes the element's edge color, which carries the edge axis as its alpha, so
|
|
1454
|
-
`.edged` draws it and `.edgeless` does not — the same answer a real border
|
|
1455
|
-
gives. Without it the ring paints on every component the aesthetic reaches
|
|
1456
|
-
and the edge classes have no effect. Use `--ui-shadow-ink` instead when the
|
|
1457
|
-
shadow is depth cast in the intent's own ink, such as a hard offset slab,
|
|
1458
|
-
which an `.edgeless` element still gets.
|
|
1459
|
-
|
|
1460
|
-
The token is read for its presence, not its value: declare it empty to
|
|
1461
|
-
switch the ring on, and `initial` to switch it back off in an aesthetic
|
|
1462
|
-
nested inside one that declares it.
|
|
1432
|
+
Add an aesthetic from the source entrypoint the same way as the compiled one, after the base import:
|
|
1463
1433
|
|
|
1464
1434
|
```css
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1435
|
+
@import "tailwindcss";
|
|
1436
|
+
@import "@codenhub/styles/tw";
|
|
1437
|
+
@import "@codenhub/styles/tw/aesthetics";
|
|
1438
|
+
|
|
1439
|
+
@source "./src";
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
## Using a focused entrypoint
|
|
1443
|
+
|
|
1444
|
+
Import a focused `/tw/*` entrypoint instead of the combined one — such as `/tw/button` or `/tw/form` — when a build only needs one component family; see the full [import path table](docs/setup.md#import-paths) for what each one composes.
|
|
1445
|
+
|
|
1446
|
+
## Troubleshooting
|
|
1447
|
+
|
|
1448
|
+
**Classes resolve to no styling**
|
|
1449
|
+
|
|
1450
|
+
- **What happened:** components use documented classes like `.btn.primary`, but no styling appears even though `@import "@codenhub/styles/tw"` is in the stylesheet.
|
|
1451
|
+
- **Why:** this is rarely `@source` itself — Tailwind already scans your project automatically. The more common cause is markup living somewhere that automatic scan excludes by default: a `.gitignore`d path, or a component pulled from `node_modules`. Tailwind may not see a class used only there and skips generating a rule for it.
|
|
1452
|
+
- **Fix:** add an explicit `@source` pointing at that directory, resolved relative to this stylesheet — matching the [example above](#import-the-source-entrypoint).
|
|
1453
|
+
|
|
1454
|
+
**Duplicate token and reset rules**
|
|
1455
|
+
|
|
1456
|
+
- **What happened:** the built stylesheet is larger than expected, and browser devtools show the same custom properties or reset rules defined twice.
|
|
1457
|
+
- **Why:** importing both a compiled entrypoint (`@codenhub/styles`) and a `/tw` entrypoint (`@codenhub/styles/tw`) in the same build compiles the same tokens and reset from two independent sources; Tailwind's build has no way to know they describe the same package.
|
|
1458
|
+
- **Fix:** pick one path per project — `/tw` entrypoints for a project running Tailwind CSS v4, compiled entrypoints everywhere else.
|
|
1459
|
+
|
|
1460
|
+
Tailwind CSS 4 or newer is required for every `/tw` entrypoint.
|
|
1461
|
+
|
|
1462
|
+
<!-- Source: docs/integrating/nextjs.md -->
|
|
1463
|
+
|
|
1464
|
+
# Next.js
|
|
1465
|
+
|
|
1466
|
+
Next.js has two router architectures, and they treat global CSS differently:
|
|
1467
|
+
|
|
1468
|
+
- **App Router** (an `app/` directory): global CSS — including an external package's stylesheet like this one — can be imported from any layout, page, or component under `app/`. Importing it from the root layout is only a recommendation, for predictable CSS ordering across routes; Next.js does not enforce it and no error occurs if you import it elsewhere.
|
|
1469
|
+
- **Pages Router** (a `pages/` directory, no `app/`): global CSS is restricted to `pages/_app.tsx`. Importing it from anywhere else fails the build — Next.js enforces this itself, not this package; see [Troubleshooting](#troubleshooting).
|
|
1470
|
+
- **Both directories present** (mid-migration): import the stylesheet in both root files. The two routers serve their own routes independently, so a route under `pages/` never sees a stylesheet imported only in `app/layout.tsx`, and vice versa.
|
|
1471
|
+
|
|
1472
|
+
## Import the stylesheet
|
|
1473
|
+
|
|
1474
|
+
Add the import to the root file for your router:
|
|
1475
|
+
|
|
1476
|
+
**App Router**
|
|
1477
|
+
|
|
1478
|
+
```tsx
|
|
1479
|
+
// app/layout.tsx
|
|
1480
|
+
import "@codenhub/styles";
|
|
1481
|
+
|
|
1482
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1483
|
+
return (
|
|
1484
|
+
<html lang="en">
|
|
1485
|
+
<body>{children}</body>
|
|
1486
|
+
</html>
|
|
1487
|
+
);
|
|
1476
1488
|
}
|
|
1477
1489
|
```
|
|
1478
1490
|
|
|
1479
|
-
|
|
1480
|
-
read them; only the `--ui-*` inputs above are public.
|
|
1491
|
+
**Pages Router**
|
|
1481
1492
|
|
|
1482
|
-
|
|
1493
|
+
```tsx
|
|
1494
|
+
// pages/_app.tsx
|
|
1495
|
+
import "@codenhub/styles";
|
|
1496
|
+
import type { AppProps } from "next/app";
|
|
1483
1497
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
imported.
|
|
1498
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
1499
|
+
return <Component {...pageProps} />;
|
|
1500
|
+
}
|
|
1501
|
+
```
|
|
1489
1502
|
|
|
1490
|
-
|
|
1491
|
-
| ------------------------ | --------------- | ----------------------------------------------------------- | --------------------------------- |
|
|
1492
|
-
| `--neo-offset` | `.neobrutalism` | Hard shadow offset and distance the press travels. | `4px` |
|
|
1493
|
-
| `--glass-radius` | `.glass` | Control corner radius. | `0.75rem` |
|
|
1494
|
-
| `--glass-radius-surface` | `.glass` | Surface corner radius. | `1rem` |
|
|
1495
|
-
| `--pixel-unit` | `.pixel` | One pixel of the grid. The corner cut and the ring are one. | `4px` |
|
|
1496
|
-
| `--font-pixel` | `.pixel` | Consumer-supplied pixel font stack. | Falls back to the monospace stack |
|
|
1497
|
-
| `--tile-radius` | `.chunky-tile` | Corner radius, on controls and surfaces alike. | `0.75rem` |
|
|
1498
|
-
| `--tile-lift` | `.chunky-tile` | Depth of the seated bar, and how far a press travels. | `4px` |
|
|
1499
|
-
| `--font-rounded` | `.chunky-tile` | Consumer-supplied rounded font stack. | Falls back to the page stack |
|
|
1503
|
+
Running Tailwind CSS v4? `@import "@codenhub/styles/tw"` is a CSS directive, so it belongs in a CSS file, not a bare import in `layout.tsx`/`_app.tsx` — add it (with `@import "tailwindcss"` and, if needed, `@source`) to your global CSS file, and import that file from the same root file shown above instead. See [Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
1500
1504
|
|
|
1501
|
-
|
|
1502
|
-
as a `var()` fallback rather than declared, so it resolves once into a
|
|
1503
|
-
private on the aesthetic's own class (`--_pixel-unit`, `--_neo-offset`,
|
|
1504
|
-
`--_tile-radius`, `--_tile-lift`, and so on). Set a knob on the element
|
|
1505
|
-
carrying the aesthetic class, or on any ancestor including `:root`:
|
|
1505
|
+
## Force a theme
|
|
1506
1506
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1507
|
+
Apply `.dark` or `.light` on `<html>` to force a theme instead of following the system preference; see [Setup → Configuration](docs/setup.md#configuration).
|
|
1508
|
+
|
|
1509
|
+
```tsx
|
|
1510
|
+
<html lang="en" className="dark">
|
|
1511
1511
|
```
|
|
1512
1512
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1513
|
+
## Troubleshooting
|
|
1514
|
+
|
|
1515
|
+
**Pages Router build fails with "Global CSS cannot be imported from files other than your Custom `<App>`"**
|
|
1516
|
+
|
|
1517
|
+
- **What happened:** Next.js rejected a global CSS `import` statement outside `pages/_app.tsx`. This restriction is specific to the Pages Router — Next.js enforces it itself, not this package, and it applies to any global CSS import, not specifically to `@codenhub/styles`. It does not apply to the App Router, where the same import works from any layout, page, or component.
|
|
1518
|
+
- **Why:** the Pages Router has no per-route CSS boundary the way the App Router does, so Next.js requires one predictable file for global stylesheets to avoid ordering conflicts between pages.
|
|
1519
|
+
- **Fix:** move the `import "@codenhub/styles"` line to `pages/_app.tsx`, and remove it from wherever it currently is.
|
|
1520
|
+
|
|
1521
|
+
## See also
|
|
1522
|
+
|
|
1523
|
+
- [Setup → Import paths](docs/setup.md#import-paths): every entrypoint this package publishes.
|
|
1524
|
+
- [Usage](docs/usage/index.md): component classes to compose once the stylesheet is loaded.
|
|
1525
|
+
|
|
1526
|
+
<!-- Source: docs/integrating/vue.md -->
|
|
1527
|
+
|
|
1528
|
+
# Vue
|
|
1529
|
+
|
|
1530
|
+
Vue places no restriction on where CSS is imported from, but importing it once, from the app's entry file, keeps the reset and tokens from loading more than once and guarantees they're in place before any component renders.
|
|
1531
|
+
|
|
1532
|
+
## Import the stylesheet
|
|
1533
|
+
|
|
1534
|
+
```ts
|
|
1535
|
+
// main.ts
|
|
1536
|
+
import "@codenhub/styles";
|
|
1537
|
+
import { createApp } from "vue";
|
|
1538
|
+
import App from "./App.vue";
|
|
1539
|
+
|
|
1540
|
+
createApp(App).mount("#app");
|
|
1519
1541
|
```
|
|
1520
1542
|
|
|
1521
|
-
|
|
1522
|
-
apart matters. A knob resolves once at the aesthetic's own class and
|
|
1523
|
-
inherits down already-resolved, so it reaches the aesthetic **from above**:
|
|
1524
|
-
setting it on the element or any ancestor works, but setting it on a
|
|
1525
|
-
_descendant_ of the aesthetic class changes nothing — the value has already
|
|
1526
|
-
been read. `--ui-radius` and the other material tokens above are read fresh
|
|
1527
|
-
by each component as it draws, so they reach **at or below** the element
|
|
1528
|
-
carrying them, which is how you round one card differently from its
|
|
1529
|
-
neighbours without touching the knob at all.
|
|
1543
|
+
Running Tailwind CSS v4? Import `@codenhub/styles/tw` from a CSS entry file instead of `main.ts`, using the `@tailwindcss/vite` plugin — see [Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
1530
1544
|
|
|
1531
|
-
|
|
1532
|
-
`.pixel` set `--ui-ink` for their neutral outline; intent classes still
|
|
1533
|
-
override the intent slots on each component.
|
|
1545
|
+
## Force a theme
|
|
1534
1546
|
|
|
1535
|
-
|
|
1547
|
+
Apply `.dark` or `.light` on the root element to force a theme instead of following the system preference; see [Setup → Configuration](docs/setup.md#configuration).
|
|
1536
1548
|
|
|
1537
|
-
|
|
1538
|
-
`--button-*`, `--surface-*`, `--control-*`, `--feedback-*`, and `--tooltip-*`.
|
|
1539
|
-
These variables are internal wiring for class composition and are not the
|
|
1540
|
-
public token contract.
|
|
1549
|
+
## See also
|
|
1541
1550
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
component-scoped input is `--progress-value` on the `.progress` element,
|
|
1545
|
-
because consumers must provide a value such as `64%`.
|
|
1551
|
+
- [Setup → Import paths](docs/setup.md#import-paths): every entrypoint this package publishes.
|
|
1552
|
+
- [Usage](docs/usage/index.md): component classes to compose once the stylesheet is loaded.
|
|
1546
1553
|
|
|
1547
|
-
|
|
1548
|
-
`--layout-cluster-gap` tokens. No compatibility aliases are provided.
|
|
1554
|
+
<!-- Source: docs/integrating/svelte.md -->
|
|
1549
1555
|
|
|
1550
|
-
|
|
1551
|
-
look and its documented exceptions.
|
|
1556
|
+
# Svelte
|
|
1552
1557
|
|
|
1553
|
-
|
|
1558
|
+
## Import the stylesheet
|
|
1554
1559
|
|
|
1555
|
-
|
|
1560
|
+
**SvelteKit** — import from the root layout so it loads once for every route:
|
|
1556
1561
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1562
|
+
```svelte
|
|
1563
|
+
<!-- src/routes/+layout.svelte -->
|
|
1564
|
+
<script>
|
|
1565
|
+
import "@codenhub/styles";
|
|
1566
|
+
let { children } = $props();
|
|
1567
|
+
</script>
|
|
1568
|
+
|
|
1569
|
+
{@render children()}
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
**Plain Svelte + Vite** — import from the entry file instead:
|
|
1573
|
+
|
|
1574
|
+
```ts
|
|
1575
|
+
// main.ts
|
|
1576
|
+
import "@codenhub/styles";
|
|
1577
|
+
import { mount } from "svelte";
|
|
1578
|
+
import App from "./App.svelte";
|
|
1568
1579
|
|
|
1569
|
-
|
|
1580
|
+
mount(App, { target: document.getElementById("app")! });
|
|
1581
|
+
```
|
|
1570
1582
|
|
|
1571
|
-
|
|
1572
|
-
| ---------------------- | --------------------------------------- |
|
|
1573
|
-
| _(default)_ | Circular spinner (rotating arc). |
|
|
1574
|
-
| `.dots-wave` | Three dots bouncing up/down in a wave. |
|
|
1575
|
-
| `.dots-fade` | Three dots fading in and out. |
|
|
1576
|
-
| `.dots-queue` | Dot queuing from left to right. |
|
|
1577
|
-
| `.dots-rotate` | Side dots rotating around a center dot. |
|
|
1578
|
-
| `.dots-grow` | Three dots growing and shrinking. |
|
|
1579
|
-
| `.dots-grow-alternate` | Outer dots small, center dot pulses. |
|
|
1580
|
-
| `.dot-bounce` | Single dot bouncing with squash effect. |
|
|
1581
|
-
| `.bars-wave` | Three vertical bars scaling in a wave. |
|
|
1582
|
-
| `.pulse-ring` | Two concentric rings pulsing outward. |
|
|
1583
|
+
Running Tailwind CSS v4? Import `@codenhub/styles/tw` from the app's CSS entry instead — see [Tailwind CSS v4](docs/integrating/tailwind.md).
|
|
1583
1584
|
|
|
1584
|
-
|
|
1585
|
-
`prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding
|
|
1586
|
-
static mask. This fallback is part of focused loader, button, and components
|
|
1587
|
-
imports; it does not depend on the global reset. See
|
|
1588
|
-
[Accessibility](docs/accessibility.md).
|
|
1585
|
+
## Force a theme
|
|
1589
1586
|
|
|
1590
|
-
|
|
1587
|
+
Apply `.dark` or `.light` on `<html>` (edit `src/app.html` in SvelteKit) to force a theme instead of following the system preference; see [Setup → Configuration](docs/setup.md#configuration).
|
|
1591
1588
|
|
|
1592
|
-
|
|
1593
|
-
| -------- | ----------------- |
|
|
1594
|
-
| _(none)_ | Default (`2rem`). |
|
|
1595
|
-
| `.sm` | Small (`1.5rem`). |
|
|
1596
|
-
| `.lg` | Large (`2.5rem`). |
|
|
1589
|
+
## See also
|
|
1597
1590
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
and `.info`. Without an intent, they use the text palette, except `.loader`,
|
|
1601
|
-
which keeps `currentColor` so it matches whatever content surrounds it.
|
|
1591
|
+
- [Setup → Import paths](docs/setup.md#import-paths): every entrypoint this package publishes.
|
|
1592
|
+
- [Usage](docs/usage/index.md): component classes to compose once the stylesheet is loaded.
|
|
1602
1593
|
|
|
1603
|
-
|
|
1604
|
-
intent but ignore fill and edge [presentation](docs/usage/composing.md#presentation).
|
|
1594
|
+
<!-- Source: docs/integrating/astro.md -->
|
|
1605
1595
|
|
|
1606
|
-
|
|
1607
|
-
<span class="loader success" aria-hidden="true"></span>
|
|
1608
|
-
<span class="skeleton info"></span>
|
|
1609
|
-
```
|
|
1596
|
+
# Astro
|
|
1610
1597
|
|
|
1611
|
-
|
|
1612
|
-
increases the left padding and adds an embedded SVG. Success, warning, and
|
|
1613
|
-
destructive intents use corresponding symbols; other intents use the
|
|
1614
|
-
information symbol.
|
|
1598
|
+
Astro renders components statically by default, but a global stylesheet import still only needs to happen once — from a shared layout, not from individual pages or islands.
|
|
1615
1599
|
|
|
1616
|
-
|
|
1617
|
-
classes. Without one they use a tinted surface, intent-colored text, and a
|
|
1618
|
-
mixed intent border.
|
|
1600
|
+
## Import the stylesheet
|
|
1619
1601
|
|
|
1620
|
-
|
|
1602
|
+
```astro
|
|
1603
|
+
---
|
|
1604
|
+
// src/layouts/Layout.astro
|
|
1605
|
+
import "@codenhub/styles";
|
|
1606
|
+
---
|
|
1621
1607
|
|
|
1622
|
-
|
|
1623
|
-
<
|
|
1624
|
-
<
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
<span class="badge warning">Queued</span>
|
|
1628
|
-
<span class="badge success solid">Live</span>
|
|
1629
|
-
<span class="badge info soft">Draft</span>
|
|
1630
|
-
<span class="loader" aria-hidden="true"></span>
|
|
1631
|
-
<span class="loader dots-wave" aria-hidden="true"></span>
|
|
1632
|
-
<span class="loader dots-fade" aria-hidden="true"></span>
|
|
1633
|
-
<span class="loader bars-wave" aria-hidden="true"></span>
|
|
1634
|
-
<span class="loader pulse-ring sm" aria-hidden="true"></span>
|
|
1635
|
-
<span class="loader dots-grow lg" aria-hidden="true"></span>
|
|
1636
|
-
<div
|
|
1637
|
-
class="progress"
|
|
1638
|
-
role="progressbar"
|
|
1639
|
-
aria-label="Upload progress"
|
|
1640
|
-
aria-valuemin="0"
|
|
1641
|
-
aria-valuemax="100"
|
|
1642
|
-
aria-valuenow="64"
|
|
1643
|
-
style="--progress-value: 64%"
|
|
1644
|
-
></div>
|
|
1645
|
-
<div
|
|
1646
|
-
class="progress secondary active"
|
|
1647
|
-
role="progressbar"
|
|
1648
|
-
aria-label="Upload progress"
|
|
1649
|
-
aria-valuemin="0"
|
|
1650
|
-
aria-valuemax="100"
|
|
1651
|
-
aria-valuenow="64"
|
|
1652
|
-
style="--progress-value: 64%"
|
|
1653
|
-
></div>
|
|
1654
|
-
<div
|
|
1655
|
-
class="progress info indeterminate"
|
|
1656
|
-
role="progressbar"
|
|
1657
|
-
aria-label="Loading"
|
|
1658
|
-
aria-valuemin="0"
|
|
1659
|
-
aria-valuemax="100"
|
|
1660
|
-
></div>
|
|
1608
|
+
<html lang="en">
|
|
1609
|
+
<body>
|
|
1610
|
+
<slot />
|
|
1611
|
+
</body>
|
|
1612
|
+
</html>
|
|
1661
1613
|
```
|
|
1662
1614
|
|
|
1663
|
-
|
|
1664
|
-
urgency, mark decorative loading visuals with `aria-hidden="true"`, and use
|
|
1665
|
-
semantic progress elements or ARIA values when numeric progress must be
|
|
1666
|
-
announced; see [Accessibility](docs/accessibility.md).
|
|
1615
|
+
Every page that uses this layout loads the stylesheet once. Running Tailwind CSS v4? Import `@codenhub/styles/tw` through Astro's Tailwind integration instead — see [Tailwind CSS v4](docs/integrating/tailwind.md), including when an explicit `@source` is actually needed.
|
|
1667
1616
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
# Forms
|
|
1617
|
+
## Force a theme
|
|
1671
1618
|
|
|
1672
|
-
|
|
1673
|
-
| --------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
1674
|
-
| `.field` | Vertical field wrapper. |
|
|
1675
|
-
| `.label` | Form label text. |
|
|
1676
|
-
| `.hint` | Secondary helper text. |
|
|
1677
|
-
| `.hint.error` | Helper text with destructive intent. |
|
|
1678
|
-
| `.surface` | Shared public container composition utility. |
|
|
1679
|
-
| `.text-control` | Shared public text-control composition utility. |
|
|
1680
|
-
| `.ipt` | Input control styling. |
|
|
1681
|
-
| `.input-group` | Wrapper that owns the field box so a control can carry icons. |
|
|
1682
|
-
| `.textarea` | Textarea control styling. |
|
|
1683
|
-
| `.select` | Select control styling. |
|
|
1684
|
-
| `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
|
|
1685
|
-
| `input[type="radio"].radio` | Custom radio control styling. |
|
|
1686
|
-
| `input[type="checkbox"].switch` | Custom switch control styling. |
|
|
1687
|
-
| `[aria-invalid="true"]` on controls | Destructive border and focus color. |
|
|
1688
|
-
| `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
|
|
1619
|
+
Apply `.dark` or `.light` on `<html>` to force a theme instead of following the system preference; see [Setup → Configuration](docs/setup.md#configuration).
|
|
1689
1620
|
|
|
1690
|
-
##
|
|
1621
|
+
## See also
|
|
1691
1622
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
custom property, so every glyph shipped a light and a dark copy and a theme
|
|
1695
|
-
selector chose between them. That put icon artwork, and the package's only
|
|
1696
|
-
theme-by-selector branch, inside a CSS-only design system whose glyphs did
|
|
1697
|
-
not fit every aesthetic. Icons are the consumer's to choose now.
|
|
1623
|
+
- [Setup → Import paths](docs/setup.md#import-paths): every entrypoint this package publishes.
|
|
1624
|
+
- [Usage](docs/usage/index.md): component classes to compose once the stylesheet is loaded.
|
|
1698
1625
|
|
|
1699
|
-
|
|
1700
|
-
field box — border, radius, fill, focus ring, invalid and disabled state —
|
|
1701
|
-
and the control inside it goes flush, so the boundary is drawn once. Any
|
|
1702
|
-
icon element works, placed before the control for a leading icon or after it
|
|
1703
|
-
for a trailing one: an inline `<svg>`, an `<img>`, or a class from an icon
|
|
1704
|
-
set such as `@codenhub/icons`.
|
|
1626
|
+
<!-- Source: docs/accessibility.md -->
|
|
1705
1627
|
|
|
1706
|
-
|
|
1707
|
-
<label class="field">
|
|
1708
|
-
<span class="label">Email</span>
|
|
1709
|
-
<div class="input-group">
|
|
1710
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" aria-hidden="true">
|
|
1711
|
-
<rect width="20" height="16" x="2" y="4" rx="2" />
|
|
1712
|
-
<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
|
|
1713
|
-
</svg>
|
|
1714
|
-
<input class="ipt" type="email" />
|
|
1715
|
-
</div>
|
|
1716
|
-
</label>
|
|
1717
|
-
<label class="field">
|
|
1718
|
-
<span class="label">Search</span>
|
|
1719
|
-
<div class="input-group">
|
|
1720
|
-
<input class="ipt" type="search" />
|
|
1721
|
-
<i class="ic-search" aria-hidden="true"></i>
|
|
1722
|
-
</div>
|
|
1723
|
-
</label>
|
|
1724
|
-
```
|
|
1628
|
+
# Accessibility responsibilities
|
|
1725
1629
|
|
|
1726
|
-
|
|
1727
|
-
axes the same way a lone `.ipt` does — `.input-group.soft.edgeless` is the
|
|
1728
|
-
sunk variant, `.input-group.primary` colors the boundary — and follows
|
|
1729
|
-
whichever [aesthetic](docs/usage/aesthetics.md) is in scope. `aria-invalid` or
|
|
1730
|
-
`disabled` on the control inside propagates to the group; so does either on
|
|
1731
|
-
the group itself. Focus is shown with `:focus-within`, so unlike a lone
|
|
1732
|
-
field the ring also appears on a mouse click.
|
|
1630
|
+
This package provides CSS hooks for accessible states. It does not provide semantic HTML, ARIA attributes, keyboard behavior, focus management, validation, announcement timing, or JavaScript behavior.
|
|
1733
1631
|
|
|
1734
|
-
|
|
1735
|
-
pickers. WebKit's native `search` decorations are still suppressed on
|
|
1736
|
-
`.text-control` to keep them from overlapping the value.
|
|
1632
|
+
## Provided by CSS
|
|
1737
1633
|
|
|
1738
|
-
|
|
1634
|
+
### Focus and state
|
|
1739
1635
|
|
|
1740
|
-
|
|
1741
|
-
|
|
1636
|
+
| Feature | Behavior |
|
|
1637
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1638
|
+
| `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
|
|
1639
|
+
| Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
|
|
1640
|
+
| Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
|
|
1641
|
+
| Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
|
|
1642
|
+
| Open state | `[data-state="open"]` styles supported surfaces and tooltips. |
|
|
1643
|
+
| Native `<dialog>` | `dialog:not([open])` and a centered `dialog[open]` are restored in author origin, so `.card` composed onto a `<dialog>` (the closest primitive the package ships) does not render open on load or lose its centering to another rule. Focus trapping, escape handling, and inert-background behavior stay outside this package. |
|
|
1742
1644
|
|
|
1743
|
-
|
|
1744
|
-
| ----------------------------------- | ----------------------- |
|
|
1745
|
-
| `.neutral` _(default)_ | Text color, capped. |
|
|
1746
|
-
| `.primary` | Primary color. |
|
|
1747
|
-
| `.secondary` | Secondary/accent color. |
|
|
1748
|
-
| `.success` | Success color. |
|
|
1749
|
-
| `.warning` | Warning color. |
|
|
1750
|
-
| `.destructive`, `.danger`, `.error` | Destructive color. |
|
|
1751
|
-
| `.info` | Info color. |
|
|
1645
|
+
### Visually hidden content
|
|
1752
1646
|
|
|
1753
|
-
|
|
1754
|
-
them.** An unchecked ghost toggle is the silhouette every toggle already
|
|
1755
|
-
has, and a checked one is a mark on nothing — a tick floating on the page, a
|
|
1756
|
-
dot with no ring around it. A container that cascades `.ghost` onto a
|
|
1757
|
-
toggle is floored rather than obeyed.
|
|
1647
|
+
`.visually-hidden` hides content from sighted users while keeping it in the accessibility tree, using the standard clip-based recipe. It ships wherever the layout utilities do -- `.`, `./native`, `./tw`, `./tw/native`, and `./tw/utilities` -- including the vanilla entries with no Tailwind utility layer of their own; it is not part of `./components` or the narrower `./tw/components`/`./tw/*` component slices. See [Content and layout](docs/usage/content-and-layout.md).
|
|
1758
1648
|
|
|
1759
|
-
|
|
1760
|
-
rather than pinning a fill, so the class still chooses:
|
|
1649
|
+
### Control boundaries
|
|
1761
1650
|
|
|
1762
|
-
|
|
|
1763
|
-
|
|
|
1764
|
-
|
|
|
1765
|
-
| `.
|
|
1651
|
+
| Feature | Behavior |
|
|
1652
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1653
|
+
| Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice; hover and focus take the tone whole. |
|
|
1654
|
+
| Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
|
|
1655
|
+
| Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
|
|
1656
|
+
| Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest, except `.switch.ghost`: `.ghost` already asks for zero fill, so dropping the line too would leave no boundary at all, and `.switch.ghost.edgeless` keeps it up instead. |
|
|
1657
|
+
| `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
|
|
1766
1658
|
|
|
1767
|
-
|
|
1768
|
-
checked radio thickens its ring to twice the resting line and takes the
|
|
1769
|
-
intent whole on it, so `.radio.soft` is the classic ring-and-dot radio and
|
|
1770
|
-
`.radio.solid` is a filled circle whose dot stays readable — the mark
|
|
1771
|
-
follows the plate rather than being pinned to the intent color.
|
|
1659
|
+
### Motion and transparency
|
|
1772
1660
|
|
|
1773
|
-
|
|
1661
|
+
| Feature | Behavior |
|
|
1662
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
1663
|
+
| Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
|
|
1664
|
+
| Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
|
|
1665
|
+
| Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
|
|
1774
1666
|
|
|
1775
|
-
|
|
1776
|
-
focus-visible border, and both [presentation](docs/usage/composing.md#presentation)
|
|
1777
|
-
axes. `.ghost` fills nothing, `.soft` takes `12%`, and `.solid` takes `20%`
|
|
1778
|
-
— quiet enough that typed text still reads on it, and ordered so the louder
|
|
1779
|
-
name draws the stronger tint. None of them touches the line.
|
|
1667
|
+
The loader fallback is embedded in loader CSS, so focused imports that include loaders honor reduced motion without the reset. This includes `@codenhub/styles/components`, `@codenhub/styles/tw/components`, and `@codenhub/styles/tw/loader`. The complete and native entrypoints include both that fallback and the reset's broader animation and transition shortening.
|
|
1780
1668
|
|
|
1781
|
-
|
|
1782
|
-
`.edgeless` is floored, so a toolbar cannot leave a field with no mark of
|
|
1783
|
-
where typing goes. `.edgeless` on the control itself is honoured, which is
|
|
1784
|
-
how the borderless field is spelled — it does not meet WCAG 1.4.11 at rest,
|
|
1785
|
-
so it is opt-in and never a default, and the focus ring still shows.
|
|
1669
|
+
### Color and contrast
|
|
1786
1670
|
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
<div class="edgeless">
|
|
1796
|
-
<input class="ipt" placeholder="Keeps its line; the floor holds" />
|
|
1797
|
-
</div>
|
|
1798
|
-
```
|
|
1671
|
+
| Feature | Behavior |
|
|
1672
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1673
|
+
| Filled labels | Every intent's `.solid` label meets 1.4.3 (4.5:1) as normal text, in both themes. Lowest is success at 5.14:1. |
|
|
1674
|
+
| Warning ink | `--color-warning-contrast` is the near-black tone, not the page tone. Amber dark enough to carry white is not amber. |
|
|
1675
|
+
| Partial fills | Contrast ink appears only past a half fill, so a capped plate keeps `--intent-strong` rather than walking toward the page. |
|
|
1676
|
+
| Intent hue in dark | `--intent-strong` is the `-300` shade in dark, so a soft or ghost label keeps its own hue instead of reading white. |
|
|
1677
|
+
| Toggle marks | A checked mark meets 3:1 as a state indicator on every plate its fill class can paint, in both themes. |
|
|
1678
|
+
| Soft-edged border | `.alert`, `.panel`, `.badge`, `.kbd`, and `.code`/`.pre`'s `.edged` border blends toward the component's own fill; at `.soft`'s 12% fill this misses 3:1 against the fill for some intents, and which ones misses depends on the theme. Known, not fixed -- `--color-border`/`--intent-border` is a shared blend seam every one of them reads, so a targeted fix would need to touch it for one component without moving it for the rest. |
|
|
1799
1679
|
|
|
1800
|
-
|
|
1801
|
-
the package composes: ghost, edged, surface radius, and the two slots only
|
|
1802
|
-
a surface resolves — the backdrop filter and the complete surface shadow.
|
|
1803
|
-
Use it to paint a container the package does not ship, under whichever
|
|
1804
|
-
aesthetic is in scope. Prefer `.card` or `.panel` when they fit.
|
|
1680
|
+
### Forced colors and direction
|
|
1805
1681
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
```
|
|
1682
|
+
| Feature | Behavior |
|
|
1683
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
1684
|
+
| Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
|
|
1685
|
+
| Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
|
|
1811
1686
|
|
|
1812
|
-
`.text-control`
|
|
1813
|
-
provides the shared control dimensions, border, placeholder, focus-visible,
|
|
1814
|
-
invalid, and disabled styles composed by `ipt`, `textarea`, and `select`.
|
|
1815
|
-
Use it for custom text-like controls; prefer those higher-level utilities
|
|
1816
|
-
when they fit.
|
|
1687
|
+
In forced colors, class-based form controls including `.text-control` receive a 2px `Highlight` system-color focus outline. The native entrypoint provides the same visible system outline for unclassed text inputs, selects, textareas, checkboxes, and radios.
|
|
1817
1688
|
|
|
1818
|
-
|
|
1689
|
+
Custom checkboxes and radios use the system `Canvas`, `CanvasText`, `Highlight`, and `HighlightText` colors in forced-colors mode so checked and unchecked states remain distinct. Shape, size, and spacing remain unchanged.
|
|
1819
1690
|
|
|
1820
|
-
|
|
1821
|
-
<label class="field">
|
|
1822
|
-
<span class="label">Email</span>
|
|
1823
|
-
<input class="ipt" type="email" aria-invalid="true" aria-describedby="email-error" />
|
|
1824
|
-
<span class="hint error" id="email-error">Enter a valid email.</span>
|
|
1825
|
-
</label>
|
|
1826
|
-
<label style="display: flex; gap: 0.5rem; align-items: center">
|
|
1827
|
-
<input type="checkbox" class="checkbox success" />
|
|
1828
|
-
<span>Accept terms</span>
|
|
1829
|
-
</label>
|
|
1830
|
-
<label style="display: flex; gap: 0.5rem; align-items: center">
|
|
1831
|
-
<input type="radio" class="radio secondary" name="plan" />
|
|
1832
|
-
<span>Standard plan</span>
|
|
1833
|
-
</label>
|
|
1834
|
-
<label style="display: flex; gap: 0.5rem; align-items: center">
|
|
1835
|
-
<input type="checkbox" class="switch destructive" />
|
|
1836
|
-
<span>Enable</span>
|
|
1837
|
-
</label>
|
|
1838
|
-
```
|
|
1691
|
+
## Required outside CSS
|
|
1839
1692
|
|
|
1840
|
-
Use
|
|
1841
|
-
relationships; see [Accessibility](docs/accessibility.md).
|
|
1693
|
+
Use semantic HTML and behavior appropriate for the component.
|
|
1842
1694
|
|
|
1843
|
-
|
|
1695
|
+
| UI | Required outside CSS |
|
|
1696
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
1697
|
+
| Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
|
|
1698
|
+
| Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
|
|
1699
|
+
| Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
|
|
1700
|
+
| Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
|
|
1701
|
+
| Tooltips | Give the bubble a real `id` and `role="tooltip"`, and reference it from the trigger's `aria-describedby`. |
|
|
1702
|
+
| Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
|
|
1703
|
+
| Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
|
|
1704
|
+
| Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
|
|
1844
1705
|
|
|
1845
|
-
|
|
1706
|
+
## State attribute guidance
|
|
1846
1707
|
|
|
1847
|
-
|
|
1848
|
-
presentation, and aesthetic as three independent axes, resolved through
|
|
1849
|
-
tokens a component reads.
|
|
1708
|
+
Prefer native attributes first:
|
|
1850
1709
|
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
- [Customizing](docs/usage/customizing.md): The presentation and material token
|
|
1855
|
-
reference, aesthetic knobs, and defining a custom intent.
|
|
1856
|
-
- [Aesthetics](docs/usage/aesthetics.md): Each shipped aesthetic in depth, including
|
|
1857
|
-
its documented exceptions.
|
|
1858
|
-
- [Buttons](docs/usage/buttons.md)
|
|
1859
|
-
- [Forms](docs/usage/forms.md)
|
|
1860
|
-
- [Feedback](docs/usage/feedback.md)
|
|
1861
|
-
- [Tooltips](docs/usage/tooltips.md)
|
|
1862
|
-
- [Content and layout](docs/usage/content-and-layout.md): Layout helpers, tables,
|
|
1863
|
-
quotes, code, surfaces, and typography utilities.
|
|
1710
|
+
```html
|
|
1711
|
+
<button class="btn primary" disabled>Saving</button> <input class="ipt" aria-invalid="true" aria-describedby="email-error" />
|
|
1712
|
+
```
|
|
1864
1713
|
|
|
1865
|
-
|
|
1714
|
+
Use ARIA or data attributes when native attributes are not available for the element or library:
|
|
1866
1715
|
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1716
|
+
```html
|
|
1717
|
+
<a class="btn secondary" aria-disabled="true">Unavailable</a>
|
|
1718
|
+
<span class="tooltip" data-state="open">
|
|
1719
|
+
<span class="tooltip-icon" tabindex="0" aria-label="More details" aria-describedby="more-details-bubble">?</span>
|
|
1720
|
+
<span class="tooltip-bubble" role="tooltip" id="more-details-bubble">More details</span>
|
|
1721
|
+
</span>
|
|
1722
|
+
```
|
|
1872
1723
|
|
|
1873
|
-
|
|
1724
|
+
`aria-disabled="true"` communicates disabled state but does not prevent activation. JavaScript or element choice must prevent activation when required.
|
|
1874
1725
|
|
|
1875
|
-
|
|
1726
|
+
<!-- Source: docs/changelog/0.2.0.md -->
|
|
1876
1727
|
|
|
1877
|
-
|
|
1878
|
-
[Concepts → Theme selection](docs/concepts.md#theme-selection) for how a theme
|
|
1879
|
-
is chosen. Intent tokens are how a component reads that palette once an
|
|
1880
|
-
intent class is applied. Foundation tokens are the shared layout, shape, and
|
|
1881
|
-
motion defaults underneath everything.
|
|
1728
|
+
# 0.2.0
|
|
1882
1729
|
|
|
1883
|
-
|
|
1730
|
+
Fixes from a stress-test pass composing real screens (`app-shell`, `form`, `settings`) against the published `0.1.x` model. Several change default token values rather than adding new surface, so this lands as one minor rather than a run of patches -- see [Changed](#changed) for what an upgrader needs to check.
|
|
1884
1731
|
|
|
1885
|
-
|
|
1886
|
-
`light-dark()`, and the element's `color-scheme` picks one.
|
|
1732
|
+
## Added
|
|
1887
1733
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1734
|
+
- `.visually-hidden`, a utility that hides content from sighted users while keeping it in the accessibility tree. Ships wherever the layout utilities do (`.`, `/native`, `/tw`, `/tw/native`, `/tw/utilities`), including the vanilla entries that have no Tailwind utility layer of their own; not part of `/components` or the narrower `/tw/*` component slices.
|
|
1735
|
+
- `.tooltip-bubble`, a real element for a tooltip's message that can carry its own `id` and be referenced by `aria-describedby`.
|
|
1736
|
+
- `.alert-icon`, sizing and alignment for a consumer-supplied icon element dropped into `.alert` -- any inline SVG, `<img>`, or icon-set class works, not just a built-in glyph.
|
|
1737
|
+
- `dialog:not([open])`/`dialog[open]` restated in author origin, so `.card` composed onto a `<dialog>` hides until opened and centers when shown, without a dialog primitive of its own.
|
|
1738
|
+
- `.icon` added to `registry.json` under `modifiers.shape`. The button shape modifier was already shipped and documented; it was missing only from the machine-readable contract.
|
|
1739
|
+
- `.switch.ghost.edgeless` now renders a track instead of disappearing.
|
|
1893
1740
|
|
|
1894
|
-
|
|
1895
|
-
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
1896
|
-
| `--color-primary` | Primary action/content color. |
|
|
1897
|
-
| `--color-primary-contrast` | Text/icon color on primary filled surface. |
|
|
1898
|
-
| `--color-primary-hover` | Primary hover state. |
|
|
1899
|
-
| `--color-primary-subtle` | Low-emphasis companion tone for primary. Use as soft surface or subtle background. |
|
|
1900
|
-
| `--color-primary-strong` | High-emphasis companion tone for primary. Use as readable text, icon, or border on subtle surfaces. |
|
|
1901
|
-
| `--color-accent` | Secondary/accent surface. |
|
|
1902
|
-
| `--color-accent-contrast` | Text/icon color on accent filled surface. |
|
|
1903
|
-
| `--color-accent-hover` | Accent hover state. |
|
|
1904
|
-
| `--color-accent-subtle` | Low-emphasis companion tone for accent. Use as soft surface or subtle background. |
|
|
1905
|
-
| `--color-accent-strong` | High-emphasis companion tone for accent. Use as readable text, icon, or border on subtle surfaces. |
|
|
1906
|
-
| `--color-border` | Default border color. |
|
|
1907
|
-
| `--color-border-hover` | Interactive border color. |
|
|
1908
|
-
| `--color-control-border` | Resting line on the six text controls; one value, drawn at a fraction on text inputs. |
|
|
1909
|
-
| `--color-background` | Page and default raised surface background. |
|
|
1910
|
-
| `--color-foreground` | Subtle raised foreground surface. |
|
|
1911
|
-
| `--color-surface` | Nested or muted surface. |
|
|
1912
|
-
| `--color-text` | Primary text color. |
|
|
1913
|
-
| `--color-text-secondary` | Secondary text color. |
|
|
1914
|
-
| `--color-text-contrast` | Readable color on top of a full fill of `--color-text`. A checked toggle prints it. |
|
|
1915
|
-
| `--color-text-hover` | Text hover state. |
|
|
1916
|
-
| `--color-text-subtle` | Low-emphasis companion tone for text. Use as soft surface or subtle background. |
|
|
1917
|
-
| `--color-text-strong` | High-emphasis companion tone for text. Use as readable text, icon, or border on subtle surfaces. |
|
|
1918
|
-
| `--color-tooltip` | Tooltip bubble plate with no intent. Chosen per theme rather than derived: near-white in light, mid grey in dark. |
|
|
1919
|
-
| `--color-tooltip-contrast` | Ink on `--color-tooltip`. |
|
|
1920
|
-
| `--color-success` | Success state color. |
|
|
1921
|
-
| `--color-success-contrast` | Text/icon color on success filled surface. Meets 4.5:1 as normal text on `--color-success` (5.14:1). |
|
|
1922
|
-
| `--color-success-hover` | Success hover tone. |
|
|
1923
|
-
| `--color-success-subtle` | Low-emphasis companion tone for success. |
|
|
1924
|
-
| `--color-success-strong` | High-emphasis companion tone for success. |
|
|
1925
|
-
| `--color-warning` | Warning state color. |
|
|
1926
|
-
| `--color-warning-contrast` | The near-black tone, not the page tone. Meets 4.5:1 as normal text on `--color-warning` (6.19:1). |
|
|
1927
|
-
| `--color-warning-hover` | Warning hover tone. |
|
|
1928
|
-
| `--color-warning-subtle` | Low-emphasis companion tone for warning. |
|
|
1929
|
-
| `--color-warning-strong` | High-emphasis companion tone for warning. |
|
|
1930
|
-
| `--color-destructive` | Destructive/error state color. |
|
|
1931
|
-
| `--color-destructive-contrast` | Text/icon color on destructive filled surface. Meets 4.5:1 as normal text on `--color-destructive` (5.78:1). |
|
|
1932
|
-
| `--color-destructive-hover` | Destructive hover tone. |
|
|
1933
|
-
| `--color-destructive-subtle` | Low-emphasis companion tone for destructive. |
|
|
1934
|
-
| `--color-destructive-strong` | High-emphasis companion tone for destructive. |
|
|
1935
|
-
| `--color-info` | Informational state color. |
|
|
1936
|
-
| `--color-info-contrast` | Text/icon color on info filled surface. Meets 4.5:1 as normal text on `--color-info` (6.19:1). |
|
|
1937
|
-
| `--color-info-hover` | Info hover tone. |
|
|
1938
|
-
| `--color-info-subtle` | Low-emphasis companion tone for info. |
|
|
1939
|
-
| `--color-info-strong` | High-emphasis companion tone for info. |
|
|
1741
|
+
## Changed
|
|
1940
1742
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1743
|
+
- **Tooltips are composition, not a single class.** `.tooltip` now wraps a trigger and a `.tooltip-bubble`, one more DOM node than before. Intent, presentation, aesthetic, and elevation classes move to `.tooltip-bubble`, the element that actually paints.
|
|
1744
|
+
- **`.checkbox`, `.radio`, and `.switch` default to `.soft` instead of `.solid`, and their checked state now composes one plate regardless of presentation.** Unchecked, `.ghost` (0% fill), `.soft` (12%), and `.solid` now read as three distinct plates instead of three shades of one wash; `.ghost` is a supported look on every toggle for the first time.
|
|
1745
|
+
- **A neutral (no named intent) `.card.soft`, `data-table`, `.panel`, or `.alert` rests untinted instead of mixing a 12% ink wash.** A named intent (`.card.soft.destructive`, `.alert.success`, and so on) is unaffected and keeps its ordinary tint; `.badge` is unaffected either way, since it has no border to fall back on if untinted.
|
|
1746
|
+
- **Elevation no longer cascades onto descendants.** `--ui-elevation`, `--elevation-y`, and `--elevation-blur` are registered non-inheriting, so an elevation class lifts only the element it is written on. A container that used to lift a whole unclassed subtree in one class now needs an elevation class on each element that wants depth.
|
|
1747
|
+
- `--input-group-gap` replaces a hardcoded gap in `.input-group`; unchanged by default for icon compositions, and settable to `0` on a text affix for a flush look.
|
|
1748
|
+
- `.loader`'s sizes step down from 24/32/40px to 20/28/36px for `.sm`/default/`.lg`.
|
|
1749
|
+
- A `/tw` consumer's own `dark:` utilities now also fire under `prefers-color-scheme: dark` when no explicit theme selector is present, matching how the token palette already follows the OS default.
|
|
1948
1750
|
|
|
1949
|
-
##
|
|
1751
|
+
## Removed
|
|
1950
1752
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
component that supports intent, and a custom intent needs no component
|
|
1954
|
-
changes.
|
|
1753
|
+
- **`.btn.loading`.** A loading button is now a disabled `.btn` composed with a real `.loader` child instead of a hardcoded `::after` spinner -- any loader variant works, not just one circular spinner.
|
|
1754
|
+
- **`.alert.icon`'s hardcoded per-intent SVG mask.** Replaced by `.alert-icon` (above), sized for a consumer-supplied icon element.
|
|
1955
1755
|
|
|
1956
|
-
|
|
1957
|
-
| ------------------- | ------------------------------------------------------------------------------------- |
|
|
1958
|
-
| `--intent-color` | The intent's base color. What a fill is made of. |
|
|
1959
|
-
| `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
|
|
1960
|
-
| `--intent-hover` | The intent's hovered base color. |
|
|
1961
|
-
| `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
|
|
1962
|
-
| `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
|
|
1963
|
-
| `--intent-border` | Line color. Resolves to `--color-border` when no intent is set. |
|
|
1964
|
-
| `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
|
|
1756
|
+
## Fixed
|
|
1965
1757
|
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
The strong tone has to stay recognisably its own hue, not just readable. On a
|
|
1973
|
-
dark page the `-100` shade of every family is a pale tint that carries almost
|
|
1974
|
-
no chroma at small sizes: a soft success badge and a soft destructive one
|
|
1975
|
-
printed labels that both read white, and a soft radio's dot stopped matching
|
|
1976
|
-
the ring around it. The dark tones are `-300` for that reason, and the tinted
|
|
1977
|
-
grounds under them are `-900` so there is room — measured at 5.0:1 or better
|
|
1978
|
-
everywhere a partial fill prints text, and past 9:1 on the page.
|
|
1979
|
-
|
|
1980
|
-
`--intent-border` exists because a neutral border must stay the quiet border
|
|
1981
|
-
gray rather than the text color. Everything that draws a line reads it:
|
|
1982
|
-
control borders, table rules, dividers, key caps, and quote bars.
|
|
1983
|
-
|
|
1984
|
-
`--intent-fill-max` exists for the neutral intent, whose color is the page's
|
|
1985
|
-
own ink. Every other intent is a hue, legible pale at 12% and saturated at
|
|
1986
|
-
100%; a grey filled to 100% is a black or white slab, which is the loudest
|
|
1987
|
-
thing on the page and the wrong look for the intent that means _nothing in
|
|
1988
|
-
particular_. Neutral stops at `20%`, so `.solid` gives it a quiet plate that
|
|
1989
|
-
still reads as a step past `.soft`. Hover adds its step on top of the cap, so
|
|
1990
|
-
a capped fill still deepens under the pointer.
|
|
1991
|
-
|
|
1992
|
-
The foreground is gated by the plate rather than tied to the fill.
|
|
1993
|
-
`--intent-contrast` is the ink for a _full_ fill, so printing it at full
|
|
1994
|
-
strength on a fifth of one puts white on light grey. Contrast ink therefore
|
|
1995
|
-
appears only once the fill is past halfway and reaches full at a full fill;
|
|
1996
|
-
below that a component prints `--intent-strong`, the tone chosen to be read.
|
|
1997
|
-
|
|
1998
|
-
That means a capped neutral takes the strong tone whole rather than a
|
|
1999
|
-
fraction of the way toward the page. Tying it to the fill instead cost a
|
|
2000
|
-
neutral `.solid` button four points of contrast and a neutral tooltip nearly
|
|
2001
|
-
ten. A state that lifts the cap on purpose — a checked checkbox is filled
|
|
2002
|
-
with its own intent by definition — reaches a full fill and gets the contrast
|
|
2003
|
-
whole.
|
|
2004
|
-
|
|
2005
|
-
Set them directly to define an intent this package does not ship. **State
|
|
2006
|
-
every slot**, including `--intent-fill-max`: components reset all seven to
|
|
2007
|
-
the neutral values, so a slot your intent leaves out keeps neutral's — and an
|
|
2008
|
-
intent that omits the cap fills to 20% of its color instead of 100%.
|
|
1758
|
+
- `.icon` buttons kept the wrong `min-width` floor under `.compact`/`.spacious`; all three padding tiers now size `.icon` to its own padding.
|
|
1759
|
+
- `.chunky-tile`'s offset shadow read as a solid fill behind a small `.icon.ghost` button; halved for that combination.
|
|
1760
|
+
- Table row and boundary rules stayed a 1px hairline under `.chunky-tile`'s 2px frame; both now track the same width token the frame uses.
|
|
1761
|
+
- `.progress`'s track failed WCAG 1.4.11 in both themes; `--progress-surface` now mixes enough ink to clear 3:1.
|
|
1762
|
+
- `.card.soft.edged`'s border was nearly invisible against its own fill in light theme; `--color-border`'s light value moved one step darker.
|
|
1763
|
+
- A badge nested inside an untinted `data-table` lost its own fill; box now restates its fill cap on every element that uses it, so a nested badge, button, or chip no longer inherits its container's cap.
|
|
2009
1764
|
|
|
2010
|
-
|
|
2011
|
-
.brand {
|
|
2012
|
-
--intent-color: var(--color-violet-600);
|
|
2013
|
-
--intent-contrast: var(--color-neutral-50);
|
|
2014
|
-
--intent-hover: var(--color-violet-700);
|
|
2015
|
-
--intent-strong: var(--color-violet-800);
|
|
2016
|
-
--intent-subtle: var(--color-violet-100);
|
|
2017
|
-
--intent-border: var(--color-violet-600);
|
|
2018
|
-
--intent-fill-max: 100%;
|
|
2019
|
-
}
|
|
2020
|
-
```
|
|
1765
|
+
<!-- Source: docs/changelog/0.1.1.md -->
|
|
2021
1766
|
|
|
2022
|
-
|
|
2023
|
-
<button class="btn brand">Custom intent</button>
|
|
2024
|
-
<span class="badge brand soft">Works everywhere</span>
|
|
2025
|
-
```
|
|
1767
|
+
# 0.1.1
|
|
2026
1768
|
|
|
2027
|
-
|
|
2028
|
-
> palette at their own root, so a `.success` container never recolors a
|
|
2029
|
-
> nested destructive button. Put the intent class on the element that shows
|
|
2030
|
-
> the intent.
|
|
1769
|
+
## Fixed
|
|
2031
1770
|
|
|
2032
|
-
|
|
1771
|
+
- `.p-xs` and its alias `.dense` now clear a button's `--control-height` minimum, so a chip-sized button fits inside a table cell or a dense toolbar. Before this, every button kept the full control-height floor even at the tightest padding, so a button that small could not be built. Every other size and padding class still keeps the floor, so a plain button is unchanged. An icon-only `.p-xs`/`.dense` button also gives up its `min-width`; size the glyph yourself, as the package sizes no icons.
|
|
2033
1772
|
|
|
2034
|
-
|
|
2035
|
-
motion, focus, depth, and layering behavior used by helper classes.
|
|
1773
|
+
<!-- Source: docs/changelog/0.1.0.md -->
|
|
2036
1774
|
|
|
2037
|
-
|
|
2038
|
-
yourself. They are a separate thing from `.elevation-none`/`.elevation-sm`/
|
|
2039
|
-
`.elevation-md` (aliased as `.flat`/`.raised`/`.floating`), which scale a
|
|
2040
|
-
_component's own_ shadow geometry and default to no shadow at all until an
|
|
2041
|
-
aesthetic or one of those classes asks for depth — see
|
|
2042
|
-
[Composing → Elevation](docs/usage/composing.md#elevation) for that system. The two
|
|
2043
|
-
share `none`/`sm`/`md`/`lg` naming for the same weight of depth, but one is a
|
|
2044
|
-
fixed value and the other multiplies shadow parts, so they will not always
|
|
2045
|
-
paint identically.
|
|
1775
|
+
# 0.1.0
|
|
2046
1776
|
|
|
2047
|
-
|
|
2048
|
-
| -------------------------- | ----------------------------------------------------------------------- |
|
|
2049
|
-
| `--font-default` | Default app font family. |
|
|
2050
|
-
| `--container-narrow` | Narrow content width. |
|
|
2051
|
-
| `--container-max` | Default content width. |
|
|
2052
|
-
| `--container-wide` | Wide content width. |
|
|
2053
|
-
| `--layout-gutter` | Responsive inline page padding. |
|
|
2054
|
-
| `--layout-section-block` | Responsive section vertical padding. |
|
|
2055
|
-
| `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
|
|
2056
|
-
| `--layout-grid-min` | Minimum column width for `.auto-grid`. |
|
|
2057
|
-
| `--radius-small` | Small radius for compact utility surfaces such as code elements. |
|
|
2058
|
-
| `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
|
|
2059
|
-
| `--radius-surface` | Radius for cards, panels, and alerts. |
|
|
2060
|
-
| `--control-height` | Default minimum height for controls. |
|
|
2061
|
-
| `--border-width` | Default border width. |
|
|
2062
|
-
| `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
|
|
2063
|
-
| `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
|
|
2064
|
-
| `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
|
|
2065
|
-
| `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
|
|
2066
|
-
| `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
|
|
2067
|
-
| `--focus-ring` | Focus-visible ring color. |
|
|
2068
|
-
| `--focus-ring-offset` | Focus-visible outline offset. Negative, so the ring lands on the edge. |
|
|
2069
|
-
| `--focus-ring-width` | Focus-visible outline/ring width. |
|
|
2070
|
-
| `--motion-duration-fast` | Fast transition for buttons and input interactions (`120ms`). |
|
|
2071
|
-
| `--motion-duration-normal` | Normal transition for layout and modals (`200ms`). |
|
|
2072
|
-
| `--motion-duration-slow` | Slow animation for skeleton loaders and progress bars (`400ms`). |
|
|
2073
|
-
| `--motion-ease` | Default easing curve. |
|
|
2074
|
-
| `--z-popover` | Popover/tooltip z-index. |
|
|
2075
|
-
| `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
|
|
2076
|
-
| `--breakpoint-2xl` | Extended large Tailwind responsive breakpoint. |
|
|
1777
|
+
`0.1.0` replaces the model published in `0.0.4` wholesale. The old model had three orthogonal axes with per-component clamps and no chosen default: a plain element got whatever `var()` fallback each component happened to declare. This release makes each decision explicitly and is the baseline the `0.x` line builds on. Nearly everything below is breaking against `0.0.4`; there are no compatibility aliases.
|
|
2077
1778
|
|
|
2078
|
-
|
|
2079
|
-
tokens — how much of an intent shows, and what a component is made of.
|
|
1779
|
+
## Changed
|
|
2080
1780
|
|
|
2081
|
-
|
|
1781
|
+
- **Every element now composes from three named axes.** _Intent_ (`.primary`, `.success`, `.destructive`, …) sets hue only. _Presentation_ sets how much of that hue fills the element and whether it draws an edge. _Aesthetic_ sets the material — radius, edge width, shadow, silhouette, type. Intent is redeclared per component; presentation and aesthetic cascade, so a container can set the look of a subtree while any element opts out. [Concepts](docs/concepts.md) and [Composing](docs/usage/composing.md) describe the full model.
|
|
1782
|
+
- **Presentation is two independent questions.** Fill is `.solid` / `.soft` / `.ghost`; edge is `.edged` / `.edgeless`. Exactly one of each applies, and they never decide each other. The common web button — a subtle fill with a border — is `.soft.edged`, which had no spelling before.
|
|
1783
|
+
- **`.bare` is renamed `.ghost`.** `.bare` and `.edgeless` both read as "less of something", so the name is now a fill and only a fill: no fill at rest, a tint on hover.
|
|
1784
|
+
- **Hover is derived, not declared.** A hovered element takes its resting fill plus one published step and its `--intent-hover` colour. There are no hover tokens to set. An outline button therefore tints on hover rather than filling completely.
|
|
1785
|
+
- **A sunk text field is `.soft.edgeless`.** Two classes saying two things, where `.soft` alone used to silently suppress the field's border.
|
|
1786
|
+
- **Table row rules are opt-in via `.ruled`.** Rules no longer arrive by implication from a component's edge default. The head and foot separators stay at every presentation.
|
|
1787
|
+
- **Depth is a modifier, not part of a component.** `surface` no longer carries a structural shadow, so cards, buttons, and chips on a plain page rest flat. Use `.raised` / `.floating` (or `.elevation-sm` / `.elevation-md`) to add depth, and `.flat` to remove an aesthetic's.
|
|
1788
|
+
- **The Tailwind v4 entrypoint moved from `/overrides` to `/native`.**
|
|
2082
1789
|
|
|
2083
|
-
|
|
1790
|
+
## Added
|
|
2084
1791
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
| `[data-tooltip-position="left"]` | Positions tooltip left of host. |
|
|
2092
|
-
| `[data-tooltip-position="right"]` | Positions tooltip right of host. |
|
|
2093
|
-
| No `data-tooltip-position` | Defaults to top placement. |
|
|
2094
|
-
| `[data-state="open"]` | Shows tooltip without hover. |
|
|
2095
|
-
| `.tooltip.raised` / `.tooltip.floating` | Adds one or two units of depth. The bubble is flat by default. |
|
|
2096
|
-
|
|
2097
|
-
A tooltip bubble's fill, ground color, and floor behavior are covered in
|
|
2098
|
-
[Composing → Presentation](docs/usage/composing.md#presentation), and its material
|
|
2099
|
-
follows whichever [aesthetic](docs/usage/aesthetics.md) is in scope, including the
|
|
2100
|
-
pseudo-element itself.
|
|
2101
|
-
|
|
2102
|
-
The bubble rests flat. It draws no shadow of its own on a plain page, and
|
|
2103
|
-
zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the
|
|
2104
|
-
same way `.flat` does elsewhere. Add [`.raised` or
|
|
2105
|
-
`.floating`](docs/usage/composing.md#elevation) to lift it — `.tooltip.floating` is the
|
|
2106
|
-
pre-0.1.0 look. An aesthetic whose shadow is a complete value, like `.glass`,
|
|
2107
|
-
still reaches the bubble as it reaches any surface.
|
|
1792
|
+
- **Opt-in aesthetics.** `.neobrutalism`, `.glass`, `.pixel`, and `.chunky-tile`, each shipped as its own entrypoint (`@codenhub/styles/aesthetics/pixel`, …) and as an aggregate (`@codenhub/styles/aesthetics`). An aesthetic sets material tokens only; it never repaints hue. See [Aesthetics](docs/usage/aesthetics.md).
|
|
1793
|
+
- **An elevation modifier.** `.flat` / `.raised` / `.floating` (and the `.elevation-none` / `-sm` / `-md` names) multiply an aesthetic's shadow geometry. Unitless, so a container can flatten a whole toolbar and any element can opt back in.
|
|
1794
|
+
- **Granular Tailwind v4 source entrypoints.** `@codenhub/styles/tw` and per-family subpaths (`/tw/button`, `/tw/form`, `/tw/feedback`, `/tw/tooltip`, `/tw/utilities`, `/tw/aesthetics/*`, …) for consumers compiling their own build. `package.json` is also exported.
|
|
1795
|
+
- **`.input-group`.** A wrapper that draws one box and lets the control inside give up its own paint, so an icon can sit alongside a field. The package ships no icons — any element works in the slot.
|
|
1796
|
+
- **`registry.json`.** A machine-readable record of every supported class, each component's default fill/edge/elevation, the axes it reads, and every bound and exception with its reason.
|
|
1797
|
+
- **`--intent-strong`**, the intent tone chosen to be read on a page, used wherever a fill is partial.
|
|
2108
1798
|
|
|
2109
|
-
|
|
2110
|
-
<button
|
|
2111
|
-
class="tooltip tooltip-icon"
|
|
2112
|
-
data-tooltip="More details"
|
|
2113
|
-
data-tooltip-position="right"
|
|
2114
|
-
aria-label="More details"
|
|
2115
|
-
>
|
|
2116
|
-
?
|
|
2117
|
-
</button>
|
|
2118
|
-
```
|
|
1799
|
+
## Removed
|
|
2119
1800
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
1801
|
+
- **`.out` and `.fill`.** Outline is now `.ghost.edged`; there is no class that makes an outline button fill completely on hover.
|
|
1802
|
+
- **`--ui-border-scale` / `.out`'s doubled border.** The edge width comes from the aesthetic and nothing else, which removed the per-component border ceilings small components needed.
|
|
1803
|
+
- **`--color-text-subtle`**, which nothing read.
|
|
1804
|
+
- **`--layout-stack-gap` and `--layout-cluster-gap`**, replaced by a single `--layout-gap`.
|
|
1805
|
+
- **The bundled input-field icons.** Icons were a `background-image` data URI with a light and a dark copy per glyph; they are gone, and `.input-group` carries a consumer-supplied icon instead.
|