actual-css 0.12.0 → 0.14.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/CHANGELOG.md +102 -0
- package/README.md +7 -8
- package/components.json +124 -12
- package/dist/actual.full.css +1152 -309
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/architecture.md +67 -0
- package/docs/design-notes/steps.md +45 -28
- package/docs/navigation.json +2 -0
- package/docs/pages/components/alert.md +12 -2
- package/docs/pages/components/avatar.md +17 -11
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/button.md +3 -2
- package/docs/pages/components/list.md +15 -16
- package/docs/pages/components/navbar.md +42 -22
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/segmented.md +83 -0
- package/docs/pages/components/steps.md +28 -22
- package/docs/pages/enhancements/flyout.md +5 -0
- package/docs/pages/enhancements/scrollspy.md +6 -6
- package/docs/pages/enhancements/tabs.md +79 -3
- package/docs/pages/examples/overview.md +3 -1
- package/docs/pages/forms/color-swatches.md +131 -0
- package/docs/pages/forms/combobox.md +2 -2
- package/docs/pages/forms/controls.md +6 -3
- package/docs/pages/forms/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +3 -6
- package/docs/pages/forms/joined-controls.md +6 -2
- package/docs/pages/forms/overview.md +1 -0
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/forms/switch.md +4 -2
- package/docs/pages/foundations/philosophy.md +97 -1
- package/docs/pages/foundations/theming.md +1 -1
- package/docs/pages/foundations/tokens.md +94 -27
- package/docs/pages/guides/design-tools.md +14 -8
- package/docs/pages/guides/integrating-actual-css.md +74 -8
- package/docs/pages/guides/introduction.md +3 -2
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +25 -7
- package/docs/pages/guides/upgrading.md +86 -9
- package/docs/pages/layout/center.md +2 -1
- package/docs/pages/layout/cluster.md +11 -4
- package/docs/pages/layout/grid.md +2 -0
- package/docs/pages/layout/scroll-snap.md +4 -2
- package/docs/pages/layout/sidebar-layout.md +3 -0
- package/docs/pages/layout/switcher.md +2 -0
- package/docs/pages/layout/topbar.md +6 -4
- package/docs/pages/patterns/icon-slot.md +17 -1
- package/docs/pages/utilities/flex.md +1 -1
- package/docs/pages/utilities/sizing-wrapping.md +51 -6
- package/package.json +6 -6
- package/reserved-classes.json +3 -0
- package/src/cli/actual-css.js +4 -4
- package/src/css/components/alert.css +99 -48
- package/src/css/components/app-nav.css +17 -9
- package/src/css/components/avatar.css +2 -18
- package/src/css/components/badge.css +23 -10
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/button.css +17 -2
- package/src/css/components/close.css +6 -3
- package/src/css/components/drawer.css +23 -42
- package/src/css/components/flyout.css +0 -1
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +2 -2
- package/src/css/components/list.css +28 -13
- package/src/css/components/modal.css +17 -10
- package/src/css/components/navbar.css +15 -10
- package/src/css/components/pagination.css +5 -0
- package/src/css/components/scrollspy.css +1 -1
- package/src/css/components/segmented.css +186 -0
- package/src/css/components/steps.css +72 -15
- package/src/css/components/tab.css +135 -5
- package/src/css/core/focus.css +11 -6
- package/src/css/core/theme.css +6 -1
- package/src/css/core/tokens.css +24 -6
- package/src/css/core/variants.css +16 -5
- package/src/css/forms/choice-card.css +1 -1
- package/src/css/forms/choice.css +1 -1
- package/src/css/forms/color-swatch.css +159 -0
- package/src/css/forms/control.css +1 -1
- package/src/css/forms/floating-field.css +20 -18
- package/src/css/forms/form-actions.css +14 -8
- package/src/css/forms/index.css +1 -0
- package/src/css/forms/native.css +51 -21
- package/src/css/forms/otp.css +4 -5
- package/src/css/forms/switch.css +6 -3
- package/src/css/forms/validation.css +23 -30
- package/src/css/layout/center.css +2 -0
- package/src/css/layout/cluster.css +14 -0
- package/src/css/layout/grid.css +7 -0
- package/src/css/layout/scroll-snap.css +30 -0
- package/src/css/layout/scroller.css +19 -3
- package/src/css/layout/sidebar-layout.css +3 -0
- package/src/css/layout/switcher.css +3 -0
- package/src/css/layout/topbar.css +10 -0
- package/src/css/utilities/extra.css +3 -3
- package/src/js/focus.js +16 -1
- package/src/js/full.js +4 -0
- package/src/js/inline-visible.js +54 -0
- package/src/js/parsed.js +21 -0
- package/src/js/surface.js +10 -0
- package/src/js/tab.js +33 -7
- package/src/tooling/color.js +14 -1
- package/src/tooling/css-bundle.js +0 -5
- package/src/tooling/design-components.js +9 -5
- package/src/tooling/design-export.js +9 -1
- package/src/tooling/design-tokens.js +207 -76
- package/src/tooling/theme-contrast.js +182 -45
|
@@ -132,8 +132,10 @@ same optical line. Change the type or the control size and they stay level:
|
|
|
132
132
|
- `--switch-width` — track width.
|
|
133
133
|
- `--switch-block-size` — track height.
|
|
134
134
|
- `--switch-knob-margin` — inset between knob and track.
|
|
135
|
+
- `--switch-knob-inline-size` — knob width; wider than its height makes a
|
|
136
|
+
capsule knob. Defaults to the knob height.
|
|
135
137
|
|
|
136
|
-
Knob size and travel distance are derived from
|
|
137
|
-
|
|
138
|
+
Knob size and travel distance are derived from these hooks; set them rather
|
|
139
|
+
than the derived values. The knob is concentric in its track: the
|
|
138
140
|
inset is measured inside the border on both axes, so it clears the rails by the
|
|
139
141
|
same distance it clears the ends.
|
|
@@ -1,6 +1,102 @@
|
|
|
1
1
|
# Philosophy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Actual CSS is a composition-first component framework: let HTML and the
|
|
4
|
+
cascade do the work, compose layout separately from components, and add an
|
|
5
|
+
abstraction only for a real, repeated need. The outlook is close to
|
|
6
|
+
[CUBE CSS](https://cube.fyi/), without adopting its methodology or naming.
|
|
7
|
+
|
|
8
|
+
**Related terms:** principles, architecture, CUBE, BEM, naming, methodology, guidelines.
|
|
9
|
+
|
|
10
|
+
## Layers
|
|
11
|
+
|
|
12
|
+
| Layer | In Actual | Owns |
|
|
13
|
+
| --------- | --------------------------------- | ----------------------------------- |
|
|
14
|
+
| Baseline | reset, tokens, themes, base | HTML defaults, cascade, inheritance |
|
|
15
|
+
| Layout | `.stack`, `.cluster`, `.grid`, … | space between children |
|
|
16
|
+
| Utility | `.list-reset`, `.measure`, … | one reusable adjustment |
|
|
17
|
+
| Component | `.card`, `.btn`, form controls, … | intrinsic anatomy and appearance |
|
|
18
|
+
| State | native attributes, ARIA, `data-*` | what the element is doing |
|
|
19
|
+
|
|
20
|
+
## Principles
|
|
21
|
+
|
|
22
|
+
**Components own their inside; compositions own the space between them.** A
|
|
23
|
+
card sets its padding, surface and slots. Where it sits, and how far it is from
|
|
24
|
+
the next card, belongs to the layout around it:
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<section class="stack">
|
|
28
|
+
<article class="card">…</article>
|
|
29
|
+
<article class="card">…</article>
|
|
30
|
+
</section>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A rule such as `.card + .card { margin-block-start: 2rem }` moves page layout
|
|
34
|
+
into the component; put a composition around the cards instead.
|
|
35
|
+
|
|
36
|
+
**A composition on a component takes over its default layout.** A component's
|
|
37
|
+
own layout sits at zero specificity, so `<article class="card media">` lays out
|
|
38
|
+
as a `.media` without an override, whatever the import order.
|
|
39
|
+
|
|
40
|
+
**State uses the most semantic attribute; classes choose presentation.**
|
|
41
|
+
|
|
42
|
+
| Kind | Mechanism | Examples |
|
|
43
|
+
| ---------------- | -------------- | -------------------------------------------- |
|
|
44
|
+
| Native state | HTML attribute | `disabled`, `open`, `checked` |
|
|
45
|
+
| Accessible state | ARIA | `aria-current`, `aria-expanded`, `aria-busy` |
|
|
46
|
+
| Runtime state | `data-*` | no native or ARIA equivalent |
|
|
47
|
+
| Presentation | class | `.soft`, `.outline`, `.sm`, `.compact` |
|
|
48
|
+
|
|
49
|
+
Write `<a class="nav-link" aria-current="page">`, not `.nav-link.active`. The
|
|
50
|
+
few `.is-*` classes (`.is-open`, `.is-static`) are runtime internals: neither
|
|
51
|
+
set nor select them. `data-enhance` activates behavior and is never a styling
|
|
52
|
+
hook.
|
|
53
|
+
|
|
54
|
+
**A utility does one thing.** A class that sets a surface, a border, padding and
|
|
55
|
+
type together is a component: give it a name in application CSS.
|
|
56
|
+
|
|
57
|
+
**Configure before overriding.** Documented public hooks (`--grid-min`,
|
|
58
|
+
`--card-pad`, `--cluster-justify`) are the API. Other custom properties are
|
|
59
|
+
internal and may change.
|
|
60
|
+
|
|
61
|
+
## Before adding CSS
|
|
62
|
+
|
|
63
|
+
Go down the layers and stop at the first that answers the need:
|
|
64
|
+
|
|
65
|
+
1. Does HTML already express it, or the browser already do it?
|
|
66
|
+
2. Is it a relationship between children? Use a composition.
|
|
67
|
+
3. Is it one small adjustment? Use a utility.
|
|
68
|
+
4. Is it the anatomy of a component? Use the component and its hooks.
|
|
69
|
+
5. Is it a state of that component? Use the attribute that names the state.
|
|
70
|
+
|
|
71
|
+
What remains is product-specific: write it as application CSS, as described in
|
|
72
|
+
[Building with Actual CSS](../guides/integrating-actual-css.md).
|
|
73
|
+
|
|
74
|
+
## Class grammar
|
|
75
|
+
|
|
76
|
+
A component takes optional intent, variant and size classes:
|
|
77
|
+
`<button class="btn primary soft sm">`. The sections below describe each axis.
|
|
78
|
+
|
|
79
|
+
### Flat names, not BEM
|
|
80
|
+
|
|
81
|
+
A modifier is a separate class that combines with others (`.btn.primary.soft`,
|
|
82
|
+
not `.btn--primary`), and a part gets a prefixed class (`.alert-icon`,
|
|
83
|
+
`.list-item-title`) only when its element does not already say what it is.
|
|
84
|
+
There is no `__` or `--` in Actual's names, because:
|
|
85
|
+
|
|
86
|
+
- **Modifiers are shared axes.** `.primary`, `.soft` and `.sm` mean the same
|
|
87
|
+
thing on every component that supports them, so one class serves `.btn`,
|
|
88
|
+
`.badge` and `.alert`. BEM names a modifier per block, which multiplies the
|
|
89
|
+
vocabulary and keeps a custom intent such as `.tertiary` from working
|
|
90
|
+
everywhere at once.
|
|
91
|
+
- **State is not a modifier.** `disabled`, `aria-current` and `open` already
|
|
92
|
+
name the state; a `--disabled` or `--active` class would duplicate them and
|
|
93
|
+
drift out of sync.
|
|
94
|
+
- **Elements come from HTML.** A card's `header` and `footer`, an avatar's
|
|
95
|
+
`<img>` need no `__element` class; a prefixed class is kept for the parts
|
|
96
|
+
HTML cannot name.
|
|
97
|
+
|
|
98
|
+
Application CSS can use any convention, BEM included, alongside Actual's
|
|
99
|
+
classes: `<article class="card pricing-plan pricing-plan--featured">`.
|
|
4
100
|
|
|
5
101
|
## Components
|
|
6
102
|
|
|
@@ -56,7 +56,7 @@ Set `--form-actions-justify` (and `--form-actions-align`).
|
|
|
56
56
|
|
|
57
57
|
### Customize a switch's size
|
|
58
58
|
|
|
59
|
-
Change `--switch-width`, `--switch-block-size`, and `--switch-knob-margin`; knob size and travel are derived from them. To rescale the whole row of inline choices at once, use `.sm` / `.lg` on `.choice`, or set `--choice-control-size` directly — the checkbox, the radio, and the switch track all follow it. Density contexts leave inline choices alone.
|
|
59
|
+
Change `--switch-width`, `--switch-block-size`, and `--switch-knob-margin`; knob size and travel are derived from them. `--switch-knob-inline-size` widens the knob into a capsule. To rescale the whole row of inline choices at once, use `.sm` / `.lg` on `.choice`, or set `--choice-control-size` directly — the checkbox, the radio, and the switch track all follow it. Density contexts leave inline choices alone.
|
|
60
60
|
|
|
61
61
|
→ Forms · Switch
|
|
62
62
|
|
|
@@ -35,6 +35,28 @@ Global tokens are the ones to reach for in application CSS. Write a relay only t
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### Which hooks inherit
|
|
39
|
+
|
|
40
|
+
A hook either describes one element's own layout, or a policy its context
|
|
41
|
+
hands down. Instance geometry stops at every new instance; policy inherits.
|
|
42
|
+
|
|
43
|
+
| Kind | Examples | Inherits |
|
|
44
|
+
| ----------------- | ----------------------------------------- | -------- |
|
|
45
|
+
| Palette, theme | `--text`, `--primary` | yes |
|
|
46
|
+
| Rhythm, density | `--gap`, `--control-size` | yes |
|
|
47
|
+
| Page policy | `--center-size`, `--measure` | yes |
|
|
48
|
+
| Role policy | `--form-actions-justify`, `--frame-ratio` | yes |
|
|
49
|
+
| Instance geometry | `--cluster-justify`, `--grid-columns` | no |
|
|
50
|
+
|
|
51
|
+
Set instance geometry on the element itself (a class or inline style), and
|
|
52
|
+
select nested instances to change them (`.toolbar > .cluster`). Set a policy
|
|
53
|
+
once on the region or on `:root` that owns it. A new hook follows the same
|
|
54
|
+
test: "how this element lays out" stops at the element, "what this context
|
|
55
|
+
hands down" inherits. The stop needs the instance to read its own hook:
|
|
56
|
+
`.switcher` and `.sidebar-layout` hooks are read by their children, and a
|
|
57
|
+
nested instance that is itself such a child still needs its parent's value,
|
|
58
|
+
so those keep inheriting.
|
|
59
|
+
|
|
38
60
|
### Color
|
|
39
61
|
|
|
40
62
|
Intent tokens are used by `.primary`, `.secondary`, `.success`, `.warning`, `.danger`, and `.neutral`. They are curated color/foreground pairs. Actual CSS does not compute foregrounds automatically in the core theme contract.
|
|
@@ -85,21 +107,34 @@ Surfaces are painted material and may be translucent. Two roles must stay opaque
|
|
|
85
107
|
|
|
86
108
|
A theme with translucent surfaces overrides both with opaque values.
|
|
87
109
|
|
|
110
|
+
`--border` separates surfaces: cards, tables, dividers, panels. `--border-control`
|
|
111
|
+
is the resting edge that tells a control apart from the surface — fields,
|
|
112
|
+
checkboxes and radios, switches, OTP cells, choice cards, join addons, the color
|
|
113
|
+
and file inputs. It defaults to `--border`; set it alone to make controls
|
|
114
|
+
stand out (input border, field outline, form control contrast) without
|
|
115
|
+
darkening every separator. Hover, checked, invalid and disabled states keep
|
|
116
|
+
their own colors.
|
|
117
|
+
|
|
88
118
|
Use `*-fg` pairs only for solid backgrounds where the component controls both foreground and background. Do not require a foreground token for every surface token.
|
|
89
119
|
|
|
90
|
-
Focus styling is outline-first. A global focus baseline
|
|
120
|
+
Focus styling is outline-first. A global focus baseline draws a `--focus-ring-width` line in `--focus-outline-color`, offset by `--focus-outline-offset`: older browsers show it on `:focus`, while modern browsers limit it to `:focus-visible`. The baseline is a `currentColor` line, so text-like elements (links, `summary`, nav and menu items) follow whatever surface the application painted. Boxed components draw a solid `--focus-ring-width` line in `--focus`: inside the border box on text fields (`.input`, `.textarea`, `.select`), outside at `--focus-outline-offset` on actions (`.btn`, `.close`, choices, switches). The color never follows an intent or a local surface, so `--focus` must hold 3:1 against both `--surface` and `--surface-solid` — the page, fields and a solid band all rely on it. Both lines are `--focus-ring-width` wide. Under `forced-colors: active`, outlines map to the system focus color.
|
|
91
121
|
|
|
92
122
|
```css
|
|
93
123
|
:root {
|
|
94
124
|
--focus: hsl(268 30% 55%);
|
|
95
125
|
|
|
96
126
|
--focus-outline-color: currentColor;
|
|
97
|
-
--focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
|
|
98
127
|
--focus-outline-offset: 2px;
|
|
99
128
|
--focus-ring-width: 2px;
|
|
100
129
|
}
|
|
101
130
|
```
|
|
102
131
|
|
|
132
|
+
The line is composed on the focused element, so a theme island or a region
|
|
133
|
+
that sets `--focus-ring-width` widens links and buttons alike. Set
|
|
134
|
+
`--focus-outline` to replace the whole generic line (`4px dashed currentColor`);
|
|
135
|
+
it has no default value. Application CSS that draws the generic line itself
|
|
136
|
+
writes `var(--focus-ring-width) solid var(--focus-outline-color)`.
|
|
137
|
+
|
|
103
138
|
### Palette coherence
|
|
104
139
|
|
|
105
140
|
A coherent palette treats surfaces, text, borders, intents, and shadows as related roles rather than isolated colors.
|
|
@@ -116,34 +151,42 @@ Color should reinforce structure and meaning, not carry them alone. Information
|
|
|
116
151
|
|
|
117
152
|
The default palette is **Ink & Terra**: a near-neutral aubergine `primary`, terracotta `secondary`, and pigment-toned statuses (sage, ochre, brick) tuned to equal perceived weight. The neutral ramp (text, borders, subtle surfaces) is tinted toward the primary hue so the whole page quietly carries the identity. Optional themes can re-introduce vivid intent palettes on top of this foundation.
|
|
118
153
|
|
|
119
|
-
Soft surfaces
|
|
154
|
+
Soft surfaces are generated from intent colors through `color-mix()`, in two
|
|
155
|
+
recipes that share one ink:
|
|
156
|
+
|
|
157
|
+
- **Tint** — the default `.badge` and `.alert`, and `.btn.soft`: a translucent
|
|
158
|
+
intent laid over whatever surface is underneath, with no rim. It keeps
|
|
159
|
+
separating on `--surface-subtle` and `--surface-raised`, where a fixed mix
|
|
160
|
+
with `--surface` would match the surface it sits on.
|
|
161
|
+
- **Mix** — the shared `.soft` on zones (`.card`, `.chat-bubble`, `.navbar`,
|
|
162
|
+
`.app-nav`): an opaque mix of `--surface` with the intent and a tinted rim.
|
|
163
|
+
A zone may sit over an image, where a translucent fill would hand the text's
|
|
164
|
+
contrast to whatever is behind it.
|
|
165
|
+
|
|
166
|
+
Themes tune both globally instead of rewriting component selectors:
|
|
120
167
|
|
|
121
168
|
```css
|
|
122
169
|
:root {
|
|
123
|
-
--soft-bg-mix: 88%;
|
|
124
|
-
--soft-
|
|
125
|
-
--soft-hover-alpha: 12%;
|
|
170
|
+
--soft-bg-mix: 88%; /* share of surface; the tint's opacity is 100% minus it */
|
|
171
|
+
--soft-hover-alpha: 6%; /* extra intent share on hover */
|
|
126
172
|
--soft-fg-mix: 100%;
|
|
127
173
|
}
|
|
128
174
|
```
|
|
129
175
|
|
|
130
|
-
`--soft-border-mix`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
with no contour — the flatter language of a modern component set, from one
|
|
134
|
-
declaration:
|
|
176
|
+
`--soft-border-mix` has no default. Unset, the tint draws no rim and the mix
|
|
177
|
+
keeps a 65% one. A theme that sets it gives every soft treatment a tinted rim
|
|
178
|
+
of that weight — contoured badges and alerts, from one declaration:
|
|
135
179
|
|
|
136
180
|
```css
|
|
137
|
-
[data-theme="
|
|
138
|
-
--soft-border-mix:
|
|
181
|
+
[data-theme="contoured"] {
|
|
182
|
+
--soft-border-mix: 65%;
|
|
139
183
|
}
|
|
140
184
|
```
|
|
141
185
|
|
|
142
|
-
|
|
143
|
-
(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
borderless soft is a look, not a contract the Degraded tier upholds.
|
|
186
|
+
A rule that re-derives a soft color reads it with the recipe's own fallback:
|
|
187
|
+
`var(--soft-border-mix, 100%)` for a tint, `var(--soft-border-mix, 65%)` for a
|
|
188
|
+
mix. Both recipes are rendering-only — the baseline path below `color-mix()`
|
|
189
|
+
keeps `var(--surface-subtle)` and `var(--border)`.
|
|
147
190
|
|
|
148
191
|
`--soft-fg-mix` is the share of raw intent in soft *text*. At its `100%` default soft ink is the intent color itself, which is what a palette of dark, muted intents wants. A vivid or light palette cannot afford that: a soft badge then paints intent-tinted ink on an intent-tinted surface, and the two converge. Lowering the mix rebates the ink toward `--text`, which is the right direction in both schemes because `--text` is dark on a light theme and light on a dark one. Around `45%` a fully saturated palette recovers AA while the ink still reads as its intent.
|
|
149
192
|
|
|
@@ -158,17 +201,19 @@ hook, and the soft recipe reads it before falling back to the global mix:
|
|
|
158
201
|
- `--soft-fg-mix` — the **global fallback derivation**, unchanged.
|
|
159
202
|
|
|
160
203
|
**Soft foregrounds are calibrated against the darkest or closest interactive
|
|
161
|
-
surface they are expected to appear on.**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
the
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
surface they are expected to appear on.** The tint is translucent, so the same
|
|
205
|
+
pair reads differently on each surface — usually closest on `--surface-subtle`.
|
|
206
|
+
For interactive soft treatments the fill moves toward the intent on hover
|
|
207
|
+
(`--soft-hover-alpha`), so the binding pair is the *hovered* tint over
|
|
208
|
+
`--surface-subtle` and the resting fill simply carries more margin; the default
|
|
209
|
+
palette ships its own `--primary`/`--secondary`/… `-soft-fg` hooks to keep
|
|
210
|
+
every pair ≥ 4.5:1 on `--surface`, `--surface-raised` and `--surface-subtle` in
|
|
211
|
+
both color schemes, enforced by `tests/browser/soft-recipe.test.js`. The hard line stops at the default theme:
|
|
167
212
|
presets and custom `[data-theme]` islands keep the `--soft-fg-mix` derivation
|
|
168
213
|
unless they declare the hooks themselves (the framework resets them outside
|
|
169
214
|
the default boundary). A theme that overrides the soft palette is responsible
|
|
170
215
|
for preserving sufficient text contrast; `bunx --bun actual-css contrast --theme FILE`
|
|
171
|
-
measures its resting and hovered pairs.
|
|
216
|
+
measures its resting and hovered pairs, composited on each of those surfaces.
|
|
172
217
|
|
|
173
218
|
A theme whose primary should read more muted than its statuses sets only
|
|
174
219
|
`--primary-soft-fg`, and every soft `.primary` foreground follows it. Without a
|
|
@@ -376,7 +421,7 @@ Motion durations keep transitions consistent and easy to disable.
|
|
|
376
421
|
}
|
|
377
422
|
```
|
|
378
423
|
|
|
379
|
-
Ordinary interaction/state transitions have **no easing token**: they use the CSS default `ease` directly, and the dialog shake feedback plus the `actual-dialog` View Transition keep that default deliberately. The presence pair exists only for one-shot open/close where open and closed states own separate
|
|
424
|
+
Ordinary interaction/state transitions have **no easing token**: they use the CSS default `ease` directly, and the dialog shake feedback plus the `actual-dialog` View Transition keep that default deliberately. The presence pair exists only for one-shot open/close where open and closed states own separate easing values (`.status-bar`/`.is-open`, `dialog.modal[open]`, `dialog.drawer[open]`): `--ease-enter` decelerates the appearance, `--ease-exit` accelerates the departure. The pair follows Material's standard rhythm; the emphasized-decelerate entrance is deliberately not offered because it only pays off at 250–400ms durations, beyond this vocabulary. Backdrops keep `ease` — a scrim only fades, it does not travel — but track their surface's duration. Toggles that share one transition list at `--duration-fast` (flyout, tooltip) also stay on `ease`: at 100ms the curve barely resolves and entry/exit cannot split on a single rule. These absences are deliberate, not gaps; a theme with a different motion character overrides `--ease-enter` / `--ease-exit`.
|
|
380
425
|
|
|
381
426
|
`reset.css` clamps raw animation and transition durations to `0.01ms`, resets their delays, and restores `html` scroll behavior under `prefers-reduced-motion: reduce`. Decorative loops effectively complete immediately without blanket-disabling animation support.
|
|
382
427
|
|
|
@@ -503,7 +548,7 @@ Rules:
|
|
|
503
548
|
|
|
504
549
|
Themes override tokens, not selectors. The themes in `src/css/themes/` are repository-only demo examples, not included in the default stylesheet or the npm package; they exist to show valid ways to use this contract, such as `ocean`, `square`, `cyberpunk`, and `brutalist`.
|
|
505
550
|
|
|
506
|
-
A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, hover overlay, and shadow color. Pick `--focus` as a mid-tone that holds 3:1 against both `--surface` and `--surface-solid` in every scheme the theme declares; a pastel dark-scheme primary usually fails on the light solid surface. `bunx --bun actual-css contrast --theme FILE` measures both pairs, and exits 1 when a pair misses.
|
|
551
|
+
A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, hover overlay, and shadow color. Pick `--focus` as a mid-tone that holds 3:1 against both `--surface` and `--surface-solid` in every scheme the theme declares; a pastel dark-scheme primary usually fails on the light solid surface. `bunx --bun actual-css contrast --theme FILE` measures both pairs, and exits 1 when a pair misses. The file may declare a `[data-theme]` island or set the tokens on `:root`; the tool reads each scheme through `color-scheme`, so write dark values with `light-dark()` — a `prefers-color-scheme` media block is not measured.
|
|
507
552
|
|
|
508
553
|
When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A theme that declares no hook falls back to the `--soft-fg-mix` derivation: one global percentage applied to every role, so it carries no contrast guarantee for a palette it was not tuned against. `actual-css contrast` prints the resting and hovered soft pair for every intent in every scheme the theme declares, which is how a theme finds the roles that need a hook or a lower `--soft-fg-mix`.
|
|
509
554
|
|
|
@@ -518,6 +563,7 @@ A full theme can override:
|
|
|
518
563
|
- intent colors and foreground pairs
|
|
519
564
|
- surface, text, border, focus, and overlay colors
|
|
520
565
|
- `--surface-opaque` and `--surface-solid-fg`, when surfaces are translucent
|
|
566
|
+
- `--border-control`, when controls need a stronger edge than separators
|
|
521
567
|
- radius tokens
|
|
522
568
|
- optional shadow, motion, typography, and soft-variant mix tokens
|
|
523
569
|
|
|
@@ -563,12 +609,33 @@ A minimal recolor theme (illustrative, not a shipped theme):
|
|
|
563
609
|
|
|
564
610
|
Several tokens are theme-derived aliases that reference other tokens — `--state-selected`/`--state-selected-fg`/`--state-disabled`, `--indicator-ring`, `--shadow`/`--shadow-popout`, `--heading`, `--selection-bg`/`--selection-fg`, and `--surface-opaque`/`--surface-solid-fg`. They are declared on `:root, [data-theme]` so they recompute on every theme boundary: a custom property resolves its `var()` references at computed-value time on the element that declares it, so an alias declared only on `:root` would be inherited as an already-resolved value and would not follow a `[data-theme]` island's overridden tokens. A theme that wants a distinct alias (e.g. a `--selection-bg` of its own) overrides it explicitly afterwards, which wins by cascade order.
|
|
565
611
|
|
|
612
|
+
Selected state has three roles:
|
|
613
|
+
|
|
614
|
+
- `--state-selected` — the selected fill or accent: a pressed button, a checked switch, a completed step.
|
|
615
|
+
- `--state-selected-fg` — ink on that fill.
|
|
616
|
+
- `--state-selected-text` — selected ink on a regular surface: the current nav link, the selected tab, the current step's number and ring.
|
|
617
|
+
|
|
618
|
+
`--state-selected-text` has no default: the core resets it to the guaranteed-invalid value on every theme boundary, and each consumer reads `var(--state-selected-text, var(--state-selected))`. The reset is what lets a nested theme island reach its own `--state-selected`: a theme that resolves the hook to another token bakes the reference where it is declared, so an island that sets nothing would otherwise inherit the resolved value and never fall back. Set it when one color cannot be both a fill under its `-fg` and text on the page — typically a dark scheme, where a fill dark enough for white ink is too dark to read as text. Pulling the accent toward `--text` keeps its hue in both schemes:
|
|
619
|
+
|
|
620
|
+
```css
|
|
621
|
+
[data-theme="my-theme"] {
|
|
622
|
+
--state-selected-text: light-dark(
|
|
623
|
+
var(--primary),
|
|
624
|
+
color-mix(in oklab, var(--primary) 50%, var(--text))
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`actual-css contrast` measures it on each surface (4.5:1 for text, 3:1 for the step ring).
|
|
630
|
+
|
|
566
631
|
Without `data-theme`, the default theme advertises `color-scheme: light dark` and follows the user's OS preference in browsers that support `light-dark()`. Dark themes should set `color-scheme: dark`. Light themes should set `color-scheme: light`. Browsers without `light-dark()` receive the light fallback.
|
|
567
632
|
|
|
568
633
|
Because the native select chevron is an SVG background image and cannot inherit `currentColor`, the select hands the arrow back to the browser under `prefers-contrast: more` (same treatment as `forced-colors: active`) instead of shipping a recolored chevron per theme — one native fallback covers every theme without a high-contrast icon variant to maintain.
|
|
569
634
|
|
|
570
635
|
Under `prefers-contrast: more`, the user's contrast preference wins over a theme's palette by contract: the core collapses `--text-muted`/`--text-subtle` to `--text` and thickens borders and focus so no theme can defeat a legibility requirement. This is a deliberate decision, not a specificity quirk — the repeated `[data-theme][data-theme]` attribute only outranks a preset theme stylesheet loaded after the core.
|
|
571
636
|
|
|
637
|
+
Under `prefers-reduced-transparency: reduce`, `.topbar` and `.form-actions.sticky` paint `--surface` without a backdrop blur. A theme with translucent `--surface-*` redeclares them opaque inside the same media query; the glass demo theme shows the recipe.
|
|
638
|
+
|
|
572
639
|
Under `forced-colors: active`, the browser automatically remaps `color`, `background`, and `border` properties to system colors. The core only adjusts what the remapping cannot express on its own — `--state-disabled` becomes `GrayText` and `--disabled-opacity` becomes `1` — declared on `:root` and `[data-theme]` so every theme boundary follows. Keep local forced-color rules only for custom control geometry, native bar parts, or selected/checked states that would otherwise be communicated only by background color.
|
|
573
640
|
|
|
574
641
|
## Component mapping
|
|
@@ -6,20 +6,26 @@
|
|
|
6
6
|
bunx --bun actual-css design --theme src/theme.css --out design/
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
`--theme` is a file declaring one `[data-theme="…"]` block (see [Theming](../foundations/theming.md)); without it the default theme is exported. The command needs Bun, and `--bun` makes `bunx` run it under Bun instead of the CLI's Node shebang: it reads the theme in headless Chrome, so every `light-dark()`, `color-mix()` and `var()` chain resolves exactly as the browser paints it.
|
|
9
|
+
`--theme` is a file declaring one theme, on `:root` or in a `[data-theme="…"]` block (see [Theming](../foundations/theming.md)); without it the default theme is exported. The command needs Bun, and `--bun` makes `bunx` run it under Bun instead of the CLI's Node shebang: it reads the theme in headless Chrome, so every `light-dark()`, `color-mix()` and `var()` chain resolves exactly as the browser paints it.
|
|
10
10
|
|
|
11
11
|
## What is exported
|
|
12
12
|
|
|
13
|
-
| File | Content
|
|
14
|
-
| --------------------------------------------- |
|
|
15
|
-
| `figma/foundations.tokens.json` | spacing, radius, borders, type sizes
|
|
16
|
-
| `figma/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
13
|
+
| File | Content |
|
|
14
|
+
| --------------------------------------------- | -------------------------------------- |
|
|
15
|
+
| `figma/foundations.tokens.json` | spacing, radius, borders, type sizes |
|
|
16
|
+
| `figma/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme |
|
|
17
|
+
| `dtcg/foundations.tokens.json` | the portable profile, every foundation |
|
|
18
|
+
| `dtcg/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme |
|
|
19
|
+
| `penpot/tokens.json` | all tokens, as sets and themes |
|
|
20
|
+
| `penpot/plugin/` | the plugin and component spec |
|
|
19
21
|
|
|
20
22
|
Token names are the CSS names without `--`: `surface-raised` in the design tool is `var(--surface-raised)` in code. A scheme exists when the theme declares it: `color-scheme: light dark` exports both, anything else one. Lengths are exported in px, with the CSS value kept in `$extensions["actual-css"]`.
|
|
21
23
|
|
|
22
|
-
Colors no theme token paints — the soft fills and borders of `.soft` — are exported as recipe tokens named `<variant>-<intent>-<role>` (`soft-danger-bg`, `soft-danger-border`, `soft-danger-fg`), next to the theme colors so they switch with the scheme.
|
|
24
|
+
Colors no theme token paints — the soft fills and borders of `.soft` — are exported as recipe tokens named `<variant>-<intent>-<role>` (`soft-danger-bg`, `soft-danger-border`, `soft-danger-fg`), next to the theme colors so they switch with the scheme.
|
|
25
|
+
|
|
26
|
+
## DTCG
|
|
27
|
+
|
|
28
|
+
`dtcg/` is the portable profile, for any reader that follows the [Design Tokens Format](https://www.designtokens.org/format/): every foundation carries the `$type` the spec defines (`dimension`, `fontWeight`, `fontFamily`, `duration`, `cubicBezier`, `number`), and each color carries a 6-digit `hex` with `alpha` separate. It keeps font stacks, line heights and motion, which `figma/` drops, and omits Figma's adaptations: a font weight is a `fontWeight` here, a `number` there. Shadows and z-index are not exported anywhere.
|
|
23
29
|
|
|
24
30
|
## Figma
|
|
25
31
|
|
|
@@ -8,7 +8,8 @@ In short: **use Actual for generic structure and behavior; add product-specific
|
|
|
8
8
|
classes for brand and compositions rather than forcing a generic primitive.** A
|
|
9
9
|
local rule is not a failure of Actual — overfitting the framework to a
|
|
10
10
|
product-specific look is. Reach for an Actual primitive first, and write
|
|
11
|
-
application CSS when the need is genuinely product-specific (see §
|
|
11
|
+
application CSS when the need is genuinely product-specific (see §9–10). The
|
|
12
|
+
principles behind this order are in [Philosophy](../foundations/philosophy.md).
|
|
12
13
|
|
|
13
14
|
## 1. Start with the theme
|
|
14
15
|
|
|
@@ -60,7 +61,29 @@ For example, three panels that must all stack together are a `.switcher`, not au
|
|
|
60
61
|
|
|
61
62
|
If local CSS mainly makes one layout primitive behave like another, reconsider the primitive first.
|
|
62
63
|
|
|
63
|
-
## 3.
|
|
64
|
+
## 3. Keep outer spacing in the layout
|
|
65
|
+
|
|
66
|
+
Components own their internal rhythm; layout primitives own the space between
|
|
67
|
+
components.
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<div class="stack">
|
|
71
|
+
<article class="card">…</article>
|
|
72
|
+
<article class="card">…</article>
|
|
73
|
+
</div>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
/* Avoid */
|
|
78
|
+
.card + .card {
|
|
79
|
+
margin-block-start: var(--space-50);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A card then moves between a stack, a grid or a sidebar without its outer
|
|
84
|
+
spacing to undo.
|
|
85
|
+
|
|
86
|
+
## 4. Tune before replacing
|
|
64
87
|
|
|
65
88
|
Actual primitives expose public hooks for common adjustments.
|
|
66
89
|
|
|
@@ -97,7 +120,12 @@ over reimplementing their layout:
|
|
|
97
120
|
|
|
98
121
|
**Rule:** tune the recipe before replacing the recipe.
|
|
99
122
|
|
|
100
|
-
|
|
123
|
+
`--gap` is inherited: set on a region, it re-spaces every nested stack, cluster
|
|
124
|
+
and grid too. Set it on the layout that reads it, as `.feature-grid` does above.
|
|
125
|
+
For a region's own rhythm, set the `gap` property instead; see
|
|
126
|
+
[Region rhythm](../layout/stack.md#region-rhythm-goes-on-gap-not-on-gap).
|
|
127
|
+
|
|
128
|
+
## 5. Reuse the existing vocabulary
|
|
101
129
|
|
|
102
130
|
Before introducing a generic application class, check whether Actual already expresses the idea.
|
|
103
131
|
|
|
@@ -113,7 +141,7 @@ Small alignment change → existing utility
|
|
|
113
141
|
|
|
114
142
|
Application classes should primarily describe product-specific concepts, not recreate generic framework utilities.
|
|
115
143
|
|
|
116
|
-
##
|
|
144
|
+
## 6. Use structural component markup
|
|
117
145
|
|
|
118
146
|
When a component exposes meaningful structure, use it.
|
|
119
147
|
|
|
@@ -140,7 +168,44 @@ Connection restored.
|
|
|
140
168
|
|
|
141
169
|
Persistent state such as metrics, availability, queue size or account information belongs in normal page content.
|
|
142
170
|
|
|
143
|
-
##
|
|
171
|
+
## 7. Style state through its attribute
|
|
172
|
+
|
|
173
|
+
Use the native attribute or the ARIA state the markup already needs, then
|
|
174
|
+
`data-*` for state with no equivalent. Classes stay for presentation.
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<a class="nav-link" aria-current="page">Settings</a>
|
|
178
|
+
<button class="btn primary" disabled>Save</button>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```css
|
|
182
|
+
.checkout-form[aria-busy="true"] { … }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Not `.nav-link.active` or `.is-disabled`: a state class duplicates the attribute
|
|
186
|
+
and drifts from it. Two runtime markers are off limits too:
|
|
187
|
+
|
|
188
|
+
- `data-enhance` turns behavior on; it is never a styling hook.
|
|
189
|
+
- `.is-*` classes (`.is-open`, `.is-static`) are runtime internals: neither set
|
|
190
|
+
nor select them.
|
|
191
|
+
|
|
192
|
+
## 8. Bring third-party widgets onto the tokens
|
|
193
|
+
|
|
194
|
+
A date picker, rich text editor or data grid can come from a dedicated package.
|
|
195
|
+
Map its public visual hooks to Actual tokens instead of restyling it:
|
|
196
|
+
|
|
197
|
+
```css
|
|
198
|
+
.third-party-picker {
|
|
199
|
+
--picker-bg: var(--surface-raised);
|
|
200
|
+
--picker-border: var(--border);
|
|
201
|
+
--picker-text: var(--text);
|
|
202
|
+
--picker-focus: var(--focus);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The package keeps its behavior; the theme reaches it through the tokens.
|
|
207
|
+
|
|
208
|
+
## 9. Keep application CSS for application identity
|
|
144
209
|
|
|
145
210
|
Custom CSS is expected for things such as:
|
|
146
211
|
|
|
@@ -166,7 +231,7 @@ A better target is:
|
|
|
166
231
|
|
|
167
232
|
> No duplicated framework behavior and no palette literals outside the token layer.
|
|
168
233
|
|
|
169
|
-
##
|
|
234
|
+
## 10. Before adding a CSS rule
|
|
170
235
|
|
|
171
236
|
Ask, in this order:
|
|
172
237
|
|
|
@@ -175,9 +240,10 @@ Ask, in this order:
|
|
|
175
240
|
3. Can a public hook configure it?
|
|
176
241
|
4. Does an existing utility express the adjustment?
|
|
177
242
|
5. Can the color be derived from a theme token?
|
|
178
|
-
6.
|
|
243
|
+
6. Does an attribute already name the state?
|
|
244
|
+
7. Is this genuinely application-specific CSS?
|
|
179
245
|
|
|
180
|
-
If the answer reaches step
|
|
246
|
+
If the answer reaches step 7, write the CSS.
|
|
181
247
|
|
|
182
248
|
That is the intended integration model:
|
|
183
249
|
|
|
@@ -22,7 +22,8 @@ cascade layers, import order, or a project-owned build-time prefix transform.
|
|
|
22
22
|
The distinction is structural, not stylistic: presentation lives in classes,
|
|
23
23
|
JavaScript behavior opts in through `data-enhance` tokens, semantics come from
|
|
24
24
|
ARIA and native HTML, and per-widget configuration uses self-describing
|
|
25
|
-
`data-*` attributes.
|
|
25
|
+
`data-*` attributes. [Philosophy](../foundations/philosophy.md) lays out the
|
|
26
|
+
layers and the order to follow before adding CSS.
|
|
26
27
|
|
|
27
28
|
## Install
|
|
28
29
|
|
|
@@ -52,7 +53,7 @@ optimizing — the bare `actual-css` entrypoint and `actual-css/full` both resol
|
|
|
52
53
|
to the complete compiled framework:
|
|
53
54
|
|
|
54
55
|
```html
|
|
55
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.
|
|
56
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.14/dist/actual.full.min.css">
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
The package does not maintain separate partial bundles. Modular entrypoints map
|
|
@@ -99,7 +99,7 @@ cells, so they read as a list:
|
|
|
99
99
|
base and extra utilities
|
|
100
100
|
|
|
101
101
|
`.form-actions` composes with `.cluster`: a build that takes forms without the
|
|
102
|
-
layout family imports `actual-css/css/layout/cluster
|
|
102
|
+
layout family also imports `actual-css/css/layout/cluster`, in any order.
|
|
103
103
|
|
|
104
104
|
Individual modules can be imported through their domain path:
|
|
105
105
|
|
|
@@ -63,11 +63,30 @@ handle, and cleanup happens per element when an element leaves the DOM.
|
|
|
63
63
|
|
|
64
64
|
## The full runtime
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
Import `actual-css/js/full` to enable the full runtime:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import "actual-css/js/full";
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The same entrypoint exposes the public enhancement API from the same runtime
|
|
73
|
+
instance, for adopters that extend the runtime they already run:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { applyEnhancement, enhance, registerEnhancement } from "actual-css/js/full";
|
|
77
|
+
|
|
78
|
+
registerEnhancement("autosubmit", (form) => {
|
|
79
|
+
// ...
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For anything beyond that extension API, keep using `actual-css/js/*`.
|
|
84
|
+
|
|
85
|
+
The same entrypoint produces the `dist/actual.full.js` standalone for a
|
|
86
|
+
classic `<script>`, where no global API is exposed. Static HTML that needs to
|
|
87
|
+
trigger something from its own inline `<script>` dispatches the module's
|
|
88
|
+
public event instead of calling into a global — `actual:status` for the
|
|
89
|
+
status bar, for example:
|
|
71
90
|
|
|
72
91
|
```html
|
|
73
92
|
<script src="actual.full.js"></script>
|
|
@@ -110,8 +129,7 @@ needed.
|
|
|
110
129
|
The JS is designed to run on its own, without Actual's stylesheet.
|
|
111
130
|
|
|
112
131
|
- **`data-enhance` tokens** register named behaviors. The runtime discovers
|
|
113
|
-
components through `data-enhance="tabs"`, `data-enhance="flyout"`,
|
|
114
|
-
`data-enhance="range"`, `data-enhance="scrollspy"`, and
|
|
132
|
+
components through `data-enhance="tabs"`, `data-enhance="flyout"`, `data-enhance="range"`, `data-enhance="scrollspy"`, and
|
|
115
133
|
`data-enhance="validation"` — no Actual presentation class needed.
|
|
116
134
|
Self-describing attributes (`data-mask`, `data-tooltip`,
|
|
117
135
|
`data-context-menu`) stay framework-neutral by construction.
|