Styling
All the styling APIs in Vanilla Extract take a style object as input. Describing styles as a JavaScript object enables much better use of TypeScript through your styling code, as the styles are typed data-structures like the rest of your application code. It also brings type-safety and autocomplete to CSS authoring (via csstype).
CSS Properties
At the top-level of the style object, CSS properties can be set just like when writing a regular CSS class.
The only difference is all properties use camelCase
rather than kebab-case
.
import { style, globalStyle } from '@vanilla-extract/css';
export const myStyle = style({
display: 'flex',
paddingTop: '3px'
});
globalStyle('body', {
margin: 0
});
.app_myStyle__sznanj0 {
display: flex;
padding-top: 3px;
}
body {
margin: 0;
}
Unitless Properties
Some properties accept numbers as values. Excluding unitless properties, these values are assumed to be a pixel and px
is automatically appended to the value.
import { style } from '@vanilla-extract/css';
export const myStyle = style({
// cast to pixels
padding: 10,
marginTop: 25,
// unitless properties
flexGrow: 1,
opacity: 0.5
});
.styles_myStyle__1hiof570 {
padding: 10px;
margin-top: 25px;
flex-grow: 1;
opacity: 0.5;
}
Vendor Prefixes
If you want to target a vendor specific property (e.g. -webkit-tap-highlight-color
), you can do so using PascalCase
and removing the beginning -
.
import { style } from '@vanilla-extract/css';
export const myStyle = style({
WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)'
});
.styles_myStyle__1hiof570 {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
CSS Variables
In regular CSS, variables (or CSS custom properties) are able to be set alongside the other properties within the rule.
In Vanilla Extract, CSS variables must be nested within the vars
key — providing more accurate static typing for the other CSS properties.
import { style } from '@vanilla-extract/css';
const myStyle = style({
vars: {
'--my-global-variable': 'purple'
}
});
.styles_myStyle__1hiof570 {
--my-global-variable: purple;
}
The vars
key also accepts scoped CSS variables, created via the createVar API.
import { style, createVar } from '@vanilla-extract/css';
const myVar = createVar();
const myStyle = style({
vars: {
[myVar]: 'purple'
}
});
.styles_myStyle__1hiof571 {
--myVar__1hiof570: purple;
}
Media Queries
Unlike in regular CSS, Vanilla Extract lets you embed media queries within your style definitions using the @media
key.
This allows you to easily co-locate the responsive rules of a style into a single data-structure.
import { style } from '@vanilla-extract/css';
const myStyle = style({
'@media': {
'screen and (min-width: 768px)': {
padding: 10
},
'(prefers-reduced-motion)': {
transitionProperty: 'color'
}
}
});
@media screen and (min-width: 768px) {
.styles_myStyle__1hiof570 {
padding: 10px;
}
}
@media (prefers-reduced-motion) {
.styles_myStyle__1hiof570 {
transition-property: color;
}
}
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
,@supports
, and@container
condition blocks together to create the smallest possible CSS output.
Selectors
There are two methods of specifying selectors for a given style, simple pseudo selectors that can be used alongside all other CSS properties, and the selectors
option which allows construction of more complex rules.
🧠 All selectors are not available forglobalStyle
. This API accepts a selector as its first parameter (e.g.ul li:first-of-type, a > span
), merging selectors may produce unexpected results.
Simple Pseudo Selectors
Simple pseudo selectors are those that don’t take any parameters and therefore can be easily detected and statically typed. These can be used at the top level alongside the other CSS properties and can only contain CSS Properties and CSS Variables.
import { style } from '@vanilla-extract/css';
const myStyle = style({
':hover': {
color: 'pink'
},
':first-of-type': {
color: 'blue'
},
'::before': {
content: ''
}
});
.styles_myStyle__1hiof570:hover {
color: pink;
}
.styles_myStyle__1hiof570:first-of-type {
color: blue;
}
.styles_myStyle__1hiof570::before {
content: "";
}
Complex Selectors
More complex rules can be written using the selectors
key.
To improve maintainability, each style block can only target a single element. To enforce this, all selectors must target the &
character which is a reference to the current element.
import { style } from '@vanilla-extract/css';
const link = style({
selectors: {
'&:hover:not(:active)': {
border: '2px solid aquamarine'
},
'nav li > &': {
textDecoration: 'underline'
}
}
});
.styles_link__1hiof570:hover:not(:active) {
border: 2px solid aquamarine;
}
nav li > .styles_link__1hiof570 {
text-decoration: underline;
}
Selectors can also reference other scoped class names.
import { style } from '@vanilla-extract/css';
export const parent = style({});
export const child = style({
selectors: {
[`${parent}:focus &`]: {
background: '#fafafa'
}
}
});
.styles_parent__1hiof570:focus .styles_child__1hiof571 {
background: #fafafa;
}
Invalid selectors are those attempting to target an element other than the current class.
styles.css.tsimport { style } from '@vanilla-extract/css'; const invalid = style({ selectors: { // ❌ ERROR: Targetting `a[href]` '& a[href]': {...}, // ❌ ERROR: Targetting `.otherClass` '& ~ div > .otherClass': {...} } });
If you want to target another scoped class then it should be defined within the style block of that class instead.
styles.css.tsimport { style } from '@vanilla-extract/css'; // Invalid example: export const child = style({}); export const parent = style({ selectors: { // ❌ ERROR: Targetting `child` from `parent` [`& ${child}`]: {...} } }); // Valid example: export const parent = style({}); export const child = style({ selectors: { [`${parent} &`]: {...} } });
If you need to globally target child nodes within the current element (e.g. '& a[href]'
), you should use globalStyle instead.
import { style, globalStyle } from '@vanilla-extract/css';
export const parent = style({});
globalStyle(`${parent} a[href]`, {
color: 'pink'
});
.styles_parent__1hiof570 a[href] {
color: pink;
}
Circular Selectors
If your selectors are dependent on each other you can use getters to define them:
import { style } from '@vanilla-extract/css';
export const child = style({
background: 'blue',
get selectors() {
return {
[`${parent} &`]: {
color: 'red'
}
};
}
});
export const parent = style({
background: 'yellow',
selectors: {
[`&:has(${child})`]: {
padding: 10
}
}
});
.styles_child__1hiof570 {
background: blue;
}
.styles_parent__1hiof571 .styles_child__1hiof570 {
color: red;
}
.styles_parent__1hiof571 {
background: yellow;
}
.styles_parent__1hiof571:has(.styles_child__1hiof570) {
padding: 10px;
}
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.
import { style } from '@vanilla-extract/css';
const myStyle = style({
'@container': {
'(min-width: 768px)': {
padding: 10
}
}
});
@container (min-width: 768px) {
.styles_myStyle__1hiof570 {
padding: 10px;
}
}
You can also create scoped containers using createContainer.
import {
style,
createContainer
} from '@vanilla-extract/css';
const sidebar = createContainer();
const myStyle = style({
containerName: sidebar,
'@container': {
[`${sidebar} (min-width: 768px)`]: {
padding: 10
}
}
});
.styles_myStyle__1hiof571 {
container-name: styles_sidebar__1hiof570;
}
@container styles_sidebar__1hiof570 (min-width: 768px) {
.styles_myStyle__1hiof571 {
padding: 10px;
}
}
Layers
As with media queries above, Vanilla Extract lets you assign styles to layers by using the @layer
key within your style definition.
🚧 Ensure your target browsers support layers. Vanilla Extract supports the layers syntax but does not polyfill the feature in unsupported browsers.
import { style } from '@vanilla-extract/css';
const text = style({
'@layer': {
typography: {
fontSize: '1rem'
}
}
});
@layer typography;
@layer typography {
.styles_text__1hiof570 {
font-size: 1rem;
}
}
The @layer
key also accepts a scoped layer reference, created via the layer API.
import { style, layer } from '@vanilla-extract/css';
const typography = layer();
const text = style({
'@layer': {
[typography]: {
fontSize: '1rem'
}
}
});
@layer styles_typography__1hiof570;
@layer styles_typography__1hiof570 {
.styles_text__1hiof571 {
font-size: 1rem;
}
}
To learn more about managing layers, check out the API documentation for layer and globalLayer.
Supports Queries
Supports queries work the same as Media queries and are nested inside the @supports
key.
import { style } from '@vanilla-extract/css';
const myStyle = style({
'@supports': {
'(display: grid)': {
display: 'grid'
}
}
});
@supports (display: grid) {
.styles_myStyle__1hiof570 {
display: grid;
}
}
Fallback Styles
When using CSS property values that don’t exist in some browsers, you’ll often declare the property twice and the older browser will ignore the value it doesn’t understand. This isn’t possible using JS objects as you can’t declare the same key twice. So instead, we use an array to define fallback values.
import { style } from '@vanilla-extract/css';
export const myStyle = style({
// In Firefox and IE the "overflow: overlay" will be
// ignored and the "overflow: auto" will be applied
overflow: ['auto', 'overlay']
});
.styles_myStyle__1hiof570 {
overflow: auto;
overflow: overlay;
}