tailwind-oklch 0.4.0 → 0.6.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 (4) hide show
  1. package/README.md +49 -14
  2. package/index.css +51 -57
  3. package/package.json +1 -1
  4. package/plugin.js +171 -1
package/README.md CHANGED
@@ -184,6 +184,36 @@ The direction automatically adapts to light/dark mode — "up" always means more
184
184
  </p>
185
185
  ```
186
186
 
187
+ ### Arbitrary Values
188
+
189
+ All three axes support arbitrary values using Tailwind's bracket syntax. This gives you fine-grained control beyond the named stops.
190
+
191
+ **Hue** — any degree value (0–360):
192
+
193
+ ```html
194
+ <div class="hue-[180] bg-3-mid">Teal background</div>
195
+ <div class="bg-h-[280] text-h-[40]">Purple bg, orange text</div>
196
+ ```
197
+
198
+ **Chroma** — integer 0–100, mapped to OKLCH 0.00–1.00 (practical range is roughly 0–25):
199
+
200
+ ```html
201
+ <div class="chroma-[8] bg-lc-3">All properties at chroma 0.08</div>
202
+ <div class="bg-c-[15]">Background chroma 0.15</div>
203
+ ```
204
+
205
+ **Luminance** — integer 0–100, with automatic light/dark mode flip:
206
+
207
+ ```html
208
+ <div class="bg-lc-[60]">
209
+ Light mode: L=0.60 · Dark mode: L=0.40
210
+ </div>
211
+ ```
212
+
213
+ Arbitrary luminance values automatically invert in dark mode (reflected around 0.50), so `bg-lc-[70]` renders as 0.70 in light mode and 0.30 in dark mode — always maintaining the same relationship to the page.
214
+
215
+ Available for all property prefixes (`bg-`, `text-`, `border-`, etc.), global setters (`hue-`, `chroma-`), and gradients (`from-`, `to-`).
216
+
187
217
  ### Gradients
188
218
 
189
219
  ```html
@@ -246,19 +276,24 @@ document.documentElement.style.setProperty('--hue-primary', '180');
246
276
 
247
277
  ### Luminance Contrast Scale
248
278
 
249
- | Stop | Dark Mode | Light Mode |
279
+ | Stop | Light Mode | Dark Mode |
250
280
  |---|---|---|
251
- | `0` / `base` | 0.12 | 0.95 |
252
- | `1` | 0.20 | 0.87 |
253
- | `2` | 0.28 | 0.79 |
254
- | `3` | 0.36 | 0.71 |
255
- | `4` | 0.44 | 0.63 |
256
- | `5` | 0.52 | 0.55 |
257
- | `6` | 0.60 | 0.47 |
258
- | `7` | 0.68 | 0.39 |
259
- | `8` | 0.76 | 0.31 |
260
- | `9` | 0.84 | 0.23 |
261
- | `10` / `fore` | 0.92 | 0.15 |
281
+ | `0` / `base` | 0.95 | 0.12 |
282
+ | `1` | 0.91 | 0.20 |
283
+ | `2` | 0.85 | 0.28 |
284
+ | `3` | 0.78 | 0.36 |
285
+ | `4` | 0.71 | 0.44 |
286
+ | `5` | 0.63 | 0.52 |
287
+ | `6` | 0.54 | 0.60 |
288
+ | `7` | 0.44 | 0.68 |
289
+ | `8` | 0.34 | 0.76 |
290
+ | `9` | 0.23 | 0.84 |
291
+ | `10` / `fore` | 0.15 | 0.92 |
292
+
293
+ Light mode uses a power curve (p≈1.3) so steps near the high-luminance
294
+ base are smaller — small luminance differences are very perceptible
295
+ against a near-white surface, so `lc-1` needs a gentler delta than the
296
+ linear 0.08 would give. Dark mode stays linear.
262
297
 
263
298
  ### Named Chroma Stops
264
299
 
