actual-css 0.12.0 → 0.13.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -8
  3. package/components.json +8 -2
  4. package/dist/actual.full.css +505 -233
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/architecture.md +67 -0
  8. package/docs/design-notes/steps.md +45 -28
  9. package/docs/pages/components/avatar.md +17 -11
  10. package/docs/pages/components/breadcrumb.md +16 -2
  11. package/docs/pages/components/list.md +9 -13
  12. package/docs/pages/components/navbar.md +42 -22
  13. package/docs/pages/components/pagination.md +19 -0
  14. package/docs/pages/components/steps.md +24 -20
  15. package/docs/pages/enhancements/scrollspy.md +6 -6
  16. package/docs/pages/enhancements/tabs.md +74 -3
  17. package/docs/pages/forms/floating-field.md +21 -7
  18. package/docs/pages/forms/form-actions.md +3 -6
  19. package/docs/pages/forms/states-and-sizes.md +15 -15
  20. package/docs/pages/foundations/philosophy.md +97 -1
  21. package/docs/pages/foundations/tokens.md +40 -4
  22. package/docs/pages/guides/design-tools.md +1 -1
  23. package/docs/pages/guides/integrating-actual-css.md +74 -8
  24. package/docs/pages/guides/introduction.md +3 -2
  25. package/docs/pages/guides/modular-import.md +1 -1
  26. package/docs/pages/guides/progressive-enhancement.md +25 -7
  27. package/docs/pages/guides/upgrading.md +86 -9
  28. package/docs/pages/layout/center.md +2 -1
  29. package/docs/pages/layout/cluster.md +11 -4
  30. package/docs/pages/layout/grid.md +2 -0
  31. package/docs/pages/layout/sidebar-layout.md +3 -0
  32. package/docs/pages/layout/switcher.md +2 -0
  33. package/docs/pages/layout/topbar.md +6 -4
  34. package/docs/pages/patterns/icon-slot.md +17 -1
  35. package/docs/pages/utilities/flex.md +1 -1
  36. package/docs/pages/utilities/sizing-wrapping.md +51 -6
  37. package/package.json +5 -5
  38. package/src/cli/actual-css.js +1 -1
  39. package/src/css/components/alert.css +34 -34
  40. package/src/css/components/avatar.css +2 -18
  41. package/src/css/components/badge.css +0 -1
  42. package/src/css/components/breadcrumb.css +74 -8
  43. package/src/css/components/drawer.css +23 -42
  44. package/src/css/components/flyout.css +0 -1
  45. package/src/css/components/join.css +2 -2
  46. package/src/css/components/list.css +10 -10
  47. package/src/css/components/modal.css +17 -10
  48. package/src/css/components/navbar.css +10 -8
  49. package/src/css/components/pagination.css +5 -0
  50. package/src/css/components/scrollspy.css +1 -1
  51. package/src/css/components/steps.css +66 -12
  52. package/src/css/components/tab.css +134 -4
  53. package/src/css/core/focus.css +8 -6
  54. package/src/css/core/theme.css +6 -1
  55. package/src/css/core/tokens.css +4 -2
  56. package/src/css/forms/choice-card.css +1 -1
  57. package/src/css/forms/choice.css +1 -1
  58. package/src/css/forms/control.css +1 -1
  59. package/src/css/forms/floating-field.css +20 -18
  60. package/src/css/forms/form-actions.css +5 -8
  61. package/src/css/forms/native.css +2 -2
  62. package/src/css/forms/otp.css +4 -5
  63. package/src/css/forms/switch.css +1 -1
  64. package/src/css/forms/validation.css +23 -30
  65. package/src/css/layout/center.css +2 -0
  66. package/src/css/layout/cluster.css +14 -0
  67. package/src/css/layout/grid.css +7 -0
  68. package/src/css/layout/scroller.css +19 -3
  69. package/src/css/layout/sidebar-layout.css +3 -0
  70. package/src/css/layout/switcher.css +3 -0
  71. package/src/css/utilities/extra.css +3 -3
  72. package/src/js/full.js +4 -0
  73. package/src/js/inline-visible.js +54 -0
  74. package/src/js/parsed.js +21 -0
  75. package/src/js/tab.js +33 -7
  76. package/src/tooling/design-export.js +1 -1
  77. package/src/tooling/design-tokens.js +44 -33
  78. package/src/tooling/theme-contrast.js +32 -24
