@tenphi/tasty 0.0.0-snapshot.1c5dff6 → 0.0.0-snapshot.1f1c00a

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 (331) hide show
  1. package/README.md +377 -238
  2. package/dist/babel-rZcEwPLM.d.ts +82 -0
  3. package/dist/chunks/async-storage-CG5HnUT5.js +20 -0
  4. package/dist/chunks/async-storage-CG5HnUT5.js.map +1 -0
  5. package/dist/chunks/build-config-CL4eRu2z.js +42 -0
  6. package/dist/chunks/build-config-CL4eRu2z.js.map +1 -0
  7. package/dist/chunks/collector-D2ARGT2L.js +208 -0
  8. package/dist/chunks/collector-D2ARGT2L.js.map +1 -0
  9. package/dist/chunks/config-engine-DEmmPPdp.js +272 -0
  10. package/dist/chunks/config-engine-DEmmPPdp.js.map +1 -0
  11. package/dist/chunks/css-definitions-DotDkIXt.js +876 -0
  12. package/dist/chunks/css-definitions-DotDkIXt.js.map +1 -0
  13. package/dist/chunks/css-resources-P7IBUUZs.js +142 -0
  14. package/dist/chunks/css-resources-P7IBUUZs.js.map +1 -0
  15. package/dist/chunks/debug-D8pg9RGI.js +502 -0
  16. package/dist/chunks/debug-D8pg9RGI.js.map +1 -0
  17. package/dist/chunks/dsl-DRhFWQzo.js +1378 -0
  18. package/dist/chunks/dsl-DRhFWQzo.js.map +1 -0
  19. package/dist/chunks/hydration-TRcz5vnk.js +41 -0
  20. package/dist/chunks/hydration-TRcz5vnk.js.map +1 -0
  21. package/dist/chunks/react-runtime-BpxlOTbO.js +562 -0
  22. package/dist/chunks/react-runtime-BpxlOTbO.js.map +1 -0
  23. package/dist/chunks/runtime-engine-CKFXUH-A.js +2416 -0
  24. package/dist/chunks/runtime-engine-CKFXUH-A.js.map +1 -0
  25. package/dist/chunks/runtime-state-BxaLhVdP.js +127 -0
  26. package/dist/chunks/runtime-state-BxaLhVdP.js.map +1 -0
  27. package/dist/{utils/merge-styles.js → chunks/shared-utils-y-HjvPN4.js} +61 -58
  28. package/dist/chunks/shared-utils-y-HjvPN4.js.map +1 -0
  29. package/dist/chunks/style-engine-BBbBuSkT.js +5260 -0
  30. package/dist/chunks/style-engine-BBbBuSkT.js.map +1 -0
  31. package/dist/{zero/extractor.js → chunks/zero-engine-gJrKBRiw.js} +126 -66
  32. package/dist/chunks/zero-engine-gJrKBRiw.js.map +1 -0
  33. package/dist/collector-w4mBj5gd.d.ts +164 -0
  34. package/dist/config-CZWCzBr6.d.ts +1349 -0
  35. package/dist/core/index.d.ts +5 -33
  36. package/dist/core/index.js +9 -26
  37. package/dist/index-G9JuwM9p.d.ts +1920 -0
  38. package/dist/index.d.ts +5 -48
  39. package/dist/index.js +11 -33
  40. package/dist/{utils/merge-styles.d.ts → merge-styles-VXwJ0Frr.d.ts} +3 -4
  41. package/dist/ssr/astro-client.d.ts +1 -0
  42. package/dist/ssr/astro-client.js +6 -0
  43. package/dist/ssr/astro-client.js.map +1 -0
  44. package/dist/ssr/astro-middleware-extract-static.d.ts +10 -0
  45. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  46. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  47. package/dist/ssr/astro-middleware-extract.d.ts +10 -0
  48. package/dist/ssr/astro-middleware-extract.js +9 -0
  49. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  50. package/dist/ssr/astro-middleware-static.d.ts +17 -0
  51. package/dist/ssr/astro-middleware-static.js +7 -0
  52. package/dist/ssr/astro-middleware-static.js.map +1 -0
  53. package/dist/ssr/astro-middleware.d.ts +18 -0
  54. package/dist/ssr/astro-middleware.js +7 -0
  55. package/dist/ssr/astro-middleware.js.map +1 -0
  56. package/dist/ssr/astro.d.ts +127 -0
  57. package/dist/ssr/astro.js +221 -0
  58. package/dist/ssr/astro.js.map +1 -0
  59. package/dist/ssr/index.d.ts +36 -0
  60. package/dist/ssr/index.js +10 -0
  61. package/dist/ssr/index.js.map +1 -0
  62. package/dist/ssr/next-config.d.ts +64 -0
  63. package/dist/ssr/next-config.js +128 -0
  64. package/dist/ssr/next-config.js.map +1 -0
  65. package/dist/ssr/next.d.ts +46 -0
  66. package/dist/ssr/next.js +60 -0
  67. package/dist/ssr/next.js.map +1 -0
  68. package/dist/static/index.d.ts +90 -5
  69. package/dist/static/index.js +31 -4
  70. package/dist/static/index.js.map +1 -0
  71. package/dist/static/inject.d.ts +4 -0
  72. package/dist/static/inject.js +17 -0
  73. package/dist/static/inject.js.map +1 -0
  74. package/dist/types-JEY8PVYP.d.ts +1567 -0
  75. package/dist/zero/babel.d.ts +2 -113
  76. package/dist/zero/babel.js +230 -106
  77. package/dist/zero/babel.js.map +1 -1
  78. package/dist/zero/index.d.ts +80 -3
  79. package/dist/zero/index.js +2 -4
  80. package/dist/zero/next.d.ts +57 -33
  81. package/dist/zero/next.js +93 -59
  82. package/dist/zero/next.js.map +1 -1
  83. package/docs/README.md +39 -0
  84. package/docs/adoption.md +323 -0
  85. package/docs/ai-agents.md +250 -0
  86. package/docs/comparison.md +448 -0
  87. package/docs/configuration.md +684 -56
  88. package/docs/debug.md +170 -343
  89. package/docs/design-system.md +455 -0
  90. package/docs/dsl.md +1065 -0
  91. package/docs/getting-started.md +240 -0
  92. package/docs/injector.md +159 -53
  93. package/docs/methodology.md +648 -0
  94. package/docs/migration-v3.md +285 -0
  95. package/docs/pipeline.md +765 -0
  96. package/docs/plugins.md +353 -0
  97. package/docs/react-api.md +683 -0
  98. package/docs/runtime-benchmarks.md +388 -0
  99. package/docs/ssr.md +610 -0
  100. package/docs/styles.md +211 -63
  101. package/docs/tasty-static.md +273 -67
  102. package/package.json +182 -58
  103. package/tasty.config.ts +6 -0
  104. package/dist/_virtual/_rolldown/runtime.js +0 -8
  105. package/dist/chunks/cacheKey.js +0 -70
  106. package/dist/chunks/cacheKey.js.map +0 -1
  107. package/dist/chunks/definitions.d.ts +0 -37
  108. package/dist/chunks/definitions.js +0 -260
  109. package/dist/chunks/definitions.js.map +0 -1
  110. package/dist/chunks/renderChunk.js +0 -61
  111. package/dist/chunks/renderChunk.js.map +0 -1
  112. package/dist/config.d.ts +0 -288
  113. package/dist/config.js +0 -403
  114. package/dist/config.js.map +0 -1
  115. package/dist/debug.d.ts +0 -204
  116. package/dist/debug.js +0 -733
  117. package/dist/debug.js.map +0 -1
  118. package/dist/hooks/useGlobalStyles.d.ts +0 -27
  119. package/dist/hooks/useGlobalStyles.js +0 -56
  120. package/dist/hooks/useGlobalStyles.js.map +0 -1
  121. package/dist/hooks/useKeyframes.d.ts +0 -56
  122. package/dist/hooks/useKeyframes.js +0 -54
  123. package/dist/hooks/useKeyframes.js.map +0 -1
  124. package/dist/hooks/useProperty.d.ts +0 -79
  125. package/dist/hooks/useProperty.js +0 -91
  126. package/dist/hooks/useProperty.js.map +0 -1
  127. package/dist/hooks/useRawCSS.d.ts +0 -53
  128. package/dist/hooks/useRawCSS.js +0 -28
  129. package/dist/hooks/useRawCSS.js.map +0 -1
  130. package/dist/hooks/useStyles.d.ts +0 -40
  131. package/dist/hooks/useStyles.js +0 -169
  132. package/dist/hooks/useStyles.js.map +0 -1
  133. package/dist/injector/index.d.ts +0 -157
  134. package/dist/injector/index.js +0 -154
  135. package/dist/injector/index.js.map +0 -1
  136. package/dist/injector/injector.d.ts +0 -139
  137. package/dist/injector/injector.js +0 -417
  138. package/dist/injector/injector.js.map +0 -1
  139. package/dist/injector/sheet-manager.d.ts +0 -135
  140. package/dist/injector/sheet-manager.js +0 -728
  141. package/dist/injector/sheet-manager.js.map +0 -1
  142. package/dist/injector/types.d.ts +0 -144
  143. package/dist/keyframes/index.js +0 -206
  144. package/dist/keyframes/index.js.map +0 -1
  145. package/dist/parser/classify.js +0 -319
  146. package/dist/parser/classify.js.map +0 -1
  147. package/dist/parser/const.js +0 -33
  148. package/dist/parser/const.js.map +0 -1
  149. package/dist/parser/lru.js +0 -109
  150. package/dist/parser/lru.js.map +0 -1
  151. package/dist/parser/parser.d.ts +0 -25
  152. package/dist/parser/parser.js +0 -116
  153. package/dist/parser/parser.js.map +0 -1
  154. package/dist/parser/tokenizer.js +0 -69
  155. package/dist/parser/tokenizer.js.map +0 -1
  156. package/dist/parser/types.d.ts +0 -51
  157. package/dist/parser/types.js +0 -46
  158. package/dist/parser/types.js.map +0 -1
  159. package/dist/pipeline/conditions.d.ts +0 -134
  160. package/dist/pipeline/conditions.js +0 -406
  161. package/dist/pipeline/conditions.js.map +0 -1
  162. package/dist/pipeline/exclusive.js +0 -231
  163. package/dist/pipeline/exclusive.js.map +0 -1
  164. package/dist/pipeline/index.d.ts +0 -53
  165. package/dist/pipeline/index.js +0 -660
  166. package/dist/pipeline/index.js.map +0 -1
  167. package/dist/pipeline/materialize.js +0 -856
  168. package/dist/pipeline/materialize.js.map +0 -1
  169. package/dist/pipeline/parseStateKey.d.ts +0 -15
  170. package/dist/pipeline/parseStateKey.js +0 -451
  171. package/dist/pipeline/parseStateKey.js.map +0 -1
  172. package/dist/pipeline/simplify.js +0 -516
  173. package/dist/pipeline/simplify.js.map +0 -1
  174. package/dist/pipeline/warnings.js +0 -18
  175. package/dist/pipeline/warnings.js.map +0 -1
  176. package/dist/plugins/okhsl-plugin.d.ts +0 -35
  177. package/dist/plugins/okhsl-plugin.js +0 -371
  178. package/dist/plugins/okhsl-plugin.js.map +0 -1
  179. package/dist/plugins/types.d.ts +0 -69
  180. package/dist/properties/index.js +0 -221
  181. package/dist/properties/index.js.map +0 -1
  182. package/dist/properties/property-type-resolver.d.ts +0 -24
  183. package/dist/properties/property-type-resolver.js +0 -83
  184. package/dist/properties/property-type-resolver.js.map +0 -1
  185. package/dist/states/index.d.ts +0 -49
  186. package/dist/states/index.js +0 -416
  187. package/dist/states/index.js.map +0 -1
  188. package/dist/static/tastyStatic.d.ts +0 -46
  189. package/dist/static/tastyStatic.js +0 -31
  190. package/dist/static/tastyStatic.js.map +0 -1
  191. package/dist/static/types.d.ts +0 -49
  192. package/dist/static/types.js +0 -24
  193. package/dist/static/types.js.map +0 -1
  194. package/dist/styles/align.d.ts +0 -15
  195. package/dist/styles/align.js +0 -14
  196. package/dist/styles/align.js.map +0 -1
  197. package/dist/styles/border.d.ts +0 -25
  198. package/dist/styles/border.js +0 -114
  199. package/dist/styles/border.js.map +0 -1
  200. package/dist/styles/color.d.ts +0 -14
  201. package/dist/styles/color.js +0 -23
  202. package/dist/styles/color.js.map +0 -1
  203. package/dist/styles/createStyle.js +0 -77
  204. package/dist/styles/createStyle.js.map +0 -1
  205. package/dist/styles/dimension.js +0 -97
  206. package/dist/styles/dimension.js.map +0 -1
  207. package/dist/styles/display.d.ts +0 -37
  208. package/dist/styles/display.js +0 -67
  209. package/dist/styles/display.js.map +0 -1
  210. package/dist/styles/fade.d.ts +0 -15
  211. package/dist/styles/fade.js +0 -58
  212. package/dist/styles/fade.js.map +0 -1
  213. package/dist/styles/fill.d.ts +0 -42
  214. package/dist/styles/fill.js +0 -52
  215. package/dist/styles/fill.js.map +0 -1
  216. package/dist/styles/flow.d.ts +0 -16
  217. package/dist/styles/flow.js +0 -12
  218. package/dist/styles/flow.js.map +0 -1
  219. package/dist/styles/gap.d.ts +0 -31
  220. package/dist/styles/gap.js +0 -37
  221. package/dist/styles/gap.js.map +0 -1
  222. package/dist/styles/height.d.ts +0 -17
  223. package/dist/styles/height.js +0 -20
  224. package/dist/styles/height.js.map +0 -1
  225. package/dist/styles/index.d.ts +0 -2
  226. package/dist/styles/index.js +0 -9
  227. package/dist/styles/index.js.map +0 -1
  228. package/dist/styles/inset.d.ts +0 -52
  229. package/dist/styles/inset.js +0 -150
  230. package/dist/styles/inset.js.map +0 -1
  231. package/dist/styles/justify.d.ts +0 -15
  232. package/dist/styles/justify.js +0 -14
  233. package/dist/styles/justify.js.map +0 -1
  234. package/dist/styles/list.d.ts +0 -16
  235. package/dist/styles/list.js +0 -98
  236. package/dist/styles/list.js.map +0 -1
  237. package/dist/styles/margin.d.ts +0 -24
  238. package/dist/styles/margin.js +0 -104
  239. package/dist/styles/margin.js.map +0 -1
  240. package/dist/styles/outline.d.ts +0 -29
  241. package/dist/styles/outline.js +0 -65
  242. package/dist/styles/outline.js.map +0 -1
  243. package/dist/styles/padding.d.ts +0 -24
  244. package/dist/styles/padding.js +0 -104
  245. package/dist/styles/padding.js.map +0 -1
  246. package/dist/styles/predefined.d.ts +0 -73
  247. package/dist/styles/predefined.js +0 -241
  248. package/dist/styles/predefined.js.map +0 -1
  249. package/dist/styles/preset.d.ts +0 -47
  250. package/dist/styles/preset.js +0 -126
  251. package/dist/styles/preset.js.map +0 -1
  252. package/dist/styles/radius.d.ts +0 -14
  253. package/dist/styles/radius.js +0 -51
  254. package/dist/styles/radius.js.map +0 -1
  255. package/dist/styles/scrollbar.d.ts +0 -23
  256. package/dist/styles/scrollbar.js +0 -34
  257. package/dist/styles/scrollbar.js.map +0 -1
  258. package/dist/styles/shadow.d.ts +0 -14
  259. package/dist/styles/shadow.js +0 -24
  260. package/dist/styles/shadow.js.map +0 -1
  261. package/dist/styles/styledScrollbar.d.ts +0 -47
  262. package/dist/styles/styledScrollbar.js +0 -38
  263. package/dist/styles/styledScrollbar.js.map +0 -1
  264. package/dist/styles/transition.d.ts +0 -14
  265. package/dist/styles/transition.js +0 -158
  266. package/dist/styles/transition.js.map +0 -1
  267. package/dist/styles/types.d.ts +0 -498
  268. package/dist/styles/width.d.ts +0 -17
  269. package/dist/styles/width.js +0 -20
  270. package/dist/styles/width.js.map +0 -1
  271. package/dist/tasty.d.ts +0 -982
  272. package/dist/tasty.js +0 -206
  273. package/dist/tasty.js.map +0 -1
  274. package/dist/tokens/typography.d.ts +0 -19
  275. package/dist/tokens/typography.js +0 -237
  276. package/dist/tokens/typography.js.map +0 -1
  277. package/dist/types.d.ts +0 -184
  278. package/dist/utils/cache-wrapper.js +0 -26
  279. package/dist/utils/cache-wrapper.js.map +0 -1
  280. package/dist/utils/case-converter.js +0 -8
  281. package/dist/utils/case-converter.js.map +0 -1
  282. package/dist/utils/colors.d.ts +0 -5
  283. package/dist/utils/colors.js +0 -9
  284. package/dist/utils/colors.js.map +0 -1
  285. package/dist/utils/css-types.d.ts +0 -7
  286. package/dist/utils/dotize.d.ts +0 -26
  287. package/dist/utils/dotize.js +0 -122
  288. package/dist/utils/dotize.js.map +0 -1
  289. package/dist/utils/filter-base-props.d.ts +0 -15
  290. package/dist/utils/filter-base-props.js +0 -45
  291. package/dist/utils/filter-base-props.js.map +0 -1
  292. package/dist/utils/get-display-name.d.ts +0 -7
  293. package/dist/utils/get-display-name.js +0 -10
  294. package/dist/utils/get-display-name.js.map +0 -1
  295. package/dist/utils/hsl-to-rgb.js +0 -38
  296. package/dist/utils/hsl-to-rgb.js.map +0 -1
  297. package/dist/utils/is-dev-env.js +0 -19
  298. package/dist/utils/is-dev-env.js.map +0 -1
  299. package/dist/utils/is-valid-element-type.js +0 -15
  300. package/dist/utils/is-valid-element-type.js.map +0 -1
  301. package/dist/utils/merge-styles.js.map +0 -1
  302. package/dist/utils/mod-attrs.d.ts +0 -8
  303. package/dist/utils/mod-attrs.js +0 -21
  304. package/dist/utils/mod-attrs.js.map +0 -1
  305. package/dist/utils/okhsl-to-rgb.js +0 -296
  306. package/dist/utils/okhsl-to-rgb.js.map +0 -1
  307. package/dist/utils/process-tokens.d.ts +0 -31
  308. package/dist/utils/process-tokens.js +0 -171
  309. package/dist/utils/process-tokens.js.map +0 -1
  310. package/dist/utils/resolve-recipes.d.ts +0 -17
  311. package/dist/utils/resolve-recipes.js +0 -147
  312. package/dist/utils/resolve-recipes.js.map +0 -1
  313. package/dist/utils/selector-transform.js +0 -32
  314. package/dist/utils/selector-transform.js.map +0 -1
  315. package/dist/utils/string.js +0 -8
  316. package/dist/utils/string.js.map +0 -1
  317. package/dist/utils/styles.d.ts +0 -177
  318. package/dist/utils/styles.js +0 -730
  319. package/dist/utils/styles.js.map +0 -1
  320. package/dist/utils/typography.d.ts +0 -36
  321. package/dist/utils/typography.js +0 -53
  322. package/dist/utils/typography.js.map +0 -1
  323. package/dist/utils/warnings.d.ts +0 -16
  324. package/dist/utils/warnings.js +0 -16
  325. package/dist/utils/warnings.js.map +0 -1
  326. package/dist/zero/css-writer.d.ts +0 -45
  327. package/dist/zero/css-writer.js +0 -74
  328. package/dist/zero/css-writer.js.map +0 -1
  329. package/dist/zero/extractor.d.ts +0 -24
  330. package/dist/zero/extractor.js.map +0 -1
  331. package/docs/usage.md +0 -695
