Skip to content

Commit b8a6441

Browse files
Support style compositions in selectors (#259)
1 parent c3d9d78 commit b8a6441

File tree

30 files changed

+1103
-612
lines changed

30 files changed

+1103
-612
lines changed

.changeset/curly-peaches-draw.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@vanilla-extract/css': minor
3+
'@vanilla-extract/esbuild-plugin': minor
4+
'@vanilla-extract/integration': minor
5+
'@vanilla-extract/snowpack-plugin': minor
6+
'@vanilla-extract/vite-plugin': minor
7+
'@vanilla-extract/webpack-plugin': minor
8+
---
9+
10+
Allow the result of `composeStyles` to be used in selectors
11+
12+
When style compositions are used in selectors, they are now assigned an additional class so they can be uniquely identified. When selectors are processed internally, the composed classes are removed, only leaving behind the unique identifier classes. This allows you to treat them as if they were a single class within vanilla-extract selectors.
13+
14+
```ts
15+
import {
16+
style,
17+
globalStyle,
18+
composeStyles
19+
} from '@vanilla-extract/css';
20+
21+
const background = style({ background: 'mintcream' });
22+
const padding = style({ padding: 12 });
23+
24+
export const container = composeStyles(background, padding);
25+
26+
globalStyle(`${container} *`, {
27+
boxSizing: 'border-box'
28+
});
29+
```

.changeset/rich-crews-marry.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@vanilla-extract/sprinkles': minor
3+
---
4+
5+
Allow the result of calling `atoms` to be used in selectors
6+
7+
Sprinkles now uses vanilla-extract’s updated [`composeStyles`](https://github.com/seek-oss/vanilla-extract#composestyles) function internally, which means that atomic styles can be treated as if they were a single class within vanilla-extract selectors.
8+
9+
```ts
10+
// styles.css.ts
11+
import { globalStyle } from '@vanilla-extract/css';
12+
import { atoms } from './sprinkles.css.ts';
13+
14+
export const container = atoms({
15+
padding: 'small',
16+
});
17+
18+
globalStyle(`${container} *`, {
19+
boxSizing: 'border-box'
20+
});
21+
```

README.md

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Want to work at a higher level while maximising style re-use? Check out 🍨 [S
8989
- [style](#style)
9090
- [styleVariants](#styleVariants)
9191
- [globalStyle](#globalstyle)
92+
- [composeStyles](#composestyles)
9293
- [createTheme](#createtheme)
9394
- [createGlobalTheme](#createglobaltheme)
9495
- [createThemeContract](#createthemecontract)
@@ -99,7 +100,6 @@ Want to work at a higher level while maximising style re-use? Check out 🍨 [S
99100
- [globalFontFace](#globalfontface)
100101
- [keyframes](#keyframes)
101102
- [globalKeyframes](#globalkeyframes)
102-
- [composeStyles](#composestyles)
103103
- [Dynamic API](#dynamic-api)
104104
- [createInlineTheme](#createinlinetheme)
105105
- [setElementTheme](#setelementtheme)
@@ -437,6 +437,50 @@ globalStyle(`${parentClass} > a`, {
437437
});
438438
```
439439

440+
### composeStyles
441+
442+
Combines multiple styles into a single class string, while also deduplicating and removing unnecessary spaces.
443+
444+
```ts
445+
import { style, composeStyles } from '@vanilla-extract/css';
446+
447+
const button = style({
448+
padding: 12,
449+
borderRadius: 8
450+
});
451+
452+
export const primaryButton = composeStyles(
453+
button,
454+
style({ background: 'coral' })
455+
);
456+
457+
export const secondaryButton = composeStyles(
458+
button,
459+
style({ background: 'peachpuff' })
460+
);
461+
```
462+
463+
> 💡 Styles can also be provided in shallow and deeply nested arrays, similar to [classnames.](https://github.com/JedWatson/classnames)
464+
465+
When style compositions are used in selectors, they are assigned an additional class so they can be uniquely identified. When selectors are processed internally, the composed classes are removed, only leaving behind the unique identifier classes. This allows you to treat them as if they were a single class within vanilla-extract selectors.
466+
467+
```ts
468+
import {
469+
style,
470+
globalStyle,
471+
composeStyles
472+
} from '@vanilla-extract/css';
473+
474+
const background = style({ background: 'mintcream' });
475+
const padding = style({ padding: 12 });
476+
477+
export const container = composeStyles(background, padding);
478+
479+
globalStyle(`${container} *`, {
480+
boxSizing: 'border-box'
481+
});
482+
```
483+
440484
### createTheme
441485

442486
Creates a locally scoped theme class and a theme contract which can be consumed within your styles.
@@ -709,28 +753,6 @@ export const animated = style({
709753
});
710754
```
711755

712-
### composeStyles
713-
714-
Combines multiple styles into a single class string, while also deduplicating and removing unnecessary spaces.
715-
716-
```ts
717-
import { style, composeStyles } from '@vanilla-extract/css';
718-
719-
const base = style({
720-
padding: 12
721-
});
722-
723-
export const blue = composeStyles(base, style({
724-
background: 'blue'
725-
}));
726-
727-
export const green = composeStyles(base, style({
728-
background: 'green'
729-
}));
730-
```
731-
732-
> 💡 Styles can also be provided in shallow and deeply nested arrays. Think of it as a static version of [classnames.](https://github.com/JedWatson/classnames)
733-
734756
## Dynamic API
735757

736758
We also provide a lightweight standalone package to support dynamic runtime theming.

fixtures/sprinkles/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
atoms,
33
mapResponsiveValue,
44
normalizeResponsiveValue,
5+
preComposedAtoms,
6+
preComposedAtomsUsedInSelector,
57
} from './styles.css';
68
import testNodes from '../test-nodes.json';
79

@@ -19,6 +21,8 @@ function render() {
1921
})}">
2022
Sprinkles
2123
</div>
24+
<div class="${preComposedAtoms}">Precomposed atoms</div>
25+
<div class="${preComposedAtomsUsedInSelector}">Precomposed Atoms Used In Selector</div>
2226
`;
2327
}
2428

fixtures/sprinkles/src/styles.css.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { globalStyle } from '@vanilla-extract/css';
12
import {
23
createAtomicStyles,
34
createAtomsFn,
@@ -36,3 +37,17 @@ export const atoms = createAtomsFn(responsiveStyles);
3637
export const mapResponsiveValue = createMapValueFn(responsiveStyles);
3738
export const normalizeResponsiveValue =
3839
createNormalizeValueFn(responsiveStyles);
40+
41+
export const preComposedAtoms = atoms({
42+
display: 'block',
43+
paddingTop: 'small',
44+
});
45+
46+
export const preComposedAtomsUsedInSelector = atoms({
47+
display: 'flex',
48+
paddingTop: 'medium',
49+
});
50+
51+
globalStyle(`body > ${preComposedAtomsUsedInSelector}`, {
52+
background: 'red',
53+
});

fixtures/themed/src/index.ts

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,51 +26,27 @@ function render() {
2626
<div id="${testNodes.root}" class="${shadow}">
2727
Root theme
2828
<div id="${testNodes.rootContainer}" class="${container}">
29-
<button id="${testNodes.rootButton}" class="${button.join(
30-
' ',
31-
)}">Main theme button</button>
29+
<button id="${testNodes.rootButton}" class="${button}">Main theme button</button>
3230
<div class="${altTheme}">
3331
Alt theme
3432
<div id="${testNodes.altContainer}" class="${container}">
35-
<button id="${testNodes.altButton}" class="${button.join(
36-
' ',
37-
)}">Alt theme button</button>
33+
<button id="${testNodes.altButton}" class="${button}">Alt theme button</button>
3834
<div class="${theme}">
3935
Back to root theme
4036
<div id="${testNodes.nestedRootContainer}" class="${container}">
41-
<button id="${testNodes.nestedRootButton}" class="${button.join(
42-
' ',
43-
)}">Main theme button</button>
37+
<button id="${testNodes.nestedRootButton}" class="${button}">Main theme button</button>
4438
<div style="${inlineTheme}">
4539
Inline theme
46-
<div id="${
47-
testNodes.inlineThemeContainer
48-
}" class="${container}">
49-
<button id="${
50-
testNodes.inlineThemeButton
51-
}" class="${button.join(' ')} ${
52-
opacity['1/2']
53-
}">Inline theme button</button>
40+
<div id="${testNodes.inlineThemeContainer}" class="${container}">
41+
<button id="${testNodes.inlineThemeButton}" class="${button} ${opacity['1/2']}">Inline theme button</button>
5442
<div>
5543
Dynamic vars
56-
<div id="${
57-
testNodes.dynamicVarsContainer
58-
}" class="${container}">
59-
<button id="${
60-
testNodes.dynamicVarsButton
61-
}" class="${button.join(
62-
' ',
63-
)}">Dynamic vars button</button>
44+
<div id="${testNodes.dynamicVarsContainer}" class="${container}">
45+
<button id="${testNodes.dynamicVarsButton}" class="${button}">Dynamic vars button</button>
6446
<div class="${responsiveTheme}">
6547
Responsive theme
66-
<div id="${
67-
testNodes.responsiveThemeContainer
68-
}" class="${container}">
69-
<button id="${
70-
testNodes.responsiveThemeButton
71-
}" class="${button.join(
72-
' ',
73-
)}">Responsive theme button</button>
48+
<div id="${testNodes.responsiveThemeContainer}" class="${container}">
49+
<button id="${testNodes.responsiveThemeButton}" class="${button}">Responsive theme button</button>
7450
</div>
7551
</div>
7652
</div>

fixtures/themed/src/styles.css.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
globalFontFace,
88
keyframes,
99
globalKeyframes,
10+
composeStyles,
11+
globalStyle,
1012
} from '@vanilla-extract/css';
1113
import { shadow } from './shared.css';
1214
import { vars, theme, altTheme } from './themes.css';
@@ -50,9 +52,13 @@ export const container = style({
5052
},
5153
});
5254

53-
export const button = [
55+
const iDunno = composeStyles(
56+
style({ zIndex: 1 }),
57+
style({ position: 'relative' }),
58+
);
59+
60+
export const button = composeStyles(
5461
style({
55-
animation: `3s infinite alternate ${slide} ease-in-out`,
5662
fontFamily: impact,
5763
backgroundColor: fallbackVar(
5864
vars.colors.backgroundColor,
@@ -72,7 +78,12 @@ export const button = [
7278
},
7379
}),
7480
shadow,
75-
];
81+
iDunno,
82+
);
83+
84+
globalStyle(`body ${iDunno}`, {
85+
animation: `3s infinite alternate ${slide} ease-in-out`,
86+
});
7687

7788
const blankVar1 = createVar();
7889
const blankVar2 = createVar();

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"dev": "preconstruct dev",
99
"build": "preconstruct build",
10+
"watch": "preconstruct watch",
1011
"start-fixture": "ts-node --log-error ./test-helpers/src/startFixtureCLI",
1112
"start": "yarn start-fixture themed",
1213
"start-site": "manypkg run site start",

packages/css/src/adapter.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const mockAdapter: Adapter = {
44
appendCss: () => {},
55
registerClassName: () => {},
66
onEndFileScope: () => {},
7+
registerComposition: () => {},
8+
markCompositionUsed: () => {},
79
};
810

911
let adapter: Adapter = mockAdapter;
@@ -29,6 +31,18 @@ export const registerClassName: Adapter['registerClassName'] = (...props) => {
2931
return adapter.registerClassName(...props);
3032
};
3133

34+
export const registerComposition: Adapter['registerComposition'] = (
35+
...props
36+
) => {
37+
return adapter.registerComposition(...props);
38+
};
39+
40+
export const markCompositionUsed: Adapter['markCompositionUsed'] = (
41+
...props
42+
) => {
43+
return adapter.markCompositionUsed(...props);
44+
};
45+
3246
export const onEndFileScope: Adapter['onEndFileScope'] = (...props) => {
3347
return adapter.onEndFileScope(...props);
3448
};

packages/css/src/composeStyles.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { composeStyles } from './composeStyles';
1+
import { dudupeAndJoinClassList } from './composeStyles';
22

3-
describe('composeStyles', () => {
3+
describe('dudupeAndJoinClassList', () => {
44
it.each([
55
{ args: ['1'], output: '1' },
66
{ args: ['1 1'], output: '1' },
@@ -20,6 +20,6 @@ describe('composeStyles', () => {
2020
{ args: ['1 2 3', '2 3 4', '1 5'], output: '1 2 3 4 5' },
2121
{ args: [' 1 2 3 2 ', ' 2 3 4 2 ', ' 1 5 1 '], output: '1 2 3 4 5' },
2222
])('composeStyles', ({ args, output }) => {
23-
expect(composeStyles(...args)).toBe(output);
23+
expect(dudupeAndJoinClassList(args)).toBe(output);
2424
});
2525
});

0 commit comments

Comments
 (0)