@@ -14,14 +14,17 @@
14
14
 
15
15
  - Use real tab semantics when panels switch in place.
16
16
  - Use normal links and `aria-current="page"` for page navigation that only looks like tabs.
17
- - On that link variant, `aria-current="page"` on the current link gets the same
18
- color and underline treatment as `aria-selected="true"` on a widget tab;
19
- `primary` tints it.
17
+ - On that link variant, `aria-current="page"` gets the same color and
18
+ underline treatment as `aria-selected="true"` on a widget tab; `primary`
19
+ tints it. A flyout trigger takes `"true"` (see below); any other value is
20
+ inert.
20
21
  - Tab selection never changes text metrics: the active tab keeps the shared
21
22
  weight and is marked by color and the indicator line.
22
23
  - JavaScript owns roving `tabindex`, `aria-selected`, `hidden`, and keyboard behavior.
23
24
  - Left/Right select tabs and wrap at the ends. Home/End jump to first/last. Down moves focus into the selected panel.
24
25
  - A tab list needs both `.tabs` and `role="tablist"`; `.tab` styles each trigger.
26
+ A tab belongs to its closest `role="tablist"`. `data-enhance="tabs"` on an
27
+ element without that role wires nothing and warns in the console.
25
28
  - Naming follows the container/item convention (like `menu` / `menu-item`):
26
29
  `.tabs` is the strip, `.tab` is one trigger. The JS module
27
30
  (`actual-css/js/tab`) is named for the file; the behavior token
@@ -86,6 +89,74 @@ JavaScript involved:
86
89
  </nav>
