diff --git a/.changeset/rare-numbers-pull.md b/.changeset/rare-numbers-pull.md
new file mode 100644
index 000000000..25fc34a8e
--- /dev/null
+++ b/.changeset/rare-numbers-pull.md
@@ -0,0 +1,5 @@
+---
+'@vanilla-extract/babel-plugin': minor
+---
+
+Add support for the new `createContainer` API
diff --git a/.changeset/real-cherries-give.md b/.changeset/real-cherries-give.md
new file mode 100644
index 000000000..fff3bf8a5
--- /dev/null
+++ b/.changeset/real-cherries-give.md
@@ -0,0 +1,28 @@
+---
+'@vanilla-extract/css': minor
+---
+
+Add `createContainer` API
+
+`createContainer` creates a single scoped container name for use with CSS Container Queries. This avoids potential naming collisions with other containers.
+
+```ts
+import {
+ style,
+ createContainer
+} from '@vanilla-extract/css';
+
+export const sidebarContainer = createContainer();
+
+export const sidebar = style({
+ containerName: sidebarContainer
+});
+
+export const navigation = style({
+ '@container': {
+ [`${sidebarContainer} (min-width: 400px)`]: {
+ display: 'flex'
+ }
+ }
+});
+```
\ No newline at end of file
diff --git a/.changeset/tiny-shirts-poke.md b/.changeset/tiny-shirts-poke.md
new file mode 100644
index 000000000..f64f846ec
--- /dev/null
+++ b/.changeset/tiny-shirts-poke.md
@@ -0,0 +1,17 @@
+---
+'@vanilla-extract/css': minor
+---
+
+Add support for container queries via the new `@container` key.
+
+```ts
+import { style } from '@vanilla-extract/css';
+
+export const myStyle = style({
+ '@container': {
+ '(min-width: 400px)': {
+ display: 'flex'
+ }
+ }
+});
+```
diff --git a/fixtures/low-level/src/index.ts b/fixtures/low-level/src/index.ts
index d31064023..05297f2ea 100644
--- a/fixtures/low-level/src/index.ts
+++ b/fixtures/low-level/src/index.ts
@@ -1,8 +1,10 @@
-import { block } from './styles.css';
+import { block, container } from './styles.css';
import testNodes from '../test-nodes.json';
document.body.innerHTML = `
+
`;
diff --git a/fixtures/low-level/src/styles.css.ts b/fixtures/low-level/src/styles.css.ts
index 957c3b322..42d743809 100644
--- a/fixtures/low-level/src/styles.css.ts
+++ b/fixtures/low-level/src/styles.css.ts
@@ -1,12 +1,28 @@
-import { style, createVar } from '@vanilla-extract/css';
+import { style, createVar, createContainer } from '@vanilla-extract/css';
const color = createVar();
+const myContainer = createContainer('my-container');
+
+export const container = style({
+ containerType: 'size',
+ containerName: myContainer,
+ width: 500,
+});
+
export const block = style({
vars: {
[color]: 'blue',
},
backgroundColor: color,
- color: 'white',
padding: 20,
+ '@media': {
+ 'screen and (min-width: 200px)': {
+ '@container': {
+ [`${myContainer} (min-width: 400px)`]: {
+ color: 'white',
+ },
+ },
+ },
+ },
});
diff --git a/packages/babel-plugin/src/index.test.ts b/packages/babel-plugin/src/index.test.ts
index 85655a084..4d84552bb 100644
--- a/packages/babel-plugin/src/index.test.ts
+++ b/packages/babel-plugin/src/index.test.ts
@@ -269,6 +269,25 @@ describe('babel plugin', () => {
`);
});
+ it('should handle createContainer assigned to const', () => {
+ const source = `
+ import { createContainer } from '@vanilla-extract/css';
+
+ const myContainer = createContainer();
+ `;
+
+ expect(transform(source)).toMatchInlineSnapshot(`
+ "import * as __vanilla_filescope__ from '@vanilla-extract/css/fileScope';
+
+ __vanilla_filescope__.setFileScope(\\"src/dir/mockFilename.css.ts\\", \\"@vanilla-extract/babel-plugin\\");
+
+ import { createContainer } from '@vanilla-extract/css';
+ const myContainer = createContainer(\\"myContainer\\");
+
+ __vanilla_filescope__.endFileScope();"
+ `);
+ });
+
it('should handle fontFace assigned to const', () => {
const source = `
import { fontFace } from '@vanilla-extract/css';
diff --git a/packages/babel-plugin/src/index.ts b/packages/babel-plugin/src/index.ts
index b2452c81c..3e2a19d2c 100644
--- a/packages/babel-plugin/src/index.ts
+++ b/packages/babel-plugin/src/index.ts
@@ -43,6 +43,9 @@ const debuggableFunctionConfig = {
recipe: {
maxParams: 2,
},
+ createContainer: {
+ maxParams: 1,
+ },
};
const styleFunctions = [
diff --git a/packages/css/src/container.ts b/packages/css/src/container.ts
new file mode 100644
index 000000000..7760f987c
--- /dev/null
+++ b/packages/css/src/container.ts
@@ -0,0 +1,6 @@
+import { generateIdentifier } from './identifier';
+
+// createContainer is used for local scoping of CSS containers
+// For now it is mostly just an alias of generateIdentifier
+export const createContainer = (debugId?: string) =>
+ generateIdentifier(debugId);
diff --git a/packages/css/src/index.ts b/packages/css/src/index.ts
index 2cc5e58c8..57ae91eab 100644
--- a/packages/css/src/index.ts
+++ b/packages/css/src/index.ts
@@ -12,3 +12,4 @@ export * from './identifier';
export * from './theme';
export * from './style';
export * from './vars';
+export { createContainer } from './container';
diff --git a/packages/css/src/transformCss.test.ts b/packages/css/src/transformCss.test.ts
index 5adc2ec99..35ac38bbf 100644
--- a/packages/css/src/transformCss.test.ts
+++ b/packages/css/src/transformCss.test.ts
@@ -961,7 +961,41 @@ describe('transformCss', () => {
`);
});
- it('should handle nested @supports and @media queries', () => {
+ it('should handle @container queries', () => {
+ expect(
+ transformCss({
+ composedClassLists: [],
+ localClassNames: ['testClass'],
+ cssObjs: [
+ {
+ type: 'local',
+ selector: 'testClass',
+ rule: {
+ display: 'flex',
+ containerName: 'sidebar',
+ '@container': {
+ 'sidebar (min-width: 700px)': {
+ display: 'grid',
+ },
+ },
+ },
+ },
+ ],
+ }).join('\n'),
+ ).toMatchInlineSnapshot(`
+ ".testClass {
+ display: flex;
+ container-name: sidebar;
+ }
+ @container sidebar (min-width: 700px) {
+ .testClass {
+ display: grid;
+ }
+ }"
+ `);
+ });
+
+ it('should handle nested @supports, @media and @container queries', () => {
expect(
transformCss({
composedClassLists: [],
@@ -978,16 +1012,27 @@ describe('transformCss', () => {
'@media': {
'screen and (min-width: 700px)': {
display: 'grid',
+ '@container': {
+ 'sidebar (min-width: 700px)': {
+ display: 'grid',
+ },
+ },
},
},
},
},
+
'@media': {
'screen and (min-width: 700px)': {
color: 'green',
'@supports': {
'(display: grid)': {
borderColor: 'blue',
+ '@container': {
+ 'sidebar (min-width: 700px)': {
+ display: 'grid',
+ },
+ },
},
},
},
@@ -1008,6 +1053,11 @@ describe('transformCss', () => {
.testClass {
border-color: blue;
}
+ @container sidebar (min-width: 700px) {
+ .testClass {
+ display: grid;
+ }
+ }
}
}
@supports (display: grid) {
@@ -1018,12 +1068,17 @@ describe('transformCss', () => {
.testClass {
display: grid;
}
+ @container sidebar (min-width: 700px) {
+ .testClass {
+ display: grid;
+ }
+ }
}
}"
`);
});
- it('should merge nested @supports and @media queries', () => {
+ it('should merge nested @supports, @media and @container queries', () => {
expect(
transformCss({
composedClassLists: [],
@@ -1038,12 +1093,18 @@ describe('transformCss', () => {
'@supports': {
'(display: grid)': {
borderColor: 'blue',
+ '@container': {
+ 'sidebar (min-width: 700px)': {
+ display: 'grid',
+ },
+ },
},
},
},
},
},
},
+
{
type: 'local',
selector: 'otherClass',
@@ -1053,6 +1114,11 @@ describe('transformCss', () => {
'@supports': {
'(display: grid)': {
backgroundColor: 'yellow',
+ '@container': {
+ 'sidebar (min-width: 700px)': {
+ display: 'grid',
+ },
+ },
},
},
},
@@ -1070,6 +1136,14 @@ describe('transformCss', () => {
.otherClass {
background-color: yellow;
}
+ @container sidebar (min-width: 700px) {
+ .testClass {
+ display: grid;
+ }
+ .otherClass {
+ display: grid;
+ }
+ }
}
}"
`);
diff --git a/packages/css/src/transformCss.ts b/packages/css/src/transformCss.ts
index 4deae29fb..77557c9dd 100644
--- a/packages/css/src/transformCss.ts
+++ b/packages/css/src/transformCss.ts
@@ -7,13 +7,12 @@ import type {
CSSStyleBlock,
CSSKeyframesBlock,
CSSPropertiesWithVars,
- FeatureQueries,
- MediaQueries,
StyleRule,
StyleWithSelectors,
GlobalFontFaceRule,
CSSSelectorBlock,
Composition,
+ WithQueries,
} from './types';
import { markCompositionUsed } from './adapter';
import { forEach, omit, mapKeys } from './utils';
@@ -81,7 +80,13 @@ function dashify(str: string) {
const DOUBLE_SPACE = ' ';
-const specialKeys = [...simplePseudos, '@media', '@supports', 'selectors'];
+const specialKeys = [
+ ...simplePseudos,
+ '@media',
+ '@supports',
+ '@container',
+ 'selectors',
+];
interface CSSRule {
conditions?: Array;
@@ -144,6 +149,7 @@ class Stylesheet {
this.transformMedia(root, root.rule['@media']);
this.transformSupports(root, root.rule['@supports']);
+ this.transformContainer(root, root.rule['@container']);
this.transformSimplePseudos(root, root.rule);
this.transformSelectors(root, root.rule);
@@ -318,9 +324,7 @@ class Stylesheet {
transformMedia(
root: CSSStyleBlock | CSSSelectorBlock,
- rules:
- | MediaQueries>
- | undefined,
+ rules: WithQueries['@media'],
parentConditions: Array = [],
) {
if (rules) {
@@ -350,15 +354,49 @@ class Stylesheet {
}
this.transformSupports(root, mediaRule!['@supports'], conditions);
+ this.transformContainer(root, mediaRule!['@container'], conditions);
+ });
+ }
+ }
+
+ transformContainer(
+ root: CSSStyleBlock | CSSSelectorBlock,
+ rules: WithQueries['@container'],
+ parentConditions: Array = [],
+ ) {
+ if (rules) {
+ this.currConditionalRuleset?.addConditionPrecedence(
+ parentConditions,
+ Object.keys(rules).map((query) => `@container ${query}`),
+ );
+
+ forEach(rules, (containerRule, query) => {
+ const containerQuery = `@container ${query}`;
+
+ const conditions = [...parentConditions, containerQuery];
+
+ this.addConditionalRule(
+ {
+ selector: root.selector,
+ rule: omit(containerRule, specialKeys),
+ },
+ conditions,
+ );
+
+ if (root.type === 'local') {
+ this.transformSimplePseudos(root, containerRule!, conditions);
+ this.transformSelectors(root, containerRule!, conditions);
+ }
+
+ this.transformSupports(root, containerRule!['@supports'], conditions);
+ this.transformMedia(root, containerRule!['@media'], conditions);
});
}
}
transformSupports(
root: CSSStyleBlock | CSSSelectorBlock,
- rules:
- | FeatureQueries>
- | undefined,
+ rules: WithQueries['@supports'],
parentConditions: Array = [],
) {
if (rules) {
@@ -383,6 +421,7 @@ class Stylesheet {
this.transformSelectors(root, supportsRule!, conditions);
}
this.transformMedia(root, supportsRule!['@media'], conditions);
+ this.transformContainer(root, supportsRule!['@container'], conditions);
});
}
}
diff --git a/packages/css/src/types.ts b/packages/css/src/types.ts
index c366db779..5f0038483 100644
--- a/packages/css/src/types.ts
+++ b/packages/css/src/types.ts
@@ -1,15 +1,25 @@
import type { MapLeafNodes, CSSVarFunction } from '@vanilla-extract/private';
-import type { PropertiesFallback, AtRule, Properties } from 'csstype';
+import type { Properties, AtRule } from 'csstype';
import type { SimplePseudos } from './simplePseudos';
-type CSSTypeProperties = PropertiesFallback;
+// csstype is yet to ship container property types as they are not in
+// the output MDN spec files yet. Remove this once that's done.
+// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries
+interface ContainerProperties {
+ container?: string;
+ containerType?: 'size' | 'inline-size' | (string & {});
+ containerName?: string;
+}
+
+type CSSTypeProperties = Properties &
+ ContainerProperties;
export type CSSProperties = {
[Property in keyof CSSTypeProperties]:
| CSSTypeProperties[Property]
| CSSVarFunction
- | Array;
+ | Array;
};
export interface CSSKeyframes {
@@ -28,14 +38,6 @@ type PseudoProperties = {
type CSSPropertiesAndPseudos = CSSPropertiesWithVars & PseudoProperties;
-interface SelectorMap {
- [selector: string]: CSSPropertiesWithVars &
- MediaQueries<
- CSSPropertiesWithVars & FeatureQueries
- > &
- FeatureQueries>;
-}
-
export interface MediaQueries {
'@media'?: {
[query: string]: StyleType;
@@ -48,17 +50,41 @@ export interface FeatureQueries {
};
}
+export interface ContainerQueries {
+ '@container'?: {
+ [query: string]: StyleType;
+ };
+}
+
+export type WithQueries = MediaQueries<
+ StyleType &
+ FeatureQueries> &
+ ContainerQueries>
+> &
+ FeatureQueries<
+ StyleType &
+ MediaQueries> &
+ ContainerQueries>
+ > &
+ ContainerQueries<
+ StyleType &
+ MediaQueries> &
+ FeatureQueries>
+ >;
+
+interface SelectorMap {
+ [selector: string]: CSSPropertiesWithVars &
+ WithQueries;
+}
+
export interface StyleWithSelectors extends CSSPropertiesAndPseudos {
selectors?: SelectorMap;
}
-export type StyleRule = StyleWithSelectors &
- MediaQueries> &
- FeatureQueries>;
+export type StyleRule = StyleWithSelectors & WithQueries;
export type GlobalStyleRule = CSSPropertiesWithVars &
- MediaQueries> &
- FeatureQueries>;
+ WithQueries;
export type GlobalFontFaceRule = Omit &
Required>;
diff --git a/site/contents.js b/site/contents.js
index d316a09ba..98ff9dffc 100644
--- a/site/contents.js
+++ b/site/contents.js
@@ -23,6 +23,7 @@ const contents = [
'assign-vars',
'font-face',
'keyframes',
+ 'create-container',
],
},
{
diff --git a/site/docs/api/create-container.md b/site/docs/api/create-container.md
new file mode 100644
index 000000000..b302d7475
--- /dev/null
+++ b/site/docs/api/create-container.md
@@ -0,0 +1,43 @@
+---
+title: createContainer
+parent: api
+---
+
+# createContainer
+
+Creates a single scoped container name for use with [CSS Container Queries]. This avoids potential naming collisions with other containers.
+
+> 🚧 Ensure your target browsers [support container queries].
+> Vanilla-extract supports the [container query syntax][css container queries] but does not polyfill the feature in unsupported browsers.
+
+```ts compiled
+// sidebar.css.ts
+import {
+ style,
+ createContainer
+} from '@vanilla-extract/css';
+
+export const sidebarContainer = createContainer();
+
+export const sidebar = style({
+ containerName: sidebarContainer
+});
+
+// navigation.css.ts
+import {
+ style,
+ createContainer
+} from '@vanilla-extract/css';
+import { sidebarContainer } from './sidebar.css.ts';
+
+export const navigation = style({
+ '@container': {
+ [`${sidebarContainer} (min-width: 400px)`]: {
+ display: 'flex'
+ }
+ }
+});
+```
+
+[css container queries]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries
+[support container queries]: https://caniuse.com/css-container-queries
diff --git a/site/docs/overview/styling.md b/site/docs/overview/styling.md
index 15b840f0b..3e0709516 100644
--- a/site/docs/overview/styling.md
+++ b/site/docs/overview/styling.md
@@ -114,7 +114,7 @@ const myStyle = style({
When processing your code into CSS, vanilla-extract will always render your media queries **at the end of the file**. This means styles inside the `@media` key will always have higher precedence than other styles due to CSS rule order precedence.
-> 🧠When it's safe to do so, vanilla-extract will merge your `@media` (and `@supports`) condition blocks together to create the smallest possible CSS output.
+> 🧠When it's safe to do so, vanilla-extract will merge your `@media`, `@supports`, and `@container` condition blocks together to create the smallest possible CSS output.
## Selectors
@@ -237,6 +237,46 @@ globalStyle(`${parent} a[href]`, {
});
```
+## Container Queries
+
+Container queries work the same as [media queries] and are nested inside the `@container` key.
+
+> 🚧 Ensure your target browsers [support container queries]. Vanilla-extract supports the [container query syntax] but does not polyfill the feature in unsupported browsers.
+
+```ts compiled
+// styles.css.ts
+import { style } from '@vanilla-extract/css';
+
+const myStyle = style({
+ '@container': {
+ '(min-width: 768px)': {
+ padding: 10
+ }
+ }
+});
+```
+
+You can also create scoped containers using [createContainer].
+
+```ts compiled
+// styles.css.ts
+import {
+ style,
+ createContainer
+} from '@vanilla-extract/css';
+
+const sidebar = createContainer();
+
+const myStyle = style({
+ containerName: sidebar,
+ '@container': {
+ [`${sidebar} (min-width: 768px)`]: {
+ padding: 10
+ }
+ }
+});
+```
+
## Supports Queries
Supports queries work the same as [Media queries] and are nested inside the `@supports` key.
@@ -275,7 +315,10 @@ export const myStyle = style({
[csstype]: https://github.com/frenic/csstype
[unitless properties]: https://github.com/seek-oss/vanilla-extract/blob/6068246343ceb58a04006f4ce9d9ff7ecc7a6c09/packages/css/src/transformCss.ts#L25
[createvar]: /documentation/api/create-var/
+[createcontainer]: /documentation/api/create-container/
[css properties]: #css-properties
[css variables]: #css-variables
[globalstyle]: /documentation/global-api/global-style
[media queries]: #media-queries
+[support container queries]: https://caniuse.com/css-container-queries
+[container query syntax]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries
diff --git a/site/src/HomePage/HomePage.tsx b/site/src/HomePage/HomePage.tsx
index 9ac710af8..b79a7ff29 100644
--- a/site/src/HomePage/HomePage.tsx
+++ b/site/src/HomePage/HomePage.tsx
@@ -316,8 +316,8 @@ export const HomePage = () => {
Write maintainable CSS at scale without sacrificing platform
features. Variables, selectors, pseudo‑classes,
- media/feature queries, keyframes, font‑face rules and
- global styles are all supported.
+ media/feature/container queries, keyframes, font‑face
+ and global styles are all supported.
diff --git a/tests/stylesheets/__snapshots__/low-level.test.ts.snap b/tests/stylesheets/__snapshots__/low-level.test.ts.snap
new file mode 100644
index 000000000..cc3c2ecad
--- /dev/null
+++ b/tests/stylesheets/__snapshots__/low-level.test.ts.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`low-level - esbuild should create valid stylesheet 1`] = `
+"._1amv5mo2 {
+ container-type: size;
+ container-name: _1amv5mo1;
+ width: 500px;
+}
+._1amv5mo3 {
+ --_1amv5mo0: blue;
+ background-color: var(--_1amv5mo0);
+ padding: 20px;
+}
+@media screen and (min-width: 200px) {
+ @container _1amv5mo1 (min-width: 400px) {
+ ._1amv5mo3 {
+ color: #fff;
+ }
+ }
+}
+"
+`;
+
+exports[`low-level - mini-css-extract should create valid stylesheet 1`] = `
+".cj5d032 {
+ container-type: size;
+ container-name: cj5d031;
+ width: 500px;
+}
+.cj5d033 {
+ --cj5d030: blue;
+ background-color: var(--cj5d030);
+ padding: 20px;
+}
+@media screen and (min-width: 200px) {
+ @container cj5d031 (min-width: 400px) {
+ .cj5d033 {
+ color: #fff;
+ }
+ }
+}
+"
+`;
+
+exports[`low-level - vite should create valid stylesheet 1`] = `
+"._1amv5mo2 {
+ container-type: size;
+ container-name: _1amv5mo1;
+ width: 500px;
+}
+._1amv5mo3 {
+ --_1amv5mo0: blue;
+ background-color: var(--_1amv5mo0);
+ padding: 20px;
+}
+@media screen and (min-width: 200px) {
+ @container _1amv5mo1 (min-width: 400px) {
+ ._1amv5mo3 {
+ color: #fff;
+ }
+ }
+}
+"
+`;
diff --git a/tests/stylesheets/low-level.test.ts b/tests/stylesheets/low-level.test.ts
new file mode 100644
index 000000000..4b2f88bff
--- /dev/null
+++ b/tests/stylesheets/low-level.test.ts
@@ -0,0 +1,36 @@
+import {
+ getStylesheet,
+ startFixture,
+ TestServer,
+} from '@vanilla-extract-private/test-helpers';
+
+const workerIndex = parseInt(process.env.JEST_WORKER_ID ?? '', 10);
+let testCounter = 0;
+
+const buildTypes = ['vite', 'esbuild', 'mini-css-extract'] as const;
+
+buildTypes.forEach((buildType) => {
+ describe(`low-level - ${buildType}`, () => {
+ let server: TestServer;
+
+ beforeAll(async () => {
+ const portRange = 100 * workerIndex;
+
+ server = await startFixture('low-level', {
+ type: buildType,
+ mode: 'production',
+ basePort: 12000 + portRange + testCounter++,
+ });
+ });
+
+ test('should create valid stylesheet', async () => {
+ expect(
+ await getStylesheet(server.url, server.stylesheet),
+ ).toMatchSnapshot();
+ });
+
+ afterAll(async () => {
+ await server.close();
+ });
+ });
+});