Skip to content

Commit dcdb8b8

Browse files
VladBrokai
andauthored
Add light-dark() support (#26)
* add light-dark support * add light-dark() example to README * fix * fix * review issues * prevent from transfomring light-dark inside strings * Update README.md Co-authored-by: Andrey Sitnik <[email protected]> * Update README.md Co-authored-by: Andrey Sitnik <[email protected]> * move link --------- Co-authored-by: Andrey Sitnik <[email protected]>
1 parent 68fa93b commit dcdb8b8

3 files changed

Lines changed: 299 additions & 1 deletion

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ CSS solution for light/dark/auto theme switcher for websites.
1212
by subset/sunrise (all operating systems now have theme switching schedule).
1313

1414
[PostCSS] plugin to make switcher to force dark or light theme by copying styles
15-
from media query to special class.
15+
from media query or [light-dark()] to special class.
1616

1717
[PostCSS]: https://github.com/postcss/postcss
1818
[FART]: https://css-tricks.com/flash-of-inaccurate-color-theme-fart/
19+
[light-dark()]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark
1920

2021
```css
2122
/* Input CSS */
@@ -28,6 +29,10 @@ from media query to special class.
2829
background: black
2930
}
3031
}
32+
33+
section {
34+
background: light-dark(white, black);
35+
}
3136
```
3237

3338
```css
@@ -47,6 +52,23 @@ html:where(.is-dark) {
4752
:where(html.is-dark) body {
4853
background: black
4954
}
55+
56+
@media (prefers-color-scheme: dark) {
57+
:where(html:not(.is-light)) section {
58+
background: black;
59+
}
60+
}
61+
:where(html.is-dark) section {
62+
background: black;
63+
}
64+
@media (prefers-color-scheme: light) {
65+
:where(html:not(.is-dark)) section {
66+
background: white;
67+
}
68+
}
69+
:where(html.is-light) section {
70+
background: white;
71+
}
5072
```
5173

5274
By default (without classes on `html`), website will use browser dark/light

index.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const PREFERS_COLOR_ONLY = /^\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)$/
22
const PREFERS_COLOR = /\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)/g
3+
const LIGHT_DARK = /light-dark\(\s*(.+?)\s*,\s*(.+?)\s*\)/g
4+
const STRING = /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/dg
35

46
function escapeRegExp(string) {
57
return string.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&')
@@ -9,6 +11,38 @@ function replaceAll(string, find, replace) {
911
return string.replace(new RegExp(escapeRegExp(find), 'g'), replace)
1012
}
1113

14+
function addColorSchemeMedia(isDark, propValue, declaration, postcss) {
15+
let mediaQuery = postcss.atRule({
16+
name: 'media',
17+
params: `(prefers-color-scheme:${isDark ? 'dark' : 'light'})`
18+
})
19+
mediaQuery.append(
20+
postcss.rule({
21+
nodes: [
22+
postcss.decl({
23+
prop: declaration.prop,
24+
value: propValue
25+
})
26+
],
27+
selector: declaration.parent.selector
28+
})
29+
)
30+
declaration.parent.after(mediaQuery)
31+
}
32+
33+
function replaceLightDark(isDark, declaration, stringBoundaries) {
34+
return declaration.value.replaceAll(
35+
LIGHT_DARK,
36+
(match, lightColor, darkColor, offset) => {
37+
let isInsideString = stringBoundaries.some(
38+
boundary => offset > boundary[0] && offset < boundary[1]
39+
)
40+
if (isInsideString) return match
41+
return isDark ? darkColor : lightColor
42+
}
43+
)
44+
}
45+
1246
module.exports = (opts = {}) => {
1347
let dark = opts.darkSelector || '.is-dark'
1448
let light = opts.lightSelector || '.is-light'
@@ -109,6 +143,30 @@ module.exports = (opts = {}) => {
109143
}
110144
}
111145
},
146+
DeclarationExit: (declaration, { postcss }) => {
147+
if (!declaration.value.includes('light-dark')) return
148+
149+
let stringBoundaries = []
150+
let value = declaration.value.slice()
151+
let match = STRING.exec(value)
152+
while (match) {
153+
stringBoundaries.push(match.indices[0])
154+
match = STRING.exec(value)
155+
}
156+
157+
let lightValue = replaceLightDark(false, declaration, stringBoundaries)
158+
if (declaration.value === lightValue) return
159+
let darkValue = replaceLightDark(true, declaration, stringBoundaries)
160+
161+
addColorSchemeMedia(false, lightValue, declaration, postcss)
162+
addColorSchemeMedia(true, darkValue, declaration, postcss)
163+
164+
let parent = declaration.parent
165+
declaration.remove()
166+
if (parent.nodes.length === 0) {
167+
parent.remove()
168+
}
169+
},
112170
postcssPlugin: 'postcss-dark-theme-class'
113171
}
114172
}

index.test.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,222 @@ test('ignores already transformed rules - light scheme', () => {
383383
)
384384
})
385385