87
90
  ```
88
91
 
92
+ ## Equal-width tabs
93
+
94
+ To make a few tabs share the strip's width (segmented, full-width, fill), set
95
+ `--tab-flex: 1 1 0` on the strip. `.switcher` does not apply here: a tab strip
96
+ never wraps, so it could never stack.
97
+
98
+ ```html demo
99
+ <nav aria-label="Pricing sections">
100
+ <ul class="tabs" style="--tab-flex: 1 1 0">
101
+ <li><a class="tab primary" href="#reasons" aria-current="page">Reasons</a></li>
102
+ <li><a class="tab" href="#rates">Rates</a></li>
103
+ </ul>
104
+ </nav>
105
+ ```
106
+
107
+ ## Too many tabs
108
+
109
+ A horizontal strip is one row and never wraps, nor does a label: a second line
110
+ would not match the Left/Right keys and reads as a nested level. When the tabs
111
+ do not fit (narrow screen, mobile, long translations), the strip scrolls
112
+ horizontally without a scrollbar; in supporting browsers, the edges it can
113
+ still scroll toward fade out. Selecting or focusing the tab clipped at the edge
114
+ scrolls it into view with the next one peeking, so a click, the arrow keys or a
115
+ swipe move along the strip. A tab selected in the markup starts in view too,
116
+ when the strip is visible at connect; the placement moves the strip only, never
117
+ the page. In supporting browsers, so does the current link tab (or flyout
118
+ trigger), without the runtime; elsewhere the strip starts at its first tab. A
119
+ vertical strip (`aria-orientation="vertical"`) keeps wrapping its labels.
120
+
121
+ Scrolling is the fallback, not a responsive design. If a strip regularly
122
+ overflows, shorten the labels, group sections, or switch to a vertical rail.
123
+
124
+ ## Tab with a flyout
125
+
126
+ In navigation tabs, a `.tab` can be a [flyout](flyout.md) trigger for a group of
127
+ pages. The link to the current page inside its panel carries
128
+ `aria-current="page"`. The trigger is not a page, so it takes
129
+ `aria-current="true"` while its panel holds the current page: the section is
130
+ the current item of the strip. This is the only place a `.tab` takes `true`.
131
+
132
+ ```html demo
133
+ <nav aria-label="Account">
134
+ <ul class="tabs">
135
+ <li><a class="tab" href="#profile">Profile</a></li>
136
+ <li><a class="tab" href="#security">Security</a></li>
137
+ <li class="flyout-trigger">
138
+ <button class="tab"
139
+ type="button"
140
+ data-enhance="flyout"
141
+ aria-current="true"
142
+ aria-expanded="false"
143
+ aria-controls="billing-menu">
144
+ Billing <i class="ti ti-chevron-down" aria-hidden="true"></i>
145
+ </button>
146
+ <ul class="flyout menu" id="billing-menu" hidden>
147
+ <li><a class="menu-item" href="#invoices" aria-current="page">Invoices</a></li>
148
+ <li><a class="menu-item" href="#payment">Payment methods</a></li>
149
+ </ul>
150
+ </li>
151
+ </ul>
152
+ </nav>
153
+ ```
154
+
155
+ Do not put a flyout trigger in a `role="tablist"`: a tablist holds only
156
+ `role="tab"` items, and the arrow keys of the two widgets collide.
157
+
89
158
  ## CSS hooks
90
159
 
160
+ - `--tab-flex` — flex of each item; set `1 1 0` on the strip for equal-width tabs
161
+ (segmented, fill the width). A strip that cannot fit its labels still scrolls.
91
162
  - `--tab-gap` — space between an icon and the label text (default `0.375em`).
@@ -7,11 +7,12 @@ the label rests inside the control and floats to the top when the control is
7
7
  focused or filled. It composes the existing `.field` layout and the text
8
8
  controls (`.input`, `.textarea`, `.select`) with zero JavaScript.
9
9
 
10
- The cell holds exactly one control and its label, with the control before the
11
- label. Label placement is linked through the general sibling combinator, so an
12
- injected node between the two (a password manager, an extension wrapper) is
13
- tolerated. Help and error messages stay in the surrounding `.field`, otherwise
14
- the resting label centers on the whole field instead of the control.
10
+ The cell holds exactly one control and its label, in either order: a form
11
+ theme that renders the label before the widget needs no reordering. An
12
+ injected sibling (a password manager, an extension wrapper) is tolerated as
13
+ long as the control stays a direct child of the cell. Help and error messages
14
+ stay in the surrounding `.field`, otherwise the resting label centers on the
15
+ whole field instead of the control.
15
16
 
16
17
  ```html demo
17
18
  <div class="field">
@@ -28,6 +29,17 @@ can detect emptiness — the space keeps the placeholder invisible while making
28
29
  the state detectable. `:autofill` is covered too, so a password manager that
29
30
  prefills a field still lifts the label.
30
31
 
32
+ The same cell with the label first, as a label-then-widget form theme renders it:
33
+
34
+ ```html demo
35
+ <div class="field">
36
+ <label class="floating-field">
37
+ <span class="field-label">Full name</span>
38
+ <input class="input" type="text" autocomplete="name" placeholder=" " />
39
+ </label>
40
+ </div>
41
+ ```
42
+
31
43
  ## Textarea
32
44
 
33
45
  A textarea's empty label starts at the control's normal first-line position,
@@ -89,8 +101,10 @@ line on top of the control's minimum size.
89
101
  Validation and focus come from the existing form contracts: the control's
90
102
  border and focus ring flip through `--form-invalid-border` and the shared
91
103
  control focus rules. Coloring the label itself on focus or invalid state is
92
- left to the surrounding recipe — the joined floating form in the
93
- [blocks demo](../../demo/templates/blocks.html) shows one such recipe.
104
+ left to the surrounding recipe; select the label through the cell
105
+ (`.floating-field:has(> :focus) > .field-label`) so it holds in both orders.
106
+ The joined floating form in the [blocks demo](../../demo/templates/blocks.html)
107
+ shows one such recipe.
94
108
 
95
109
  > Not currently compatible with `.input-icon`; use standard labels when an
