Skip to content

Add support for @starting-style rules #1566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-beans-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vanilla-extract/css': minor
---

Add support for `@starting-style` rules
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Add support for `@starting-style` rules
`style`: Add support for `@starting-style` rules
**EXAMPLE USAGE**:
```ts
import { style } from '@vanilla-extact/css';
export const styleWithStartingStyle = style({
backgroundColor: 'black',
'@starting-style': {
backgroundColor: 'white',
},
});
```

8 changes: 8 additions & 0 deletions fixtures/features/src/features.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ export const styleVariantsCompositionInSelector = styleVariants({
globalStyle(`body ${styleVariantsCompositionInSelector.variant}`, {
fontSize: '24px',
});

// Style with starting-style
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Style with starting-style

export const styleWithStartingStyle = style({
backgroundColor: 'black',
'@starting-style': {
backgroundColor: 'white',
},
});
1 change: 1 addition & 0 deletions fixtures/features/src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default `
<div id="${testNodes.compositionOnly}" class="${styles.compositionOnly}">Composition only</div>
<div id="${testNodes.styleCompositionInSelector}" class="${styles.styleCompositionInSelector}">Style composition in selector</div>
<div id="${testNodes.styleVariantsCompositionInSelector}" class="${styles.styleVariantsCompositionInSelector.variant}">Style variants composition in selector</div>
<div id="${testNodes.styleWithStartingStyle}" class="${styles.styleWithStartingStyle}">Style with @starting-style rule</div>
`;

// @ts-expect-error Vite env not defined
Expand Down
3 changes: 2 additions & 1 deletion fixtures/features/test-nodes.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"styleVariantsWithMappedComposition": "styleVariantsWithMappedComposition",
"compositionOnly": "compositionOnly",
"styleCompositionInSelector": "styleCompositionInSelector",
"styleVariantsCompositionInSelector": "styleVariantsCompositionInSelector"
"styleVariantsCompositionInSelector": "styleVariantsCompositionInSelector",
"styleWithStartingStyle": "styleWithStartingStyle"
}
166 changes: 166 additions & 0 deletions packages/css/src/transformCss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2500,6 +2500,172 @@ describe('transformCss', () => {
}
`);
});

it('should handle @starting-style declaration', () => {
expect(
transformCss({
composedClassLists: [],
localClassNames: ['testClass'],
cssObjs: [
{
type: 'local',
selector: 'testClass',
rule: {
opacity: 1,
top: '100%',
'@starting-style': {
opacity: 0,
top: '50%',
},
},
},
],
}).join('\n'),
).toMatchInlineSnapshot(`
.testClass {
opacity: 1;
top: 100%;
@starting-style {
opacity: 0;
top: 50%;
}
}
`);
});

it('should handle @starting-style inside media queries', () => {
expect(
transformCss({
composedClassLists: [],
localClassNames: ['testClass'],
cssObjs: [
{
type: 'local',
selector: 'testClass',
rule: {
display: 'flex',
'@media': {
'screen and (min-width: 700px)': {
top: '0',
'@starting-style': {
top: '100%',
},
},
},
},
},
],
}).join('\n'),
).toMatchInlineSnapshot(`
.testClass {
display: flex;
}
@media screen and (min-width: 700px) {
.testClass {
top: 0;
@starting-style {
top: 100%;
}
}
}
`);
});

it('should handle @starting-style inside container queries', () => {
expect(
transformCss({
composedClassLists: [],
localClassNames: ['testClass'],
cssObjs: [
{
type: 'local',
selector: 'testClass',
rule: {
'@container': {
'sidebar (min-width: 700px)': {
top: '0',
'@starting-style': {
top: '100%',
},
},
},
},
},
],
}).join('\n'),
).toMatchInlineSnapshot(`
@container sidebar (min-width: 700px) {
.testClass {
top: 0;
@starting-style {
top: 100%;
}
}
}
`);
});

it('should handle @starting-style inside a layer', () => {
expect(
transformCss({
composedClassLists: [],
localClassNames: ['testClass'],
cssObjs: [
{
type: 'local',
selector: 'testClass',
rule: {
'@layer': {
'mock-layer': {
top: '0',
'@starting-style': {
top: '100%',
},
},
},
},
},
],
}).join('\n'),
).toMatchInlineSnapshot(`
@layer mock-layer;
@layer mock-layer {
.testClass {
top: 0;
@starting-style {
top: 100%;
}
}
}
`);
});

it('should throw an error when a at-rule is use inside @starting-style scope', () => {
expect(() =>
transformCss({
composedClassLists: [],
localClassNames: ['testClass'],
cssObjs: [
{
type: 'local',
selector: 'testClass',
rule: {
'@starting-style': {
// @ts-expect-error - Using a media query inside @starting-style for testing purposes
'@media': {
'screen and (min-width: 700px)': {
display: 'grid',
},
},
},
},
},
],
}),
).toThrowErrorMatchingInlineSnapshot(
'Nested at-rules (e.g. "@media") are not allowed inside @starting-style.',
);
});
});

endFileScope();
62 changes: 62 additions & 0 deletions packages/css/src/transformCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ class Stylesheet {
this.transformMedia(root, root.rule['@media']);
this.transformSupports(root, root.rule['@supports']);
this.transformContainer(root, root.rule['@container']);
this.transformStartingStyle(root, root.rule['@starting-style']);

this.transformSimplePseudos(root, root.rule);
this.transformSelectors(root, root.rule);
Expand Down Expand Up @@ -408,6 +409,11 @@ class Stylesheet {
selectorRule['@container'],
conditions,
);
this.transformStartingStyle(
root,
selectorRule!['@starting-style'],
conditions,
);
Comment on lines +412 to +416
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This transformation inside selectors needs a unit test.

});
}

Expand Down Expand Up @@ -445,6 +451,11 @@ class Stylesheet {
this.transformLayer(root, mediaRule!['@layer'], conditions);
this.transformSupports(root, mediaRule!['@supports'], conditions);
this.transformContainer(root, mediaRule!['@container'], conditions);
this.transformStartingStyle(
root,
mediaRule!['@starting-style'],
conditions,
);
}
}
}
Expand Down Expand Up @@ -481,6 +492,11 @@ class Stylesheet {
this.transformLayer(root, containerRule!['@layer'], conditions);
this.transformSupports(root, containerRule!['@supports'], conditions);
this.transformMedia(root, containerRule!['@media'], conditions);
this.transformStartingStyle(
root,
containerRule!['@starting-style'],
conditions,
);
});
}
}
Expand Down Expand Up @@ -516,6 +532,11 @@ class Stylesheet {
this.transformMedia(root, layerRule!['@media'], conditions);
this.transformSupports(root, layerRule!['@supports'], conditions);
this.transformContainer(root, layerRule!['@container'], conditions);
this.transformStartingStyle(
root,
layerRule!['@starting-style'],
conditions,
);
});
}
}
Expand Down Expand Up @@ -550,6 +571,11 @@ class Stylesheet {
this.transformLayer(root, supportsRule!['@layer'], conditions);
this.transformMedia(root, supportsRule!['@media'], conditions);
this.transformContainer(root, supportsRule!['@container'], conditions);
this.transformStartingStyle(
root,
supportsRule!['@starting-style'],
conditions,
);
Comment on lines +574 to +578
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This transformation inside @supports needs a unit test.

});
}
}
Expand Down Expand Up @@ -589,6 +615,42 @@ class Stylesheet {
}
}

transformStartingStyle(
root: CSSStyleBlock | CSSSelectorBlock,
rules: WithQueries<StyleWithSelectors>['@starting-style'],
parentConditions: Array<string> = [],
) {
if (rules) {
// Check if there are any nested at-rule keys inside this block.
// The presence of any key starting with '@' indicates nested queries,
// which are not allowed for @starting-style.
Comment on lines +624 to +626
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just flagging that this is actually valid CSS. @media inside @starting-style is valid (example), but it relies on your browser supporting CSS nesting.

Actually, looking at the CSS output of your implementation, it generates nested CSS (@starting-style inside a selector). However, @starting-style is baseline, and it's newer than CSS nesting, so all browsers that support @starting-style support CSS nesting, so I think this is fine. However, I think for the sake of simplicity, disallowing further nested CSS within @starting-style is the right move for now. We can always remove this restriction in the future if necessary.

const nestedAtRuleKey = Object.keys(rules).find((key) =>
key.startsWith('@'),
);
if (nestedAtRuleKey) {
throw new Error(
`Nested at-rules (e.g. "${nestedAtRuleKey}") are not allowed inside @starting-style.`,
);
}

const conditions = [...parentConditions, '@starting-style'];

this.addConditionalRule(
{
selector: root.selector,
rule: omit(rules, specialKeys),
},
conditions,
);

// Process any simple pseudos or selectors associated with this style.
if (root.type === 'local') {
this.transformSimplePseudos(root, rules, conditions);
this.transformSelectors(root, rules, conditions);
}
Comment on lines +647 to +650
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should have a unit test too.

}
}

toCss() {
const css: Array<string> = [];

Expand Down
6 changes: 5 additions & 1 deletion packages/css/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,16 @@ export type MediaQueries<StyleType> = Query<'@media', StyleType>;
export type FeatureQueries<StyleType> = Query<'@supports', StyleType>;
export type ContainerQueries<StyleType> = Query<'@container', StyleType>;
export type Layers<StyleType> = Query<'@layer', StyleType>;
export type StartingStyleQueries<StyleType> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on the StartingStyleQueries name? While the Queries suffix aligns with most of the other types, it doesn't really make sense since @starting-style isn't a query like @media or @container. It's more similar to @layer in that it declares something. Maybe it should just be called StartingStyle?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good suggestion!

'@starting-style'?: Omit<StyleType, '@starting-style'>;
};

interface AllQueries<StyleType>
extends MediaQueries<StyleType & AllQueries<StyleType>>,
FeatureQueries<StyleType & AllQueries<StyleType>>,
ContainerQueries<StyleType & AllQueries<StyleType>>,
Layers<StyleType & AllQueries<StyleType>> {}
Layers<StyleType & AllQueries<StyleType>>,
StartingStyleQueries<StyleType> {}

export type WithQueries<StyleType> = StyleType & AllQueries<StyleType>;

Expand Down
2 changes: 1 addition & 1 deletion packages/parcel-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"author": "mattcompiles",
"license": "MIT",
"dependencies": {
"@parcel/plugin": "^2.7.0",
"@parcel/plugin": "^2.15.0",
"@vanilla-extract/integration": "workspace:^"
}
}
Loading