386+
test('transforms light-dark()', () => {
387+
run(
388+
`html {
389+
border: 1px solid light-dark(white, black)
390+
}`,
391+
`@media (prefers-color-scheme:dark) {
392+
html:where(:not(.is-light)) {
393+
border: 1px solid black
394+
}
395+
}
396+
html:where(.is-dark) {
397+
border: 1px solid black
398+
}
399+
@media (prefers-color-scheme:light) {
400+
html:where(:not(.is-dark)) {
401+
border: 1px solid white
402+
}
403+
}
404+
html:where(.is-light) {
405+
border: 1px solid white
406+
}`
407+
)
408+
})
409+
410+
test('does not transform light-dark() inside strings', () => {
411+
run(
412+
`html {
413+
content: ' light-dark(white, black) \
414+
light-dark(purple, yellow)
415+
';
416+
background: url("light-dark(red, blue).png");
417+
quotes: "light-dark(white, black)" "light-dark(red, green)";
418+
}`,
419+
`html {
420+
content: ' light-dark(white, black) \
421+
light-dark(purple, yellow)
422+
';
423+
background: url("light-dark(red, blue).png");
424+
quotes: "light-dark(white, black)" "light-dark(red, green)";
425+
}`
426+
)
427+
})
428+
429+
test('transforms light-dark() and disables :where() of request', () => {
430+
run(
431+
`section {
432+
color: light-dark(#888, #eee)
433+
}`,
434+
`@media (prefers-color-scheme:dark) {
435+
html:not(.is-light) section {
436+
color: #eee
437+
}
438+
}
439+
html.is-dark section {
440+
color: #eee
441+
}
442+
@media (prefers-color-scheme:light) {
443+
html:not(.is-dark) section {
444+
color: #888
445+
}
446+
}
447+
html.is-light section {
448+
color: #888
449+
}`,
450+
{ useWhere: false }
451+
)
452+
})
453+
454+
test('processes inner at-rules with light-dark()', () => {
455+
run(
456+
`@media (min-width: 500px) {
457+
@media (print) {
458+
a {
459+
background-color: light-dark(white, black)
460+
}
461+
}
462+
}`,
463+
`@media (min-width: 500px) {
464+
@media (print) {
465+
@media (prefers-color-scheme:dark) {
466+
:where(html:not(.is-light)) a {
467+
background-color: black
468+
}
469+
}
470+
:where(html.is-dark) a {
471+
background-color: black
472+
}
473+
@media (prefers-color-scheme:light) {
474+
:where(html:not(.is-dark)) a {
475+
background-color: white
476+
}
477+
}
478+
:where(html.is-light) a {
479+
background-color: white
480+
}
481+
}
482+
}`
483+
)
484+
})
485+
486+
test('ignores whitespaces for light-dark()', () => {
487+
run(
488+
`a { background: radial-gradient(light-dark( red , yellow ),
489+
light-dark( white , black ),
490+
rgb(30 144 255)); }
491+
`,
492+
`@media (prefers-color-scheme:dark) {
493+
:where(html:not(.is-light)) a {
494+
background: radial-gradient(yellow,
495+
black,
496+
rgb(30 144 255))
497+
}
498+
}
499+
:where(html.is-dark) a {
500+
background: radial-gradient(yellow,
501+
black,
502+
rgb(30 144 255))
503+
}
504+
@media (prefers-color-scheme:light) {
505+
:where(html:not(.is-dark)) a {
506+
background: radial-gradient(red,
507+
white,
508+
rgb(30 144 255))
509+
}
510+
}
511+
:where(html.is-light) a {
512+
background: radial-gradient(red,
513+
white,
514+
rgb(30 144 255))
515+
}
516+
`
517+
)
518+
})
519+
520+
test('changes root selectors for light-dark()', () => {
521+
run(
522+
`html, .s { --bg: light-dark(white, black) }
523+
p { color: light-dark(red, blue) }
524+
`,
525+
`@media (prefers-color-scheme:dark) {
526+
html:where(:not(.is-light)), .s:where(:not(.is-light)) {
527+
--bg: black
528+
}
529+
}
530+
html:where(.is-dark), .s:where(.is-dark) {
531+
--bg: black
532+
}
533+
@media (prefers-color-scheme:light) {
534+
html:where(:not(.is-dark)), .s:where(:not(.is-dark)) {
535+
--bg: white
536+
}
537+
}
538+
html:where(.is-light), .s:where(.is-light) {
539+
--bg: white
540+
}
541+
@media (prefers-color-scheme:dark) {
542+
:where(html:not(.is-light)) p,:where(.s:not(.is-light)) p {
543+
color: blue
544+
}
545+
}
546+
:where(html.is-dark) p,:where(.s.is-dark) p {
547+
color: blue
548+
}
549+
@media (prefers-color-scheme:light) {
550+
:where(html:not(.is-dark)) p,:where(.s:not(.is-dark)) p {
551+
color: red
552+
}
553+
}
554+
:where(html.is-light) p,:where(.s.is-light) p {
555+
color: red
556+
}
557+
`,
558+
{ rootSelector: ['html', ':root', '.s'] }
559+
)
560+
})
561+
562+
test('changes root selector for light-dark()', () => {
563+
run(
564+
`body { --bg: light-dark(white, black) }
565+
p { color: light-dark(green, yellow) }
566+
`,
567+
`@media (prefers-color-scheme:dark) {
568+
body:where(:not(.is-light)) {
569+
--bg: black
570+
}
571+
}
572+
body:where(.is-dark) {
573+
--bg: black
574+
}
575+
@media (prefers-color-scheme:light) {
576+
body:where(:not(.is-dark)) {
577+
--bg: white
578+
}
579+
}
580+
body:where(.is-light) {
581+
--bg: white
582+
}
583+
@media (prefers-color-scheme:dark) {
584+
:where(body:not(.is-light)) p {
585+
color: yellow
586+
}
587+
}
588+
:where(body.is-dark) p {
589+
color: yellow
590+
}
591+
@media (prefers-color-scheme:light) {
592+
:where(body:not(.is-dark)) p {
593+
color: green
594+
}
595+
}
596+
:where(body.is-light) p {
597+
color: green
598+
}
599+
`,
600+
{ rootSelector: 'body' }
601+
)
602+
})
603+
386604
test.run()

0 commit comments

Comments
 (0)