96
110
  > embedded leading icon is required.
@@ -8,10 +8,6 @@
8
8
  policy for form actions and the sticky behaviour. The space before the row
9
9
  belongs to the parent layout: put `.stack` on the form.
10
10
 
11
- `.form-actions` refines `.cluster` with alignment rules of equal specificity,
12
- so it wins on import order. In a modular build, import `layout/cluster` before
13
- `forms/form-actions`; the family entries and the full bundle already do.
14
-
15
11
  ```html demo
16
12
  <form class="stack" novalidate>
17
13
  <label class="field">
@@ -65,8 +61,9 @@ In application CSS, the same policy is one declaration on the region:
65
61
  }
66
62
  ```
67
63
 
68
- On a form action row, `--cluster-justify` and `--cluster-align` have no effect;
69
- use the hooks above, or a `.justify-content-*` / `.items-*` utility on one row.
64
+ On a form action row, use the hooks above rather than `--cluster-justify` /
65
+ `--cluster-align`: the role writes those from its own hooks. For one row, a
66
+ `.justify-content-*` / `.items-*` utility also works.
70
67
  A split pair keeps the dismissive action at the leading edge and the committing
71
68
  one at the trailing edge:
72
69
 
@@ -26,8 +26,8 @@ together. Density contexts leave both unchanged.
26
26
  <form class="stack" novalidate>
27
27
  <fieldset class="field-group">
28
28
  <legend class="field-label">Text control states</legend>
29
- <div class="stack" style="--grid-min: 14rem;">
30
- <div class="grid items-start">
29
+ <div class="stack">
30
+ <div class="grid items-start" style="--grid-min: 14rem;">
31
31
  <label class="field">
32
32
  <span class="field-label">Editable</span>
33
33
  <input class="input" type="text" value="Ocean Beach Clinic" />
@@ -46,7 +46,7 @@ together. Density contexts leave both unchanged.
46
46
  </label>
47
47
  </div>
48
48
 
49
- <div class="grid items-start">
49
+ <div class="grid items-start" style="--grid-min: 14rem;">
50
50
  <label class="field">
51
51
  <span class="field-label">Editable notes</span>
52
52
  <textarea class="textarea">Patient asked for an invoice copy.</textarea>
@@ -67,8 +67,8 @@ together. Density contexts leave both unchanged.
67
67
 
68
68
  <fieldset class="field-group">
69
69
  <legend class="field-label">Choice and select states</legend>
70
- <div class="stack" style="--grid-min: 14rem;">
71
- <div class="grid items-start">
70
+ <div class="stack">
71
+ <div class="grid items-start" style="--grid-min: 14rem;">
72
72
  <label class="field">
73
73
  <span class="field-label">Editable select</span>
74
74
  <select class="select">
@@ -98,7 +98,7 @@ together. Density contexts leave both unchanged.
98
98
  </label>
99
99
  </div>
100
100
 
101
- <div class="grid items-start">
101
+ <div class="grid items-start" style="--grid-min: 14rem;">
102
102
  <label class="choice">
103
103
  <input class="check" type="checkbox" />
104
104
  <span>
@@ -140,7 +140,7 @@ together. Density contexts leave both unchanged.
140
140
  </label>
141
141
  </div>
142
142
 
143
- <div class="grid items-start">
143
+ <div class="grid items-start" style="--grid-min: 14rem;">
144
144
  <label class="choice">
145
145
  <input class="radio" type="radio" name="demo-radio-state" />
146
146
  <span>
@@ -174,7 +174,7 @@ together. Density contexts leave both unchanged.
174
174
  </label>
175
175
  </div>
176
176
 
177
- <div class="grid items-start">
177
+ <div class="grid items-start" style="--grid-min: 14rem;">
178
178
  <label class="choice">
179
179
  <input class="switch" type="checkbox" role="switch" />
180
180
  <span>
@@ -212,8 +212,8 @@ together. Density contexts leave both unchanged.
212
212
 
213
213
  <fieldset class="field-group">