@@ -270,7 +305,7 @@ document.documentElement.style.setProperty('--hue-primary', '180');
270
305
  | `mhi` | 0.18 | Vivid |
271
306
  | `hi` | 0.25 | Maximum saturation |
272
307
 
273
- A numeric chroma scale (`c-10` through `c-95`) is also available for finer control in the decomposed utilities.
308
+ For finer control, use arbitrary chroma values (see [Arbitrary Values](#arbitrary-values) below).
274
309
 
275
310
  ### LC Adjustment Steps
276
311
 
@@ -317,7 +352,7 @@ Override in a `@theme` block:
317
352
 
318
353
  ### Light / Dark Mode
319
354
 
320
- Dark mode is the default. Light mode activates when the root element does **not** have the `.dark` class (`:root:not(.dark)`). The luminance contrast scale flips automatically — `lc-0` is always near the page, `lc-10` is always high contrast — no additional classes needed.
355
+ Light mode is the default. Dark mode activates when the root element has the `.dark` class. The luminance contrast scale flips automatically — `lc-0` is always near the page, `lc-10` is always high contrast — no additional classes needed.
321
356
 
322
357
  ## License
323
358
 
package/index.css CHANGED
@@ -31,37 +31,41 @@
31
31
 
32
32
  /* ── Luminance Contrast Range ─────────────────────────────────────
33
33
  Defines the OKLCH lightness values at 0 (base) and 10 (fore).
34
- Dark mode: base is near black, fore is near white.
35
- Light mode flips them (see :root:not(.dark) below).
34
+ Light mode (default): base is near white, fore is near black.
35
+ Dark mode flips them (see .dark below).
36
36
  Override --lc-range-start and --lc-range-end to shift the range. */
37
- --lc-range-start: 0.12;
38
- --lc-range-end: 0.92;
37
+ --lc-range-start: 0.95;
38
+ --lc-range-end: 0.15;
39
39
 
40
40
  /* ── 0–10 Luminance Contrast Scale ────────────────────────────────
41
- Computed: step N = start + (end - start) × (N / 10)
42
- Dark mode (default): 0→0.12, 5→0.52, 10→0.92
43
- Light mode (override): 0→0.95, 5→0.55, 10→0.15 */
44
- --l-0: 0.12;
45
- --l-1: 0.20;
46
- --l-2: 0.28;
47
- --l-3: 0.36;
48
- --l-4: 0.44;
49
- --l-5: 0.52;
50
- --l-6: 0.60;
51
- --l-7: 0.68;
52
- --l-8: 0.76;
53
- --l-9: 0.84;
54
- --l-10: 0.92;
41
+ Light mode uses a power curve (p≈1.3): smaller steps near the
42
+ high-luminance base, growing toward the fore. Perception is
43
+ more sensitive to differences near white, so subtle surfaces
44
+ need finer L deltas there.
45
+ Computed: step N = start + (end − start) × (N / 10)^1.3
46
+ Light mode (default): 0→0.95, 1→0.91, 5→0.63, 10→0.15
47
+ Dark mode stays linear (see .dark): 0→0.12, 5→0.52, 10→0.92 */
48
+ --l-0: 0.95;
49
+ --l-1: 0.91;
50
+ --l-2: 0.85;
51
+ --l-3: 0.78;
52
+ --l-4: 0.71;
53
+ --l-5: 0.63;
54
+ --l-6: 0.54;
55
+ --l-7: 0.44;
56
+ --l-8: 0.34;
57
+ --l-9: 0.23;
58
+ --l-10: 0.15;
55
59
 
56
60
  /* Semantic aliases */
57
- --l-base: 0.12;
58
- --l-fore: 0.92;
61
+ --l-base: 0.95;
62
+ --l-fore: 0.15;
59
63
 
60
64
  /* Absolute extremes — escape the configured range entirely.
61
65
  none = beyond base (pure white in light, pure black in dark).
62
66
  full = beyond fore (pure black in light, pure white in dark). */
63
- --l-none: 0;
64
- --l-full: 1;
67
+ --l-none: 1;
68
+ --l-full: 0;
65
69
 
66
70
  /* ── Named Chroma Stops ──────────────────────────────────────────────
67
71
  OKLCH chroma: practical range 0–0.25 */
@@ -71,18 +75,6 @@
71
75
  --c-mhi: 0.18;
72
76
  --c-hi: 0.25;
73
77
 
74
- /* ── Numeric Chroma Scale ────────────────────────────────────────── */
75
- --c-10: 0.03;
76
- --c-20: 0.06;
77
- --c-30: 0.09;
78
- --c-40: 0.12;
79
- --c-50: 0.15;
80
- --c-60: 0.18;
81
- --c-70: 0.21;
82
- --c-80: 0.24;
83
- --c-90: 0.27;
84
- --c-95: 0.30;
85
-
86
78
  /* ── LC Adjustment Steps ───────────────────────────────────────────
87
79
  Each step ≈ one position on the 0–10 scale (~0.08 OKLCH L). */
88
80
  --lc-adj-1: 0.08;
@@ -92,32 +84,33 @@
92
84
  --lc-adj-5: 0.40;
93
85
  }
94
86
 
95
- /* ── Light-mode luminance contrast range ──────────────────────────────
96
- In light mode the scale flips: 0/base is near white (blends with
97
- the page), 10/fore is near black (high contrast, like text). */
87
+ /* ── Dark-mode luminance contrast range ──────────────────────────────
88
+ In dark mode the scale flips: 0/base is near black (blends with
89
+ the page), 10/fore is near white (high contrast, like text). */
98
90
 
99
- :root:not(.dark) {
100
- --lc-dir: -1;
101
- --lc-range-start: 0.95;
102
- --lc-range-end: 0.15;
91
+ .dark {
92
+ --lc-dir: 1;
93
+ --lc-flip: 1;
94
+ --lc-range-start: 0.12;
95
+ --lc-range-end: 0.92;
103
96
 
104
- --l-0: 0.95;
105
- --l-1: 0.87;
106
- --l-2: 0.79;
107
- --l-3: 0.71;
108
- --l-4: 0.63;
109
- --l-5: 0.55;
110
- --l-6: 0.47;
111
- --l-7: 0.39;
112
- --l-8: 0.31;
113
- --l-9: 0.23;
114
- --l-10: 0.15;
97
+ --l-0: 0.12;
98
+ --l-1: 0.20;
99
+ --l-2: 0.28;
100
+ --l-3: 0.36;
101
+ --l-4: 0.44;
102
+ --l-5: 0.52;
103
+ --l-6: 0.60;
104
+ --l-7: 0.68;
105
+ --l-8: 0.76;
106
+ --l-9: 0.84;
107
+ --l-10: 0.92;
115
108
 
116
- --l-base: 0.95;
117
- --l-fore: 0.15;
109
+ --l-base: 0.12;
110
+ --l-fore: 0.92;
118
111
 
119
- --l-none: 1;
120
- --l-full: 0;
112
+ --l-none: 0;
113
+ --l-full: 1;
121
114
  }
122
115
 
123
116
  /* ── Cascade Defaults ────────────────────────────────────────────────────
@@ -126,7 +119,8 @@
126
119
  naturally flows to children that only set bg-lc-* or bg-c-*. */
127
120
 
128
121
  :root {
129
- --lc-dir: 1;
122
+ --lc-dir: -1;
123
+ --lc-flip: 0;
130
124
 
131
125
  --bg-l: var(--l-5);
132
126
  --bg-c: var(--c-lo);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwind-oklch",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "OKLCH color composition system for Tailwind CSS v4",
5
5
  "style": "index.css",
6
6
  "main": "index.css",
package/plugin.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * Load via: @plugin "tailwind-oklch/plugin";
18
18
  */
19
19
 
20
- module.exports = function ({ addUtilities }) {
20
+ module.exports = function ({ addUtilities, matchUtilities }) {
21
21
  const luminances = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'base', 'fore', 'none', 'full'];
22
22
  const chromas = ['lo', 'mlo', 'mid', 'mhi', 'hi'];
23
23
  const hues = ['primary', 'accent', 'success', 'warning', 'danger', 'info', 'neutral'];
@@ -97,4 +97,174 @@ module.exports = function ({ addUtilities }) {
97
97
  }
98
98
 
99
99
  addUtilities(utilities);
100
+
101
+ // ── Arbitrary hue values ────────────────────────────────────────────────
102
+ // hue-[180] → sets all hue properties to 180 (degrees).
103
+ // bg-h-[280] → sets only background hue to 280.
104
+
105
+ const hueVars = ['--bg-h', '--tx-h', '--bd-h', '--bdb-h', '--ac-h', '--gf-h', '--gt-h', '--sh-h'];
106
+
107
+ matchUtilities(
108
+ {
109
+ hue: (value) => Object.fromEntries(hueVars.map((v) => [v, value])),
110
+ },
111
+ { type: ['integer'] },
112
+ );
113
+
114
+ for (const prop of properties) {
115
+ matchUtilities(
116
+ {
117
+ [`${prop.prefix}-h`]: (value) => ({
118
+ [prop.vars[2]]: value,
119
+ [prop.css]: `oklch(var(${prop.vars[0]}) var(${prop.vars[1]}) var(${prop.vars[2]}))`,
120
+ }),
121
+ },
122
+ { type: ['integer'] },
123
+ );
124
+ }
125
+
126
+ // Gradient from/to arbitrary hue
127
+ matchUtilities(
128
+ {
129
+ 'from-h': (value) => ({
130
+ '--gf-h': value,
131
+ '--tw-gradient-from': 'oklch(var(--gf-l) var(--gf-c) var(--gf-h))',
132
+ '--tw-gradient-stops': stopsExpr,
133
+ }),
134
+ 'to-h': (value) => ({
135
+ '--gt-h': value,
136
+ '--tw-gradient-to': 'oklch(var(--gt-l) var(--gt-c) var(--gt-h))',
137
+ '--tw-gradient-stops': stopsExpr,
138
+ }),
139
+ },
140
+ { type: ['integer'] },
141
+ );
142
+
143
+ // Shadow arbitrary hue
144
+ matchUtilities(
145
+ {
146
+ 'shadow-h': (value) => ({
147
+ '--sh-h': value,
148
+ '--tw-shadow-color': 'oklch(var(--sh-l) var(--sh-c) var(--sh-h))',
149
+ }),
150
+ },
151
+ { type: ['integer'] },
152
+ );
153
+
154
+ // ── Arbitrary chroma values ───────────────────────────────────────────
155
+ // chroma-[15] → sets all chroma properties to 0.15.
156
+ // bg-c-[20] → sets only background chroma to 0.20.
157
+
158
+ const chromaVars = ['--bg-c', '--tx-c', '--bd-c', '--bdb-c', '--ac-c', '--gf-c', '--gt-c', '--sh-c'];
159
+
160
+ const chromaValue = (value) => {
161
+ const v = Number(value) / 100;
162
+ return `${Math.round(v * 1e6) / 1e6}`;
163
+ };
164
+
165
+ matchUtilities(
166
+ {
167
+ chroma: (value) => {
168
+ const c = chromaValue(value);
169
+ return Object.fromEntries(chromaVars.map((v) => [v, c]));
170
+ },
171
+ },
172
+ { type: ['integer'] },
173
+ );
174
+
175
+ for (const prop of properties) {
176
+ matchUtilities(
177
+ {
178
+ [`${prop.prefix}-c`]: (value) => ({
179
+ [prop.vars[1]]: chromaValue(value),
180
+ [prop.css]: `oklch(var(${prop.vars[0]}) var(${prop.vars[1]}) var(${prop.vars[2]}))`,
181
+ }),
182
+ },
183
+ { type: ['integer'] },
184
+ );
185
+ }
186
+
187
+ // Gradient from/to arbitrary chroma
188
+ matchUtilities(
189
+ {
190
+ 'from-c': (value) => ({
191
+ '--gf-c': chromaValue(value),
192
+ '--tw-gradient-from': 'oklch(var(--gf-l) var(--gf-c) var(--gf-h))',
193
+ '--tw-gradient-stops': stopsExpr,
194
+ }),
195
+ 'to-c': (value) => ({
196
+ '--gt-c': chromaValue(value),
197
+ '--tw-gradient-to': 'oklch(var(--gt-l) var(--gt-c) var(--gt-h))',
198
+ '--tw-gradient-stops': stopsExpr,
199
+ }),
200
+ },
201
+ { type: ['integer'] },
202
+ );
203
+
204
+ // Shadow arbitrary chroma
205
+ matchUtilities(
206
+ {
207
+ 'shadow-c': (value) => ({
208
+ '--sh-c': chromaValue(value),
209
+ '--tw-shadow-color': 'oklch(var(--sh-l) var(--sh-c) var(--sh-h))',
210
+ }),
211
+ },
212
+ { type: ['integer'] },
213
+ );
214
+
215
+ // ── Auto-flip luminance for arbitrary values ──────────────────────────
216
+ // bg-lc-[60] → light mode L=0.60, dark mode L=0.40 (simple 1−x flip).
217
+ // Uses --lc-flip (0 in light, 1 in dark) so the transform is pure CSS.
218
+ // Formula: L = v + flip × (1 − 2v) where v = input / 100
219
+ // flip=0 → v (light: use value as-is)
220
+ // flip=1 → 1−v (dark: reflect around 0.5)
221
+
222
+ const lcFlipValue = (value) => {
223
+ const v = Number(value) / 100;
224
+ const delta = 1 - 2 * v;
225
+ // Round to avoid floating-point noise in the CSS output
226
+ const vR = Math.round(v * 1e6) / 1e6;
227
+ const dR = Math.round(delta * 1e6) / 1e6;
228
+ return `calc(${vR} + var(--lc-flip) * ${dR})`;
229
+ };
230
+
231
+ for (const prop of properties) {
232
+ matchUtilities(
233
+ {
234
+ [`${prop.prefix}-lc`]: (value) => ({
235
+ [prop.vars[0]]: lcFlipValue(value),
236
+ [prop.css]: `oklch(var(${prop.vars[0]}) var(${prop.vars[1]}) var(${prop.vars[2]}))`,
237
+ }),
238
+ },
239
+ { type: ['integer'] },
240
+ );
241
+ }
242
+
243
+ // Gradient from/to auto-flip luminance
244
+ matchUtilities(
245
+ {
246
+ 'from-lc': (value) => ({
247
+ '--gf-l': lcFlipValue(value),
248
+ '--tw-gradient-from': 'oklch(var(--gf-l) var(--gf-c) var(--gf-h))',
249
+ '--tw-gradient-stops': stopsExpr,
250
+ }),
251
+ 'to-lc': (value) => ({
252
+ '--gt-l': lcFlipValue(value),
253
+ '--tw-gradient-to': 'oklch(var(--gt-l) var(--gt-c) var(--gt-h))',
254
+ '--tw-gradient-stops': stopsExpr,
255
+ }),
256
+ },
257
+ { type: ['integer'] },
258
+ );
259
+
260
+ // Shadow auto-flip luminance
261
+ matchUtilities(
262
+ {
263
+ 'shadow-lc': (value) => ({
264
+ '--sh-l': lcFlipValue(value),
265
+ '--tw-shadow-color': 'oklch(var(--sh-l) var(--sh-c) var(--sh-h))',
266
+ }),
267
+ },
268
+ { type: ['integer'] },
269
+ );
100
270
  };