vanilla-extract

repository·master·Indexed 11 days ago

https://github.com/vanilla-extract-css/vanilla-extract

A zero-runtime CSS-in-TypeScript library that provides type-safe, locally scoped styling by generating static CSS files at build time. It features a theme system using CSS Variables and a utility-first approach via the @vanilla-extract/sprinkles package for creating responsive, atomic CSS classes.

Tokens
52.5K
Snippets
195
Records
218
Agent score
93%

What's inside vanilla-extract

  1. What is vanilla-extract?

    master

    vanilla-extract is a zero-runtime CSS-in-TypeScript library. It allows you to write styles in TypeScript or JavaScript files using locally scoped class names and CSS Variables. These styles are evaluated and transformed into static CSS files at build time, similar to how Sass or Less work, ensuring no styling logic is included in your final production JavaScript bundle.

    Key features include:

    • Zero-runtime: Styles are generated at build time.
    • Type-safe: Uses CSSType for style definitions.
    • Locally scoped: Generates scoped class names, CSS Variables, @keyframes, and @font-face rules.
    • Framework agnostic: Works with any front-end framework or vanilla JS.
    • Theme system: High-level theme support with no global namespace pollution.
  2. Understand CSS layer merging and precedence

    master

    Vanilla Extract attempts to generate the smallest possible CSS output by merging styles assigned to the same layer within the same file, provided it doesn't impact rule precedence.

    Crucially, the order of layers declared via globalLayer determines precedence. A style assigned to a layer declared later in the stylesheet will take precedence over styles in layers declared earlier, regardless of the order in which the style() functions were called in the code.

    In the following example, themedHeading will appear later in the CSS and take precedence over heading because the theme layer is declared after the base layer:

    import { style, globalLayer } from '@vanilla-extract/css';
    
    const base = globalLayer('base');
    const theme = globalLayer('theme');
    
    const text = style({
      '@layer': {
        [base]: { fontSize: '1rem' }
      }
    });
    
    const themedHeading = style({
      '@layer': {
        [theme]: { color: 'rebeccapurple' }
      }
    });
    
    const heading = style({
      '@layer': {
        [base]: { fontSize: '2.4rem' }
      }
    });
    import { style, globalLayer } from '@vanilla-extract/css';
    
    const base = globalLayer('base');
    const theme = globalLayer('theme');
    
    const text = style({
      '@layer': {
        [base]: {
          fontSize: '1rem'
        }
      }
    });
    const themedHeading = style({
      '@layer': {
        [theme]: {
          color: 'rebeccapurple'
        }
      }
    });
    const heading = style({
      '@layer': {
        [base]: {
          fontSize: '2.4rem'
        }
      }
    });
  3. Use unitless properties and automatic pixel conversion

    master

    For most properties, providing a number as a value will automatically append px. However, 'unitless properties' (like flexGrow or opacity) will accept the number without appending units.

    import { style } from '@vanilla-extract/css';
    
    export const myStyle = style({
      // cast to pixels
      padding: 10,
      marginTop: 25,
    
      // unitless properties
      flexGrow: 1,
      opacity: 0.5
    });
  4. Perform style calculations using CSS Variables

    master

    Since theme variables in vanilla-extract are opaque CSS Variables (e.g., var(--g7vce91)), you cannot perform arbitrary JavaScript calculations on them at runtime.

    Using calc()

    For simple arithmetic (addition, subtraction, multiplication, division), use the calc function from @vanilla-extract/css-utils to generate a valid CSS calc() expression.

    Advanced Calculations

    For complex logic (like color manipulation or rounding) that CSS cannot handle, you must perform the calculation at build time and include the result as a new CSS Variable in your theme definition.

    import { style } from '@vanilla-extract/css';
    import { calc } from '@vanilla-extract/css-utils';
    import { vars } from '../vars.css';
    
    // Simple calculation
    const className = style({
      marginTop: calc.negate(vars.space.small)
    });
    
    // Advanced: Hoist logic to theme definition
    // vars.css.ts
    export const vars = createGlobalTheme(':root', {
      color: {
        brand: 'blue',
        brandLight: lighten(0.2, 'blue') // Calculated at build time
      }
    });
    
    // usage.css.ts
    export const className = style({
      background: vars.color.brandLight
    });
  5. Compose styles using arrays in style()

    master

    Vanilla-extract allows you to compose styles by passing an array to the style() function. This array can contain existing class names (returned from other style() calls) and new style objects.

    When you compose styles, the resulting value is a space-separated string of class names (a classlist). This classlist can be used directly in the className property of DOM elements.

    import { style } from '@vanilla-extract/css';
    
    const base = style({ padding: 12 });
    
    // primary is a classlist: 'base_class_id primary_class_id'
    const primary = style([base, { background: 'blue' }]);
    
    const secondary = style([base, { background: 'aqua' }]);
  6. Use composed styles within selectors

    master

    When using a composed style inside a selector (such as within the selectors object of another style() call or in globalStyle), vanilla-extract automatically handles the composition.

    Internally, the composed classes are resolved to a single unique identifier. This allows you to treat a composed style as if it were a single class name when writing complex CSS selectors, ensuring that the selector targets the composed element correctly.

    import { style, globalStyle } from '@vanilla-extract/css';
    
    const background = style({ background: 'mintcream' });
    const padding = style({ padding: 12 });
    
    // container is a composed style
    export const container = style([background, padding]);
    
    // You can use the composed 'container' identifier in global selectors
    globalStyle(`${container} *`, {
      boxSizing: 'border-box'
    });
  7. How layer merging and precedence work

    master

    Vanilla Extract optimizes CSS output by merging styles assigned to the same layer within the same file, provided it doesn't impact rule precedence.

    Crucially, the order of styles in the final stylesheet is determined by the order in which the layers themselves are declared. A style assigned to a layer declared later in your code will appear later in the CSS, even if the style() call occurred earlier in the file.

    import { style, layer } from '@vanilla-extract/css';
    
    const base = layer();
    const theme = layer();
    
    // This appears earlier in the CSS because 'base' is declared first
    const text = style({
      '@layer': {
        [base]: {
          fontSize: '1rem'
        }
      }
    });
    
    // This appears later in the CSS because 'theme' is declared after 'base'
    const themedHeading = style({
      '@layer': {
        [theme]: {
          color: 'rebeccapurple'
        }
      }
    });
    
    // This appears after 'themedHeading' in the source, but because it belongs to 'base',
    // it is grouped with 'text' earlier in the stylesheet.
    const heading = style({
      '@layer': {
        [base]: {
          fontSize: '2.4rem'
        }
      }
    });
  8. How Sprinkles works with vanilla-extract selectors

    master

    Because Sprinkles returns a class list that behaves like a single class, you can use the result of a sprinkles() call directly within vanilla-extract's globalStyle or other selector functions.

    // styles.css.ts
    import { globalStyle } from '@vanilla-extract/css';
    import { sprinkles } from './sprinkles.css.ts';
    
    export const container = sprinkles({
      padding: 'small'
    });
    
    // Using the sprinkles class as a selector base
    globalStyle(`${container} *`, {
      boxSizing: 'border-box'
    });
  9. Enable CSS code-splitting for themes using createThemeContract

    master

    Standard createTheme usage couples the theme contract to a specific implementation, which can prevent CSS code-splitting because alternative themes must import the original theme's CSS.

    To decouple them, use createThemeContract. This allows you to define the shape of your theme (the contract) without generating any CSS. You then create individual theme files that implement this contract using createTheme. This way, importing a specific theme only imports its own CSS, enabling efficient code-splitting.

    // 1. Define the contract in contract.css.ts
    import { createThemeContract } from '@vanilla-extract/css';
    
    export const vars = createThemeContract({
      color: {
        brand: ''
      },
      font: {
        body: ''
      }
    });
    
    // 2. Implement specific themes in separate files
    // blueTheme.css.ts
    import { createTheme } from '@vanilla-extract/css';
    import { vars } from './contract.css.ts';
    
    export const blueThemeClass = createTheme(vars, {
      color: {
        brand: 'blue'
      },
      font: {
        body: 'arial'
      }
    });
    
    // redTheme.css.ts
    import { createTheme } from '@vanilla-extract/css';
    import { vars } from './contract.css.ts';
    
    export const redThemeClass = createTheme(vars, {
      color: {
        brand: 'red'
      },
      font: {
        body: 'helvetica'
      }
    });
  10. Configure Sprinkles with defineProperties and createSprinkles

    master

    To set up Sprinkles, create a sprinkles.css.ts file where you define your design tokens, properties, shorthands, and conditions. You then use createSprinkles to compose these definitions into a single sprinkles function.

    Key configuration concepts:

    • properties: Maps CSS properties to sets of allowed values (tokens).
    • shorthands: Allows creating composite properties (e.g., mapping paddingX to paddingLeft and paddingRight).
    • conditions: Defines media queries or state-based variations (e.g., mobile, darkMode).
    • defaultCondition: Specifies which condition to use when none is provided.

    Example configuration:

    import {
      defineProperties,
      createSprinkles
    } from '@vanilla-extract/sprinkles';
    
    const space = {
      none: 0,
      small: '4px',
      medium: '8px',
      large: '16px'
    };
    
    const responsiveProperties = defineProperties({
      conditions: {
        mobile: {},
        tablet: { '@media': 'screen and (min-width: 768px)' },
        desktop: { '@media': 'screen and (min-width: 1024px)' }
      },
      defaultCondition: 'mobile',
      properties: {
        display: ['none', 'flex', 'block', 'inline'],
        flexDirection: ['row', 'column'],
        justifyContent: ['stretch', 'flex-start', 'center', 'flex-end', 'space-around', 'space-between'],
        alignItems: ['stretch', 'flex-start', 'center', 'flex-end'],
        paddingTop: space,
        paddingBottom: space,
        paddingLeft: space,
        paddingRight: space
      },
      shorthands: {
        padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
        paddingX: ['paddingLeft', 'paddingRight'],
        paddingY: ['paddingTop', 'paddingBottom'],
        placeItems: ['justifyContent', 'alignItems']
      }
    });
    
    const colors = {
      'blue-50': '#eff6ff',
      'blue-100': '#dbeafe',
      'blue-200': '#bfdbfe',
      'gray-700': '#374151',
      'gray-800': '#1f2937',
      'gray-900': '#111827'
    };
    
    const colorProperties = defineProperties({
      conditions: {
        lightMode: {},
        darkMode: { '@media': '(prefers-color-scheme: dark)' }
      },
      defaultCondition: 'lightMode',
      properties: {
        color: colors,
        background: colors
      }
    });
    
    export const sprinkles = createSprinkles(
      responsiveProperties,
      colorProperties
    );
    
    // Export the Sprinkles type for better DX
    export type Sprinkles = Parameters<typeof sprinkles>[0];