214
214
  <legend class="field-label">Control sizes</legend>
215
- <div class="stack" style="--grid-min: 14rem;">
216
- <div class="grid items-start">
215
+ <div class="stack">
216
+ <div class="grid items-start" style="--grid-min: 14rem;">
217
217
  <label class="field sm">
218
218
  <span class="field-label">Small field</span>
219
219
  <input class="input" type="text" value="sm" />
@@ -230,7 +230,7 @@ together. Density contexts leave both unchanged.
230
230
  </label>
231
231
  </div>
232
232
 
233
- <div class="grid items-start">
233
+ <div class="grid items-start" style="--grid-min: 14rem;">
234
234
  <label class="field sm">
235
235
  <span class="field-label">Small select</span>
236
236
  <select class="select">
@@ -253,7 +253,7 @@ together. Density contexts leave both unchanged.
253
253
  </label>
254
254
  </div>
255
255
 
256
- <div class="grid items-start">
256
+ <div class="grid items-start" style="--grid-min: 14rem;">
257
257
  <label class="field sm">
258
258
  <span class="field-label">Small textarea</span>
259
259
  <textarea class="textarea" rows="2">Short note</textarea>
@@ -270,14 +270,14 @@ together. Density contexts leave both unchanged.
270
270
  </label>
271
271
  </div>
272
272
 
273
- <div class="grid items-start">
273
+ <div class="grid items-start" style="--grid-min: 14rem;">
274
274
  <button class="btn outline sm" type="button">Small</button>
275
275
  <button class="btn outline" type="button">Default</button>
276
276
  <button class="btn outline lg" type="button">Large</button>
277
277
  </div>
278
278
 
279
279
  <!-- Size belongs on the choice wrapper so label and control move together. -->
280
- <div class="grid items-start">
280
+ <div class="grid items-start" style="--grid-min: 14rem;">
281
281
  <label class="choice sm">
282
282
  <input class="check" type="checkbox" checked />
283
283
  <span>Small checkbox</span>
@@ -297,7 +297,7 @@ together. Density contexts leave both unchanged.
297
297
  </div>
298
298
 
299
299
  <!-- The underlying typography hook remains available for custom scales. -->
300
- <div class="grid items-start" style="--control-font-size: var(--font-size-sm);">
300
+ <div class="grid items-start" style="--grid-min: 14rem; --control-font-size: var(--font-size-sm);">
301
301
  <label class="choice">
302
302
  <input class="check" type="checkbox" checked />
303
303
  <span>Smaller type</span>
@@ -1,6 +1,102 @@
1
1
  # Philosophy
2
2
 
3
- How components, intents, variants, and density compose — and what each layer is responsible for.
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
 
@@ -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 applies `--focus-outline` and `--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.
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.
@@ -376,7 +411,7 @@ Motion durations keep transitions consistent and easy to disable.
376
411
  }
377
412
  ```
378
413
 
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 transition declarations (`.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`.
414
+ 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
415
 
381
416
  `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
417
 
@@ -503,7 +538,7 @@ Rules:
503
538
 
504
539
  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
540
 
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.
541
+ 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
542
 
508
543
  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
544
 
@@ -518,6 +553,7 @@ A full theme can override:
518
553
  - intent colors and foreground pairs
519
554
  - surface, text, border, focus, and overlay colors
520
555
  - `--surface-opaque` and `--surface-solid-fg`, when surfaces are translucent
556
+ - `--border-control`, when controls need a stronger edge than separators
521
557
  - radius tokens
522
558
  - optional shadow, motion, typography, and soft-variant mix tokens
523
559
 
@@ -6,7 +6,7 @@
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
 
@@ -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 §6–7).
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. Tune before replacing
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
- ## 4. Reuse the existing vocabulary
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
- ## 5. Use structural component markup
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
- ## 6. Keep application CSS for application identity
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
- ## 7. Before adding a CSS rule
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. Is this genuinely application-specific CSS?
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 6, write the CSS.
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.12/dist/actual.full.min.css">
56
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.13/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` before the forms.
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