package/docs/dsl.md ADDED
@@ -0,0 +1,1065 @@
1
+ # Style DSL Reference
2
+
3
+ This is the language behind Tasty’s predictable state resolution. Every property can use a state map; later branches declare higher priority, and Tasty compiles them so only one generated selector can match.
4
+
5
+ The same value syntax, state maps, tokens, units, extension semantics, and special declarations apply to React `tasty()`—whether rendered on the client or server—and build-time `tastyStatic()`.
6
+
7
+ For the React API (`tasty()`, style functions, component props), see [React API](react-api.md). For all enhanced style properties, see [Style Properties](styles.md). For global configuration, see [Configuration](configuration.md).
8
+
9
+ ---
10
+
11
+ ## Dictionary
12
+
13
+ ### Style Mapping
14
+
15
+ Object where keys represent states and values are the styles to apply:
16
+
17
+ ```jsx
18
+ fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
19
+ ```
20
+
21
+ #### State Key Types
22
+
23
+ | Syntax | Example | Generated CSS |
24
+ | ------------------ | ------------------------ | ------------------------------------------------------------------------ |
25
+ | Boolean modifier | `hovered` | `[data-hovered]` |
26
+ | Value modifier | `theme=danger` | `[data-theme="danger"]` |
27
+ | Pseudo-class | `:hover` | `:hover` |
28
+ | Class selector | `.active` | `.active` |
29
+ | Attribute selector | `[aria-expanded="true"]` | `[aria-expanded="true"]` |
30
+ | Combined (AND) | `hovered & .active` | `[data-hovered].active` |
31
+ | Combined (OR) | `hovered \| focused` | `[data-hovered], [data-focused]` |
32
+ | Negated (NOT) | `!disabled` | `:not([data-disabled])` |
33
+ | Exclusive (XOR) | `hovered ^ focused` | `[data-hovered]:not([data-focused]), :not([data-hovered])[data-focused]` |
34
+
35
+ #### Automatic Native States
36
+
37
+ `disabled` and `checked` are built-in automatic states. On a Tasty component, they track the corresponding native prop or attribute, so these pairs express the same component state:
38
+
39
+ | Concise Tasty state | Explicit attribute selector |
40
+ | ------------------- | --------------------------- |
41
+ | `disabled` | `[disabled]` |
42
+ | `checked` | `[checked]` |
43
+
44
+ Prefer the concise form when authoring Tasty component styles. Explicit attribute selectors remain valid and can be useful when targeting arbitrary attributes or when CSS-shaped syntax makes an introductory example easier to recognize.
45
+
46
+ Operator precedence (highest to lowest): `!` (NOT) > `^` (XOR) > `|` (OR) > `&` (AND). Use parentheses to override: `hovered & (pressed ^ focused)`.
47
+
48
+ `^` (XOR) means "exactly one of" — `A ^ B` expands to `(A & !B) | (!A & B)`. This is useful for mutually exclusive states where exactly one should be active:
49
+
50
+ ```jsx
51
+ fill: {
52
+ '': '#surface',
53
+ 'hovered ^ focused': '#accent', // active when hovered OR focused, but not both
54
+ }
55
+ ```
56
+
57
+ Modifier and class names start with a letter and may be a single letter (`x` → `[data-x]`, `.x`). Tasty emits an `INVALID_STATE_KEY` warning for a key it cannot fully read and skips the unreadable parts. That covers characters outside the notation (`1st`, or `@media (w < 600px)` with a space before the parenthesis), an operator missing a state (`hovered &`), and two states with no operator between them (`hovered focused`). A partly read key can apply in states you did not mean, so fix the key rather than rely on what it produces.
58
+
59
+ #### Default State Ordering
60
+
61
+ Key order sets priority — later keys win and turn off earlier ones via negation. The bare default (`''`) is the lowest-priority state, so it **must be the first key**. If it appears after other states, Tasty moves it to the front and emits a dev warning (`MISPLACED_DEFAULT_STATE`); otherwise it would override every state above it.
62
+
63
+ ```jsx
64
+ // Correct — default first
65
+ color: { '': '#text', hovered: '#accent' }
66
+
67
+ // Auto-corrected with a warning — '' moved to the front
68
+ color: { hovered: '#accent', '': '#text' }
69
+ ```
70
+
71
+ The bare `''` default still **receives negation** (it is turned off when a higher-priority state matches). For a value that must always apply as a guaranteed floor — even where a query is _unknown_ — use the [`_` fallback floor](#_--fallback-floor) instead. The two can coexist: `''` is the negated default, `_` is the always-on floor. If a map contains only `_` and `''` (no other states), the `''` default is redundant — Tasty keeps the `_` value and drops `''` with a `REDUNDANT_DEFAULT_STATE` warning.
72
+
73
+ ### Sub-element
74
+
75
+ Element styled using a capitalized key. Identified by `data-element` attribute:
76
+
77
+ ```jsx
78
+ styles: {
79
+ Title: {
80
+ preset: 'h3';
81
+ }
82
+ }
83
+ // Targets: <div data-element="Title">
84
+ ```
85
+
86
+ #### Selector Affix (`$`)
87
+
88
+ Control how a sub-element selector attaches to the root selector using the `$` property inside the sub-element's styles.
89
+
90
+ Examples below assume the sub-element key is **Cell** (i.e. `[data-element="Cell"]` in CSS):
91
+
92
+ | Pattern | Result | Description |
93
+ | ----------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
94
+ | _(none)_ | `[data-element="Cell"]` | Descendant (default) |
95
+ | `>` | `> [data-element="Cell"]` | Direct child |
96
+ | `>Body>` | `> [data-element="Body"] > [data-element="Cell"]` | Chained elements |
97
+ | `> Cell` | `> [data-element="Cell"]` | Self-name shorthand — when the trailing element name matches the sub-element's own key, it acts as the placeholder (same as `@`); no duplication |
98
+ | `h1` | `h1` | Tag selector (no key injection) |
99
+ | `h1 >` | `h1 > [data-element="Cell"]` | Key is direct child of tag |
100
+ | `h1 *` | `h1 *` | Any descendant of tag |
101
+ | `*` | `*` | All descendants |
102
+ | `&::before` | `::before` | Root pseudo (no key); `&` is required |
103
+ | `@::before` | `[data-element="Cell"]::before` | Pseudo on the sub-element |
104
+ | `>@.active` | `> [data-element="Cell"].active` | Class on the sub-element |
105
+
106
+ Rules for key injection (`[data-element="..."]`):
107
+
108
+ - **Trailing combinator** (`>`, `+`, `~`) — key is injected after it
109
+ - **Uppercase element name** (`Body`, `Row`) — key is injected as descendant
110
+ - **HTML tag** (`h1`, `a`, `span`) — no key injection; the tag IS the selector
111
+ - **Universal selector** (`*`) — no key injection
112
+ - **Pseudo / class / attribute** — no key injection
113
+
114
+ The `@` placeholder marks exactly where `[data-element="..."]` is injected, allowing you to attach pseudo-classes, pseudo-elements, or class selectors directly to the sub-element instead of the root:
115
+
116
+ ```jsx
117
+ const List = tasty({
118
+ styles: {
119
+ Item: {
120
+ $: '>@:last-child',
121
+ border: 'none',
122
+ },
123
+ },
124
+ });
125
+ // → .t0 > [data-element="Item"]:last-child { border: none }
126
+ ```
127
+
128
+ ### Color Token
129
+
130
+ Named color prefixed with `#` that maps to CSS custom properties. Supports opacity with `.N` suffix:
131
+
132
+ ```jsx
133
+ fill: '#purple.5'; // → oklch(from var(--purple-color) l c h / .5)
134
+ ```
135
+
136
+ ### Modifier
137
+
138
+ State value via `mods` prop that generates `data-*` attributes:
139
+
140
+ ```jsx
141
+ mods={{ hovered: true, theme: 'danger' }}
142
+ // → data-hovered="" data-theme="danger"
143
+ ```
144
+
145
+ Modifiers can also be exposed as top-level component props via `modProps` — see [React API — Mod Props](react-api.md#mod-props).
146
+
147
+ ---
148
+
149
+ ## Color Tokens & Opacity
150
+
151
+ ```jsx
152
+ color: '#purple', // Full opacity
153
+ color: '#purple.5', // 50% opacity
154
+ color: '#purple.05', // 5% opacity
155
+ color: '#purple.$fade', // Opacity from a custom property
156
+ fill: '#current', // → currentcolor
157
+ color: '(#primary, #secondary)', // Fallback syntax
158
+ ```
159
+
160
+ The suffix sets the alpha on the token's color with CSS relative color syntax:
161
+
162
+ ```jsx
163
+ fill: '#purple.5';
164
+ // → oklch(from var(--purple-color) l c h / .5)
165
+ ```
166
+
167
+ The channels are copied over and the alpha slot is written, which asks nothing of
168
+ the color beyond _being_ a color. That means the suffix works on every one of
169
+ these:
170
+
171
+ - a token holding a `color-mix()`, a `light-dark()`, or a `color()` in a space
172
+ Tasty cannot convert — none of which have channels to decompose
173
+ - `#current`, which resolves to the color the element inherits
174
+ - a `--name-color` variable declared in your own CSS, with no Tasty token
175
+ definition behind it
176
+
177
+ Two properties follow from writing the alpha slot rather than compositing:
178
+
179
+ - **Alpha is replaced, not multiplied.** A token holding `rgb(255 0 0 / .8)`
180
+ faded to `.5` is alpha `.5`, not `.4`.
181
+ - **The alpha may be a number or a percentage.** `#purple.$fade` emits
182
+ `/ var(--fade)` unchanged, so it works whether `$fade` holds `.5` or `50%` —
183
+ which is what `--*-opacity` properties are registered to accept.
184
+
185
+ ### `#current` is the keyword
186
+
187
+ `#current` emits the `currentcolor` keyword, so it resolves against the element
188
+ that reads it. That is what lets it follow a color an ancestor faded — a ramp
189
+ that expresses its disabled state once, in `color`, fades everything painted from
190
+ `#current` below it with no further declarations.
191
+
192
+ The `color` style also publishes `--current-color` beside every color it sets,
193
+ for anything that needs the inherited color _as a color_ rather than as the
194
+ keyword — hand-authored CSS, or a place the keyword will not do. Read it as
195
+ `$current-color`. It is registered with `initial-value: currentcolor`, so where
196
+ nothing published it, it still resolves against each element's own color:
197
+
198
+ ```jsx
199
+ fill: '$current-color'; // → var(--current-color)
200
+ ```
201
+
202
+ A color that already reads the color it inherits — `#current`, a `#current` fade
203
+ — is not published into it: resolving such a value a second time at a descendant
204
+ would fade it twice.
205
+
206
+ One case needs the wider floor to be considered. A token _defined_ as `#current`
207
+ and then faded gives relative color syntax a `currentcolor` origin, which Safari
208
+ supports only from 18:
209
+
210
+ ```jsx
211
+ { '#ink': '#current', fill: '#ink.5' }
212
+ // --ink-color: currentcolor
213
+ // fill: oklch(from currentcolor l c h / .5) → Chrome, Firefox, Safari 18+
214
+ ```
215
+
216
+ Put the fade in the token instead — `{ '#ink': '#current.5' }` — and it goes
217
+ through `color-mix()`, which works from Safari 16.2 and composes like every other
218
+ `#current` fade.
219
+
220
+ ### `#current` composes instead
221
+
222
+ `#current` is the one exception, and the difference is deliberate. A token _names_
223
+ a color, so fading it sets its alpha. `currentcolor` is the color an element
224
+ **inherits**, which an ancestor may already have faded — `#current.4` means "40%
225
+ of what reaches me":
226
+
227
+ ```jsx
228
+ fill: '#current.4';
229
+ // → color-mix(in oklab, currentcolor 40%, transparent)
230
+ ```
231
+
232
+ So a `#current` fade nested inside another one composes: a label at `#current.4`
233
+ with a fill of `#current.18` under it lands at `.072`. Color ramps built on
234
+ `#current` depend on that — replacing would double the opacity of every nested
235
+ step. Because a `color-mix()` percentage cannot be a `<number>`, an opacity
236
+ custom property used as `#current.$fade` must hold a unitless number; a token
237
+ accepts either form.
238
+
239
+ The space is always `oklch`: it is unbounded, so a wide-gamut color survives a
240
+ round trip that a gamut-limited space would clamp. Nothing configures this —
241
+ see [Color space](configuration.md#color-space).
242
+
243
+ ---
244
+
245
+ ## CSS Color Functions
246
+
247
+ Every CSS color function is recognized as a color, so it lands in the color slot
248
+ of whichever style property you use it in — and tokens inside it are expanded:
249
+
250
+ ```jsx
251
+ fill: 'color-mix(in oklab, #primary 50%, #surface)',
252
+ color: 'light-dark(#dark, #light)',
253
+ color: 'contrast-color(#primary)',
254
+ fill: 'color(display-p3 1 .5 0)',
255
+ border: '1bw solid oklch(from #primary l c h / 50%)', // relative color syntax
256
+ shadow: '0 0 1x color-mix(in oklab, #dark 20%, transparent)',
257
+ ```
258
+
259
+ `light-dark()` is the one exception to "always a color": CSS lets it pick between
260
+ values of any type, so it is treated as a color only when its arguments are
261
+ colors. `padding: 'light-dark(1x, 2x)'` still reaches the padding slot.
262
+
263
+ A color token defined as one of these functions takes the
264
+ [opacity suffix](#color-tokens--opacity) like any other:
265
+
266
+ ```jsx
267
+ const Card = tasty({
268
+ styles: {
269
+ '#brand': 'color-mix(in oklab, #primary 50%, #surface)',
270
+ fill: '#brand.5',
271
+ },
272
+ });
273
+ ```
274
+
275
+ A [replace token](configuration.md#replace-tokens-parse-time-substitution) is
276
+ substituted while parsing, so its color is right there to fade in place. A
277
+ channel function takes the alpha after a slash; a derived function has no alpha
278
+ channel, so it gets wrapped:
279
+
280
+ ```jsx
281
+ configure({
282
+ replaceTokens: {
283
+ '#solid': 'hsl(220 90% 50%)',
284
+ '#adaptive': 'light-dark(#dark, #light)',
285
+ },
286
+ });
287
+
288
+ fill: '#solid.5'; // → hsl(220 90% 50% / .5)
289
+ fill: '#adaptive.5'; // → oklch(from light-dark(…) l c h / .5)
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Built-in Units
295
+
296
+ | Unit | Description | Example | CSS Output |
297
+ | ---- | ------------------ | ------- | ------------------------------- |
298
+ | `x` | Gap multiplier | `2x` | `calc(var(--gap) * 2)` |
299
+ | `r` | Border radius | `1r` | `var(--radius)` |
300
+ | `cr` | Card border radius | `1cr` | `var(--card-radius)` |
301
+ | `bw` | Border width | `2bw` | `calc(var(--border-width) * 2)` |
302
+ | `ow` | Outline width | `1ow` | `var(--outline-width)` |
303
+ | `sf` | Stable fraction | `1sf` | `minmax(0, 1fr)` |
304
+
305
+ You can register additional custom units via [`configure()`](configuration.md#options).
306
+
307
+ ---
308
+
309
+ ## Replace Tokens
310
+
311
+ Tokens defined via [`configure({ replaceTokens })`](configuration.md#replace-tokens-parse-time-substitution) are replaced at parse time and baked into the generated CSS:
312
+
313
+ ```jsx
314
+ const Card = tasty({
315
+ styles: {
316
+ padding: '$card-padding',
317
+ fill: '#surface',
318
+ border: '1bw solid #accent',
319
+ },
320
+ });
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Recipes
326
+
327
+ Apply predefined style bundles (defined via [`configure({ recipes })`](configuration.md#recipes)) using the `recipe` style property:
328
+
329
+ ```jsx
330
+ const Card = tasty({
331
+ styles: {
332
+ recipe: 'card',
333
+ color: '#text',
334
+ },
335
+ });
336
+
337
+ // Compose multiple recipes
338
+ const ElevatedCard = tasty({
339
+ styles: {
340
+ recipe: 'card elevated',
341
+ color: '#text',
342
+ },
343
+ });
344
+ ```
345
+
346
+ **Where recipes resolve:**
347
+
348
+ The `recipe` property is resolved on the component root and independently inside each sub-element's styles:
349
+
350
+ ```jsx
351
+ const CardWithPanel = tasty({
352
+ styles: {
353
+ recipe: 'card',
354
+ Panel: {
355
+ recipe: 'card elevated',
356
+ padding: '2x', // Overrides the panel's base recipe padding
357
+ },
358
+ },
359
+ });
360
+ ```
361
+
362
+ At each level, base recipes are collected in name order (later recipes replace earlier values for the same property), then local styles are merged, then post recipes are merged. The merges with local styles and post recipes follow the [state-map extension and replacement rules](#extending-vs-replacing-state-maps).
363
+
364
+ Recipe definitions themselves cannot contain a `recipe` reference to another recipe: there is no recursive expansion or recipe inheritance. Compose names in the root or sub-element `recipe` property instead. See [Configuration — Recipes](configuration.md#recipes) for an unsupported nesting example.
365
+
366
+ **Post-merge recipes (`/` separator):**
367
+
368
+ Recipes listed after `/` are applied _after_ component styles using `mergeStyles`:
369
+
370
+ ```jsx
371
+ const Input = tasty({
372
+ styles: {
373
+ recipe: 'reset input / input-autofill',
374
+ preset: 't3',
375
+ },
376
+ });
377
+ ```
378
+
379
+ Use `none` to skip base recipes and apply only post recipes:
380
+
381
+ ```jsx
382
+ const Custom = tasty({
383
+ styles: {
384
+ recipe: 'none / disabled',
385
+ padding: '2x',
386
+ },
387
+ });
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Extending vs. Replacing State Maps
393
+
394
+ When a style property uses a state map, the merge behavior depends on whether the child provides a `''` (default) key:
395
+
396
+ - **No `''` key** — extend mode: parent states are preserved, child adds/overrides
397
+ - **Has `''` key** — replace mode: child defines everything from scratch
398
+
399
+ ```jsx
400
+ // Parent has: fill: { '': '#white', hovered: '#blue', disabled: '#gray' }
401
+
402
+ // Extend — no '' key, parent states preserved
403
+ const MyButton = tasty(Button, {
404
+ styles: {
405
+ fill: {
406
+ loading: '#yellow', // append new state
407
+ disabled: '#gray.20', // override existing state in place
408
+ },
409
+ },
410
+ });
411
+
412
+ // Replace — has '' key, parent states dropped
413
+ const MyButton = tasty(Button, {
414
+ styles: {
415
+ fill: {
416
+ '': '#red',
417
+ hovered: '#blue',
418
+ },
419
+ },
420
+ });
421
+ ```
422
+
423
+ Use `'@inherit'` to pull a parent state value. In extend mode it repositions the state; in replace mode it cherry-picks it:
424
+
425
+ ```jsx
426
+ // Extend mode: reposition disabled to end (highest CSS priority)
427
+ fill: {
428
+ 'loading': '#yellow',
429
+ disabled: '@inherit',
430
+ }
431
+
432
+ // Replace mode: cherry-pick disabled from parent
433
+ fill: {
434
+ '': '#red',
435
+ disabled: '@inherit',
436
+ }
437
+ ```
438
+
439
+ Use `null` inside a state map to remove a state, or `false` to block it entirely (tombstone):
440
+
441
+ ```jsx
442
+ fill: {
443
+ pressed: null;
444
+ } // removes pressed from the result
445
+ fill: {
446
+ disabled: false;
447
+ } // tombstone — no CSS for disabled, blocks recipe too
448
+ ```
449
+
450
+ ### Resetting Properties with `null` and `false`
451
+
452
+ ```jsx
453
+ const SimpleButton = tasty(Button, {
454
+ styles: {
455
+ fill: null, // discard parent's fill, let recipe fill in
456
+ border: false, // no border at all (tombstone — blocks recipe too)
457
+ },
458
+ });
459
+ ```
460
+
461
+ | Value | Meaning | Recipe fills in? |
462
+ | ----------- | ------------------------------------ | ---------------- |
463
+ | `undefined` | Not provided — parent preserved | N/A |
464
+ | `null` | Intentional unset — parent discarded | Yes |
465
+ | `false` | Tombstone — blocks everything | No |
466
+
467
+ ---
468
+
469
+ ## Advanced States (`@` prefix)
470
+
471
+ | Prefix | Purpose | Example |
472
+ | ----------- | ----------------------------------------- | -------------------------- |
473
+ | `@media` | Media queries | `@media(w < 768px)` |
474
+ | `@(...)` | Container queries | `@(panel, w >= 300px)` |
475
+ | `@supports` | Feature/selector support | `@supports(display: grid)` |
476
+ | `@root` | Root element states | `@root(schema=dark)` |
477
+ | `@parent` | Parent/ancestor element states | `@parent(hovered)` |
478
+ | `@own` | Sub-element's own state | `@own(hovered)` |
479
+ | `@starting` | Entry animation | `@starting` |
480
+ | `_` | Fallback floor (always-on, never negated) | `_` |
481
+ | `:is()` | CSS `:is()` structural pseudo-class | `:is(fieldset > label)` |
482
+ | `:has()` | CSS `:has()` relational pseudo-class | `:has(> Icon)` |
483
+ | `:not()` | CSS `:not()` negation (prefer `!:is()`) | `:not(:first-child)` |
484
+ | `:where()` | CSS `:where()` (zero specificity) | `:where(Section)` |
485
+
486
+ > **Specificity.** All state selectors Tasty generates (modifiers, pseudo-classes,
487
+ > `:is()`/`:not()` groups, and `@root` / `@parent` context) are wrapped in
488
+ > `:where(...)` so they carry **zero specificity**. The only specificity anchors
489
+ > are the doubled component class (`.t0.t0`) and sub-element `[data-element]`
490
+ > attributes. This means overlapping rules (e.g. a `_` fallback floor and the
491
+ > states layered over it) resolve purely by **source order** — Tasty emits
492
+ > lower-priority states first and higher-priority states last so the cascade
493
+ > produces the intended winner.
494
+
495
+ ### `@media(...)` — Media Queries
496
+
497
+ Media queries support dimension shorthands and custom unit expansion:
498
+
499
+ | Shorthand | Expands to |
500
+ | --------- | ---------- |
501
+ | `w` | `width` |
502
+ | `h` | `height` |
503
+
504
+ ```jsx
505
+ fill: {
506
+ '': '#surface',
507
+ '@media(w < 768px)': '#surface-mobile',
508
+ '@media(600px <= w < 1200px)': '#surface-tablet',
509
+ '@media(prefers-color-scheme: dark)': '#surface-dark',
510
+ }
511
+ ```
512
+
513
+ | Tasty syntax | CSS output |
514
+ | ------------------------------------ | ------------------------------------- |
515
+ | `@media(w < 768px)` | `@media (width < 768px)` |
516
+ | `@media(600px <= w < 1200px)` | `@media (600px <= width < 1200px)` |
517
+ | `@media:print` | `@media print` |
518
+ | `@media:screen` | `@media screen` |
519
+ | `@media(prefers-color-scheme: dark)` | `@media (prefers-color-scheme: dark)` |
520
+ | `@media(prefers-reduced-motion)` | `@media (prefers-reduced-motion)` |
521
+
522
+ Custom units work inside media queries: `@media(w < 40x)` → `@media (width < calc(var(--gap) * 40))`.
523
+
524
+ In practice, define state aliases via `configure({ states })` and use `@mobile` instead of writing the full query in every component.
525
+
526
+ ### `@(...)` — Container Queries
527
+
528
+ Container queries use the syntax `@(name, condition)` for named containers or `@(condition)` for the nearest ancestor container. Dimension shorthands (`w`, `h`, `is`, `bs`) are expanded the same way as `@media`.
529
+
530
+ | Shorthand | Expands to |
531
+ | --------- | ------------- |
532
+ | `w` | `width` |
533
+ | `h` | `height` |
534
+ | `is` | `inline-size` |
535
+ | `bs` | `block-size` |
536
+
537
+ ```jsx
538
+ const Panel = tasty({
539
+ styles: {
540
+ flow: {
541
+ '': 'column',
542
+ '@(layout, w >= 600px)': 'row',
543
+ },
544
+ },
545
+ });
546
+ ```
547
+
548
+ | Tasty syntax | CSS output |
549
+ | ---------------------------------- | ---------------------------------------------- |
550
+ | `@(layout, w < 600px)` | `@container layout (width < 600px)` |
551
+ | `@(w < 600px)` | `@container (width < 600px)` |
552
+ | `@(layout, $variant=danger)` | `@container layout style(--variant: "danger")` |
553
+ | `@(layout, $compact)` | `@container layout style(--compact)` |
554
+ | `@(scroll-state(stuck: top))` | `@container scroll-state(stuck: top)` |
555
+ | `@(nav, scroll-state(stuck: top))` | `@container nav scroll-state(stuck: top)` |
556
+
557
+ Container style queries use `$prop` (boolean) or `$prop=value` syntax, which maps to CSS `style(--prop)` or `style(--prop: "value")`.
558
+
559
+ ### `@supports(...)` — Feature Queries
560
+
561
+ Feature queries test CSS property support and accept CSS support functions such
562
+ as `at-rule()`. Use `$` as the first argument to test selector support:
563
+
564
+ | Tasty syntax | CSS output |
565
+ | ---------------------------- | ---------------------------------- |
566
+ | `@supports(display: grid)` | `@supports (display: grid)` |
567
+ | `@supports($, :has(*))` | `@supports selector(:has(*))` |
568
+ | `@supports(at-rule(@scope))` | `@supports (at-rule(@scope))` |
569
+ | `!@supports(display: grid)` | `@supports (not (display: grid))` |
570
+ | `!@supports(at-rule(@scope))` | `@supports (not (at-rule(@scope)))` |
571
+
572
+ ```jsx
573
+ display: {
574
+ '': 'flex',
575
+ '@supports(display: grid)': 'grid',
576
+ }
577
+ ```
578
+
579
+ `at-rule()` tests whether the browser supports the named at-rule. Its surrounding
580
+ parentheses in Tasty's CSS output are valid CSS. Negation and combinations with
581
+ other states use the same syntax as property queries:
582
+
583
+ ```jsx
584
+ color: {
585
+ _: 'red',
586
+ '@supports(at-rule(@scope)) & @supports(display: grid)': 'blue',
587
+ }
588
+ ```
589
+
590
+ Use `_` to keep a base value when a browser cannot evaluate a query. A support
591
+ query only detects a browser feature; it does not add an API for authoring the
592
+ queried at-rule in a Tasty style object. For conditional stylesheet imports such
593
+ as `@import url("scoped.css") supports(at-rule(@scope));`, use an ordinary CSS
594
+ stylesheet or document-level `useRawCSS()`. Put imports before other rules in
595
+ that stylesheet. Imports cannot be used in constructable stylesheets, including
596
+ Tasty's adopted-sheet mode for Shadow DOM.
597
+
598
+ ### `_` — Fallback Floor
599
+
600
+ By default Tasty makes states **mutually exclusive**: a higher-priority state
601
+ turns the lower-priority ones off via negation, so exactly one branch applies.
602
+ This relies on `A | !A` always being true. CSS feature/container queries break
603
+ that assumption: `@supports(...)` and `@(...)` queries can be **unknown** (not
604
+ just true/false), and `not(unknown)` is also unknown — so a negated default
605
+ branch silently never applies. The classic case is `scroll-state`: a browser can
606
+ support `container-type: scroll-state` while a specific `scroll-state(...)` query
607
+ is unknown, leaving _no_ branch active.
608
+
609
+ The `_` fallback floor solves this. Use `_` as a **standalone key** and its value
610
+ **always applies** as a guaranteed floor: it never receives negation, and
611
+ higher-priority states simply layer over it via the cascade.
612
+
613
+ ```jsx
614
+ inset: {
615
+ _: '0 top',
616
+ '@supports(container-type: scroll-state) & @(scroll-state(scrolled: block-end))':
617
+ '-80x top',
618
+ }
619
+ ```
620
+
621
+ ```css
622
+ .t0.t0 {
623
+ inset: 0...;
624
+ }
625
+ @container scroll-state(scrolled: block-end) {
626
+ @supports (container-type: scroll-state) {
627
+ .t0.t0 {
628
+ inset: -80px...;
629
+ }
630
+ }
631
+ }
632
+ ```
633
+
634
+ The floor is emitted as a bare rule (no negated `@supports` / container branches),
635
+ so it always applies. When the override matches it wins because it is emitted
636
+ later and all state selectors share the same specificity (see the note on
637
+ `:where()` below).
638
+
639
+ Notes:
640
+
641
+ - `_` is a **standalone key only** — it defines a single map-wide floor and
642
+ cannot be combined with state logic. Keys like `_ & hovered` or `_ | focused`
643
+ are ignored with an `INVALID_FALLBACK_KEY` dev warning.
644
+ - `_` can coexist with the bare `''` default when other states exist: `''` is the
645
+ negated default, `_` is the always-on floor. With only `_` and `''` (no other
646
+ states) the `''` default is redundant — Tasty keeps the `_` value and drops
647
+ `''` with a `REDUNDANT_DEFAULT_STATE` warning.
648
+ - `_` is position-independent: it is always placed at the lowest priority
649
+ regardless of where it appears in the map.
650
+
651
+ #### `_` vs the `''` default
652
+
653
+ In simple maps where every other state is a plain modifier (always cleanly
654
+ on/off), `_` and `''` produce the **same visible result** — the base value shows
655
+ whenever no other state wins. They diverge in three situations, and the root
656
+ cause is always the same: **`''` is mutually exclusive (turned off by negation),
657
+ while `_` is always on (layered underneath).**
658
+
659
+ - **Unknown / invalid branches.** If a higher-priority branch sits behind a query
660
+ that can be _unknown_ (`@supports`, container, `scroll-state`) or uses a
661
+ selector the browser drops, the negated `''` default disappears along with it
662
+ (`not(unknown) = unknown`), leaving the property with no rule. The `_` floor
663
+ survives because it is an unconditional bare rule. This is the case `_` exists
664
+ for.
665
+
666
+ - **Empty / "unset" states.** A state can intentionally produce _no_ output (an
667
+ empty value) to leave a property unset while that state is active. With `''`
668
+ this works — the default is negated away and nothing replaces it, so the
669
+ property unsets. With `_` the floor can never be turned off, so its value keeps
670
+ applying and the "unset on this state" intent is lost.
671
+
672
+ ```jsx
673
+ // '' : color unsets while loading. '_' : color stays #text while loading.
674
+ color: { '': '#text', loading: '' }
675
+ ```
676
+
677
+ - **States with different output shapes.** Because `_` layers additively, when
678
+ different states emit _different sets_ of declarations, the floor's
679
+ declarations bleed through wherever the winning state does not override the
680
+ same property — which can produce inconsistent combinations. A `''` default is
681
+ swapped out cleanly by its mutually-exclusive condition.
682
+
683
+ Rule of thumb: **use `''` for the normal default** (clean mutual exclusivity,
684
+ supports unsetting), and reach for `_` only when a value must survive an
685
+ _unknown_ higher-priority branch.
686
+
687
+ ### `@root(...)` — Root Element States
688
+
689
+ Root states generate selectors on the `:root` element. They are useful for theme modes, feature flags, and other page-level conditions:
690
+
691
+ These docs use `data-schema` in examples. If your app standardizes on a different root attribute, keep the same pattern and swap the attribute name consistently in your aliases and selectors.
692
+
693
+ ```jsx
694
+ color: {
695
+ '': '#text',
696
+ '@root(schema=dark)': '#text-on-dark',
697
+ '@root(.premium-user)': '#gold',
698
+ }
699
+ ```
700
+
701
+ | Tasty syntax | CSS selector |
702
+ | ---------------------- | --------------------------------- |
703
+ | `@root(schema=dark)` | `:root[data-schema="dark"]` |
704
+ | `@root(hovered)` | `:root[data-hovered]` |
705
+ | `@root(.premium-user)` | `:root.premium-user` |
706
+ | `@root([lang="en"])` | `:root[lang="en"]` |
707
+ | `!@root(schema=dark)` | `:root:not([data-schema="dark"])` |
708
+
709
+ Root conditions are prepended to the component selector: `:root[data-schema="dark"] .t0.t0 { ... }`.
710
+
711
+ ### `@own(...)` — Sub-element's Own State
712
+
713
+ By default, state keys in sub-element styles refer to the root component's state context. Use `@own(...)` when the sub-element should react to its own state:
714
+
715
+ ```jsx
716
+ const Nav = tasty({
717
+ styles: {
718
+ NavItem: {
719
+ color: {
720
+ '': '#text',
721
+ '@own(:hover)': '#primary',
722
+ '@own(:focus-visible)': '#primary',
723
+ selected: '#primary', // root-level modifier
724
+ },
725
+ },
726
+ },
727
+ elements: { NavItem: 'a' },
728
+ });
729
+ ```
730
+
731
+ | Tasty syntax (inside sub-element) | CSS output |
732
+ | --------------------------------- | ------------------------------------------------- |
733
+ | `@own(:hover)` | `:hover` on the sub-element selector |
734
+ | `@own(hovered)` | `[data-hovered]` on the sub-element selector |
735
+ | `@own(theme=dark)` | `[data-theme="dark"]` on the sub-element selector |
736
+
737
+ `@own()` is only valid inside sub-element styles. Using it on root styles emits a warning and is treated as a regular modifier.
738
+
739
+ ### `@starting` — Entry Animation
740
+
741
+ Wraps the rule in `@starting-style`, enabling CSS entry animations for elements as they appear in the DOM:
742
+
743
+ ```jsx
744
+ const FadeIn = tasty({
745
+ styles: {
746
+ opacity: { '': '1', '@starting': '0' },
747
+ transform: { '': 'scale(1)', '@starting': 'scale(0.95)' },
748
+ transition: 'opacity, translate',
749
+ },
750
+ });
751
+ ```
752
+
753
+ | Tasty syntax | CSS output |
754
+ | ------------ | ------------------------------------ |
755
+ | `@starting` | `@starting-style { .t0.t0 { ... } }` |
756
+
757
+ ### `@parent(...)` — Parent Element States
758
+
759
+ Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
760
+
761
+ ```jsx
762
+ const Highlight = tasty({
763
+ styles: {
764
+ fill: {
765
+ '': '#white',
766
+ '@parent(hovered)': '#gray.05', // Any ancestor has [data-hovered]
767
+ '@parent(theme=dark, >)': '#dark-02', // Direct parent has [data-theme="dark"]
768
+ },
769
+ },
770
+ });
771
+ ```
772
+
773
+ | Syntax | CSS Output |
774
+ | ------------------------------------- | -------------------------------------------------------------------- |
775
+ | `@parent(hovered)` | `:is([data-hovered] *)` |
776
+ | `!@parent(hovered)` | `:not([data-hovered] *)` |
777
+ | `@parent(hovered, >)` | `:is([data-hovered] > *)` (direct parent) |
778
+ | `@parent(.active)` | `:is(.active *)` |
779
+ | `@parent(hovered & focused)` | `:is([data-hovered][data-focused] *)` (same ancestor) |
780
+ | `@parent(hovered) & @parent(focused)` | `:is([data-hovered] *):is([data-focused] *)` (independent ancestors) |
781
+ | `@parent(hovered \| focused)` | `:is([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
782
+
783
+ For sub-elements, the parent check applies to the root element's ancestors:
784
+
785
+ ```jsx
786
+ const Card = tasty({
787
+ styles: {
788
+ Label: {
789
+ color: {
790
+ '': '#text',
791
+ '@parent(hovered)': '#primary',
792
+ },
793
+ },
794
+ },
795
+ });
796
+ // → .t0.t0:is([data-hovered] *) [data-element="Label"]
797
+ ```
798
+
799
+ ### `:is()`, `:has()` — CSS Structural Pseudo-classes
800
+
801
+ Use CSS structural pseudo-classes directly in state keys. Capitalized words become `[data-element="..."]` selectors; lowercase words are HTML tags. A trailing combinator (`>`, `+`, `~`) is auto-completed with `*`.
802
+
803
+ `:where()` and `:not()` are also supported but rarely needed — use `:is()` and `!` negation instead.
804
+
805
+ > **Performance warning:** CSS structural pseudo-classes — especially `:has()` — can be costly for the browser to evaluate because they require inspecting the DOM tree beyond the matched element. Tasty already provides a rich, purpose-built state system (`@parent()`, `@own()`, modifiers, boolean logic) that covers the vast majority of use cases without the performance trade-off. **Prefer Tasty's built-in mechanisms and treat `:has()` / `:is()` as a last resort** for conditions that cannot be expressed any other way.
806
+
807
+ ```jsx
808
+ const Card = tasty({
809
+ styles: {
810
+ display: {
811
+ '': 'block',
812
+ ':has(> Icon)': 'grid', // has Icon as direct child
813
+ ':has(+ Icon)': 'grid', // immediately followed by an Icon sibling
814
+ ':has(~ Icon)': 'grid', // has an Icon sibling somewhere after
815
+ ':has(Icon +)': 'grid', // immediately preceded by an Icon sibling (auto-completes to `Icon + *`)
816
+ ':has(Icon ~)': 'grid', // has an Icon sibling somewhere before (auto-completes to `Icon ~ *`)
817
+ ':is(fieldset > label)': 'inline', // is a label inside a fieldset (HTML tags)
818
+ '!:has(> Icon)': 'flex', // negation: no Icon child
819
+ },
820
+ },
821
+ });
822
+ ```
823
+
824
+ | Syntax | CSS Output | Meaning |
825
+ | ---------------------- | --------------------------------------- | --------------------------------------- |
826
+ | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
827
+ | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
828
+ | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
829
+ | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
830
+ | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
831
+ | `:has(>)` | `:has(> *)` | Has any direct child |
832
+ | `:is(> Field + input)` | `:is(> [data-element="Field"] + input)` | Structural match |
833
+ | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
834
+ | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
835
+ | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
836
+
837
+ Combine with other states using boolean logic (`&`, `|`, `!`, `^`):
838
+
839
+ ```jsx
840
+ ':has(> Icon) & hovered'; // AND: structural + data attribute
841
+ '@parent(hovered) & :has(> Icon)'; // AND: parent check + structural
842
+ ':has(> Icon) | :has(> Button)'; // OR: either sub-element present
843
+ ':has(> Icon) ^ :has(> Button)'; // XOR: exactly one present
844
+ ```
845
+
846
+ > **Nesting limit:** The state key parser supports up to 2 levels of nested parentheses inside `:is()`, `:has()`, `:not()`, and `:where()` — e.g. `:has(Input:not(:disabled))` works, but 3+ levels like `:has(:is(:not(:hover)))` will not be tokenized correctly. This covers virtually all practical use cases.
847
+
848
+ ---
849
+
850
+ ## Keyframes
851
+
852
+ Define animations inline using the `@keyframes` key in styles:
853
+
854
+ ```jsx
855
+ const Pulse = tasty({
856
+ styles: {
857
+ animation: 'pulse 2s infinite',
858
+ '@keyframes': {
859
+ pulse: {
860
+ '0%, 100%': { transform: 'scale(1)' },
861
+ '50%': { transform: 'scale(1.05)' },
862
+ },
863
+ },
864
+ },
865
+ });
866
+ ```
867
+
868
+ ---
869
+
870
+ ## Properties (`@property`)
871
+
872
+ CSS cannot transition or animate custom properties unless the browser knows their type. Tasty solves this automatically — when you assign a concrete value to a custom property, the type is inferred and a CSS `@property` rule is registered behind the scenes:
873
+
874
+ ```jsx
875
+ const AnimatedGradient = tasty({
876
+ styles: {
877
+ '$gradient-angle': '0deg',
878
+ '#theme': 'okhst(280 80% 50%)',
879
+ image: 'linear-gradient($gradient-angle, #theme, #clear)',
880
+ transition: '$$gradient-angle, ##theme',
881
+ },
882
+ });
883
+ ```
884
+
885
+ Here `$gradient-angle: '0deg'` is detected as `<angle>` and `#theme` as `<color>` (via the `#name` naming convention), so both transitions work without any manual `@property` declarations. Numeric types (`<number>`, `<length>`, `<percentage>`, `<angle>`, `<time>`) are inferred from values; `<color>` is inferred from `#name` tokens.
886
+
887
+ Use explicit `@property` when you need non-default settings like `inherits: false`:
888
+
889
+ ```jsx
890
+ '@property': {
891
+ '$gradient-angle': { syntax: '<angle>', inherits: false, initialValue: '0deg' },
892
+ },
893
+ ```
894
+
895
+ ---
896
+
897
+ ## Font Face (`@font-face`)
898
+
899
+ Register custom fonts directly inside a `styles` object. Keys are font-family names, values are descriptor objects (or arrays of them for multiple weights/styles).
900
+
901
+ ```ts
902
+ const Heading = tasty({
903
+ styles: {
904
+ '@font-face': {
905
+ 'Brand Sans': {
906
+ src: 'url("/fonts/brand-sans.woff2") format("woff2")',
907
+ fontDisplay: 'swap',
908
+ },
909
+ },
910
+ font: '"Brand Sans", sans-serif',
911
+ },
912
+ });
913
+ ```
914
+
915
+ ### Multiple weights
916
+
917
+ Supply an array to register several variants of the same family:
918
+
919
+ ```ts
920
+ '@font-face': {
921
+ 'Brand Sans': [
922
+ { src: 'url("/fonts/brand-regular.woff2") format("woff2")', fontWeight: 400, fontDisplay: 'swap' },
923
+ { src: 'url("/fonts/brand-bold.woff2") format("woff2")', fontWeight: 700, fontDisplay: 'swap' },
924
+ ],
925
+ }
926
+ ```
927
+
928
+ ### Supported descriptors
929
+
930
+ | Descriptor | CSS property | Type |
931
+ | ----------------------- | ------------------------- | --------------------------------------------------------- |
932
+ | `src` (required) | `src` | `string` |
933
+ | `fontWeight` | `font-weight` | `string \| number` |
934
+ | `fontStyle` | `font-style` | `string` |
935
+ | `fontStretch` | `font-stretch` | `string` |
936
+ | `fontDisplay` | `font-display` | `'auto' \| 'block' \| 'swap' \| 'fallback' \| 'optional'` |
937
+ | `unicodeRange` | `unicode-range` | `string` |
938
+ | `ascentOverride` | `ascent-override` | `string` |
939
+ | `descentOverride` | `descent-override` | `string` |
940
+ | `lineGapOverride` | `line-gap-override` | `string` |
941
+ | `sizeAdjust` | `size-adjust` | `string` |
942
+ | `fontFeatureSettings` | `font-feature-settings` | `string` |
943
+ | `fontVariationSettings` | `font-variation-settings` | `string` |
944
+
945
+ > Font-face rules are permanent — they are injected once and never cleaned up, matching how browsers handle `@font-face`.
946
+
947
+ ---
948
+
949
+ ## Counter Style (`@counter-style`)
950
+
951
+ Define custom list markers via the CSS `@counter-style` at-rule. Keys are counter-style names, values are descriptor objects.
952
+
953
+ ```ts
954
+ const EmojiList = tasty({
955
+ tag: 'ol',
956
+ styles: {
957
+ '@counter-style': {
958
+ thumbs: {
959
+ system: 'cyclic',
960
+ symbols: '"👍"',
961
+ suffix: '" "',
962
+ },
963
+ },
964
+ listStyleType: 'thumbs',
965
+ },
966
+ });
967
+ ```
968
+
969
+ ### Supported descriptors
970
+
971
+ | Descriptor | CSS property | Type |
972
+ | ------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
973
+ | `system` (required) | `system` | `'cyclic' \| 'numeric' \| 'alphabetic' \| 'symbolic' \| 'additive' \| 'fixed' \| string` |
974
+ | `symbols` | `symbols` | `string` |
975
+ | `additiveSymbols` | `additive-symbols` | `string` |
976
+ | `prefix` | `prefix` | `string` |
977
+ | `suffix` | `suffix` | `string` |
978
+ | `negative` | `negative` | `string` |
979
+ | `range` | `range` | `string` |
980
+ | `pad` | `pad` | `string` |
981
+ | `fallback` | `fallback` | `string` |
982
+ | `speakAs` | `speak-as` | `string` |
983
+
984
+ > Counter-style rules are permanent — they are injected once and never cleaned up, matching how browsers handle `@counter-style`.
985
+
986
+ ---
987
+
988
+ ## Functions (`@function`)
989
+
990
+ Define reusable, parameterized CSS [custom functions](https://developer.mozilla.org/en-US/docs/Web/CSS/@function) via the CSS `@function` at-rule. A custom function takes parameters and returns a single value — like a dynamic custom property.
991
+
992
+ The token convention follows Tasty's `$` vs `$$` distinction:
993
+
994
+ - **`$$name`** — the literal callable name (`--name`). Used for the **definition key** and for **invocation** `$$name(...)`, mirroring the existing `$$gradient-angle` → `--gradient-angle` sugar. The definition key matches the call site.
995
+ - **`$name`** — a custom-property declaration/reference (`var(--name)`). Used for **parameters** and **local variables**, exactly like declaring custom props in a styles object.
996
+ - **Local variables** are declared directly as `$name` keys on the descriptor — the body reads like a mini styles object.
997
+ - `result`, local-variable values, and parameter `default` values flow through the full Tasty DSL, so units (`2x`), color tokens (`#theme`), auto-calc (`(-1 * $value)` → `calc(-1 * var(--value))`), and fallbacks (`($shadow-color, black)` → `var(--shadow-color, black)`) all work.
998
+
999
+ ```ts
1000
+ const Box = tasty({
1001
+ styles: {
1002
+ '@function': {
1003
+ // simplest: one bare param + result (auto-calc, no explicit calc())
1004
+ $$negative: { args: ['$value'], result: '(-1 * $value)' },
1005
+
1006
+ // typed param + default + return type + local variable
1007
+ $$shadow: {
1008
+ args: { '$shadow-color': { syntax: '<color>', default: 'inherit' } },
1009
+ returns: '<color>',
1010
+ $offset: '2px', // local variable
1011
+ result: '$offset $offset ($shadow-color, black)',
1012
+ },
1013
+ },
1014
+ margin: '$$negative(10px) top', // → margin: --negative(10px) 0 0 0
1015
+ shadow: '$$shadow(#accent)', // → box-shadow: --shadow(var(--accent-color))
1016
+ },
1017
+ });
1018
+ ```
1019
+
1020
+ Generated CSS:
1021
+
1022
+ ```css
1023
+ @function --negative(--value) {
1024
+ result: calc(-1 * var(--value));
1025
+ }
1026
+ @function --shadow(--shadow-color <color>: inherit) returns <color> {
1027
+ --offset: 2px;
1028
+ result: var(--offset) var(--offset) var(--shadow-color, black);
1029
+ }
1030
+ .t0 {
1031
+ margin: --negative(10px) 0 0 0;
1032
+ box-shadow: --shadow(var(--accent-color));
1033
+ }
1034
+ ```
1035
+
1036
+ ### Descriptor shape
1037
+
1038
+ | Field | Type | Notes |
1039
+ | ------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
1040
+ | `result` (required) | `string` | The `result:` value, parsed through the Tasty DSL. |
1041
+ | `args` | `string[]` \| `Record<string, FunctionParameter>` | Ordered parameters. Array form lists bare names; object form maps names to type/default. |
1042
+ | `returns` | `string` | Optional return type, e.g. `'<color>'`. |
1043
+ | `$name` keys | `string \| number` | Any `$name` key declares a local variable `--name` (value parsed). |
1044
+
1045
+ A `FunctionParameter` is one of: `true` (bare param), a string CSS type shorthand (`'<length>'`), or `{ syntax?: string; default?: string | number }`.
1046
+
1047
+ Functions can also be registered with the [`useFunction`](react-api.md#usefunction) hook or globally via [`configure({ functions })`](configuration.md#functions) (the same map also accepts bare-key parse functions).
1048
+
1049
+ > `@function` is permanent — injected once and never cleaned up, like `@counter-style`. It is an experimental CSS feature ([Chrome 139+](https://developer.mozilla.org/en-US/docs/Web/CSS/@function)); unsupported browsers safely ignore the native rule, so use it as progressive enhancement — or enable the [`configure({ polyfills: { functions: true } })`](configuration.md#polyfills) inline polyfill to expand `$$name(...)` calls into plain CSS for full cross-browser support.
1050
+
1051
+ ---
1052
+
1053
+ ## Style Properties
1054
+
1055
+ For a complete reference of all enhanced style properties — syntax, values, modifiers, and recommendations — see **[Style Properties Reference](styles.md)**.
1056
+
1057
+ ---
1058
+
1059
+ ## Learn more
1060
+
1061
+ - **[React API](react-api.md)** — `tasty()` factory, component props, variants, sub-elements, style functions
1062
+ - **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
1063
+ - **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
1064
+ - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
1065
+ - **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin