Giter Club home page Giter Club logo

vanilla-extract's Introduction

๐Ÿง vanilla-extract

Zero-runtime Stylesheets-in-TypeScript.

Write your styles in TypeScript (or JavaScript) with locally scoped class names and CSS Variables, then generate static CSS files at build time.

Basically, itโ€™s โ€œCSS Modules-in-TypeScriptโ€ but with scoped CSS Variables + heaps more.

๐Ÿ”ฅ ย  All styles generated at build time โ€” just like Sass, Less, etc.

โœจ ย  Minimal abstraction over standard CSS.

๐Ÿฆ„ ย  Works with any front-end framework โ€” or even without one.

๐ŸŒณ ย  Locally scoped class names โ€” just like CSS Modules.

๐Ÿš€ ย  Locally scoped CSS Variables, @keyframes and @font-face rules.

๐ŸŽจ ย  High-level theme system with support for simultaneous themes. No globals!

๐Ÿ›  ย  Utils for generating variable-based calc expressions.

๐Ÿ’ช ย  Type-safe styles via CSSType.

๐Ÿƒโ€โ™‚๏ธ ย  Optional runtime version for development and testing.

๐Ÿ™ˆ ย  Optional API for dynamic runtime theming.


๐Ÿ–ฅ ย  Try it out for yourself in CodeSandbox.


Write your styles in .css.ts files.

// styles.css.ts

import { createTheme, style } from '@vanilla-extract/css';

export const [themeClass, vars] = createTheme({
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

export const exampleStyle = style({
  backgroundColor: vars.color.brand,
  fontFamily: vars.font.body,
  color: 'white',
  padding: 10
});

๐Ÿ’ก These .css.ts files will be evaluated at build time. None of the code in these files will be included in your final bundle. Think of it as using TypeScript as your preprocessor instead of Sass, Less, etc.

Then consume them in your markup.

// app.ts

import { themeClass, exampleStyle } from './styles.css.ts';

document.write(`
  <section class="${themeClass}">
    <h1 class="${exampleStyle}">Hello world!</h1>
  </section>
`);

Want to work at a higher level while maximising style re-use? Check out ๐Ÿจ Sprinkles, our official zero-runtime atomic CSS framework, built on top of vanilla-extract.



Setup

There are currently a few integrations to choose from.

webpack

  1. Install the dependencies.
npm install @vanilla-extract/css @vanilla-extract/babel-plugin @vanilla-extract/webpack-plugin
  1. Add the Babel plugin.
{
  "plugins": ["@vanilla-extract/babel-plugin"]
}
  1. Add the webpack plugin.
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin');

module.exports = {
  plugins: [new VanillaExtractPlugin()],
};
You'll need to ensure you're handling CSS files in your webpack config.
For example:
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new VanillaExtractPlugin(),
    new MiniCssExtractPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.vanilla\.css$/i, // Targets only CSS files generated by vanilla-extract
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: require.resolve('css-loader'),
            options: {
              url: false // Required as image imports should be handled via JS/TS import statements
            }
          }
        ]
      }
    ]
  }
};

esbuild

  1. Install the dependencies.
npm install @vanilla-extract/css @vanilla-extract/esbuild-plugin
  1. Add the esbuild plugin to your build script.
const { vanillaExtractPlugin } = require('@vanilla-extract/esbuild-plugin');

require('esbuild').build({
  entryPoints: ['app.ts'],
  bundle: true,
  plugins: [vanillaExtractPlugin()],
  outfile: 'out.js',
}).catch(() => process.exit(1))

Please note: There are currently no automatic readable class names during development. However, you can still manually provide a debug ID as the last argument to functions that generate scoped styles, e.g. export const className = style({ ... }, 'className');

  1. Process CSS

As esbuild currently doesn't have a way to process the CSS generated by vanilla-extract, you can optionally use the processCss option.

For example, to run autoprefixer over the generated CSS.

const {
  vanillaExtractPlugin
} = require('@vanilla-extract/esbuild-plugin');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');

async function processCss(css) {
  const result = await postcss([autoprefixer]).process(
    css,
    {
      from: undefined /* suppress source map warning */
    }
  );

  return result.css;
}

require('esbuild')
  .build({
    entryPoints: ['app.ts'],
    bundle: true,
    plugins: [
      vanillaExtractPlugin({
        processCss
      })
    ],
    outfile: 'out.js'
  })
  .catch(() => process.exit(1));

Vite

Warning: Currently the Vite plugin doesn't rebuild files when dependent files change, e.g. updating theme.css.ts should rebuild styles.css.ts which imports theme.css.ts. This is a limitation in the Vite Plugin API that will hopefully be resolved soon. You can track the Vite issue here: vitejs/vite#3216

  1. Install the dependencies.
npm install @vanilla-extract/css @vanilla-extract/vite-plugin
  1. Add the Vite plugin to your Vite config.
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';

// vite.config.js
export default {
  plugins: [vanillaExtractPlugin()]
}

Please note: There are currently no automatic readable class names during development. However, you can still manually provide a debug ID as the last argument to functions that generate scoped styles, e.g. export const className = style({ ... }, 'className');

Snowpack

  1. Install the dependencies.
npm install @vanilla-extract/css @vanilla-extract/snowpack-plugin
  1. Add the Snowpack plugin to your snowpack config.
// snowpack.config.json
{
  "plugins": ["@vanilla-extract/snowpack-plugin"]
}

Please note: There are currently no automatic readable class names during development. However, you can still manually provide a debug ID as the last argument to functions that generate scoped styles, e.g. export const className = style({ ... }, 'className');

Gatsby

To add to your Gatsby site, use the gatsby-plugin-vanilla-extract plugin.

Test environments

  1. Install the dependencies.
$ npm install @vanilla-extract/babel-plugin
  1. Add the Babel plugin.
{
  "plugins": ["@vanilla-extract/babel-plugin"]
}
  1. Disable runtime styles (Optional)

In testing environments (like jsdom) vanilla-extract will create and insert styles. While this is often desirable, it can be a major slowdown in your tests. If your tests donโ€™t require styles to be available, the disableRuntimeStyles import will disable all style creation.

// setupTests.ts
import '@vanilla-extract/css/disableRuntimeStyles';

Styling API

๐Ÿฌ If you're a treat user, check out our migration guide.

style

Creates styles attached to a locally scoped class name.

import { style } from '@vanilla-extract/css';

export const className = style({
  display: 'flex'
});

CSS Variables, simple pseudos, selectors and media/feature queries are all supported.

import { style } from '@vanilla-extract/css';
import { vars } from './vars.css.ts';

export const className = style({
  display: 'flex',
  vars: {
    [vars.localVar]: 'green',
    '--global-variable': 'purple'
  },
  ':hover': {
    color: 'red'
  },
  selectors: {
    '&:nth-child(2n)': {
      background: '#fafafa'
    }
  },
  '@media': {
    'screen and (min-width: 768px)': {
      padding: 10
    }
  },
  '@supports': {
    '(display: grid)': {
      display: 'grid'
    }
  }
});

Selectors can also contain references to other scoped class names.

import { style } from '@vanilla-extract/css';

export const parentClass = style({});

export const childClass = style({
  selectors: {
    [`${parentClass}:focus &`]: {
      background: '#fafafa'
    }
  },
});

๐Ÿ’ก 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. For example, '&:hover:not(:active)' is considered valid, while '& > a' and [`& ${childClass}`] are not.

If you want to target another scoped class then it should be defined within the style block of that class instead. For example, [`& ${childClass}`] is invalid since it targets ${childClass}, so it should instead be defined in the style block for childClass.

If you want to globally target child nodes within the current element (e.g. '& > a'), you should use globalStyle instead.

styleVariants

Creates a collection of named style variants.

import { styleVariants } from '@vanilla-extract/css';

export const variant = styleVariants({
  primary: { background: 'blue' },
  secondary: { background: 'aqua' },
});

๐Ÿ’ก This is useful for mapping component props to styles, e.g. <button className={styles.variant[props.variant]}>

You can also transform the values by providing a map function as the second argument.

import { styleVariants } from '@vanilla-extract/css';

const spaceScale = {
  small: 4,
  medium: 8,
  large: 16
};

export const padding = styleVariants(spaceScale, (space) => ({
  padding: space
}));

globalStyle

Creates styles attached to a global selector.

import { globalStyle } from '@vanilla-extract/css';

globalStyle('html, body', {
  margin: 0
});

Global selectors can also contain references to other scoped class names.

import { globalStyle } from '@vanilla-extract/css';

export const parentClass = style({});

globalStyle(`${parentClass} > a`, {
  color: 'pink'
});

composeStyles

Combines multiple styles into a single class string, while also deduplicating and removing unnecessary spaces.

import { style, composeStyles } from '@vanilla-extract/css';

const button = style({
  padding: 12,
  borderRadius: 8
});

export const primaryButton = composeStyles(
  button,
  style({ background: 'coral' })
);

export const secondaryButton = composeStyles(
  button,
  style({ background: 'peachpuff' })
);

๐Ÿ’ก Styles can also be provided in shallow and deeply nested arrays, similar to classnames.

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.

import {
  style,
  globalStyle,
  composeStyles
} from '@vanilla-extract/css';

const background = style({ background: 'mintcream' });
const padding = style({ padding: 12 });

export const container = composeStyles(background, padding);

globalStyle(`${container} *`, {
  boxSizing: 'border-box'
});

createTheme

Creates a locally scoped theme class and a theme contract which can be consumed within your styles.

// theme.css.ts

import { createTheme } from '@vanilla-extract/css';

export const [themeClass, vars] = createTheme({
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

You can create theme variants by passing a theme contract as the first argument to createTheme.

// themes.css.ts

import { createTheme } from '@vanilla-extract/css';

export const [themeA, vars] = createTheme({
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

export const themeB = createTheme(vars, {
  color: {
    brand: 'pink'
  },
  font: {
    body: 'comic sans ms'
  }
});

๐Ÿ’ก All theme variants must provide a value for every variable or itโ€™s a type error.

createGlobalTheme

Creates a theme attached to a global selector, but with locally scoped variable names.

// theme.css.ts

import { createGlobalTheme } from '@vanilla-extract/css';

export const vars = createGlobalTheme(':root', {
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

๐Ÿ’ก All theme variants must provide a value for every variable or itโ€™s a type error.

createThemeContract

Creates a contract for themes to implement.

Ensure this function is called within a .css.ts context, otherwise variable names will be mismatched between themes.

๐Ÿ’ก This is useful if you want to split your themes into different bundles. In this case, your themes would be defined in separate files, but we'll keep this example simple.

// themes.css.ts

import {
  createThemeContract,
  createTheme
} from '@vanilla-extract/css';

export const vars = createThemeContract({
  color: {
    brand: null
  },
  font: {
    body: null
  }
});

export const themeA = createTheme(vars, {
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

export const themeB = createTheme(vars, {
  color: {
    brand: 'pink'
  },
  font: {
    body: 'comic sans ms'
  }
});

assignVars

Assigns a collection of CSS Variables anywhere within a style block.

๐Ÿ’ก This is useful for creating responsive themes since it can be used within @media blocks.

import { createThemeContract, style, assignVars } from '@vanilla-extract/css';

export const vars = createThemeContract({
  space: {
    small: null,
    medium: null,
    large: null
  }
});

export const responsiveSpaceTheme = style({
  vars: assignVars(vars.space, {
    small: '4px',
    medium: '8px',
    large: '16px'
  }),
  '@media': {
    'screen and (min-width: 1024px)': {
      vars: assignVars(vars.space, {
        small: '8px',
        medium: '16px',
        large: '32px'
      })
    }
  }
});

๐Ÿ’ก All variables passed into this function must be assigned or itโ€™s a type error.

createVar

Creates a single CSS Variable.

import { createVar, style } from '@vanilla-extract/css';

export const colorVar = createVar();

export const exampleStyle = style({
  color: colorVar
});

Scoped variables can be set via the vars property on style objects.

import { createVar, style } from '@vanilla-extract/css';
import { colorVar } from './vars.css.ts';

export const parentStyle = style({
  vars: {
    [colorVar]: 'blue'
  }
});

fallbackVar

Provides fallback values when consuming variables.

import { createVar, fallbackVar, style } from '@vanilla-extract/css';

export const colorVar = createVar();

export const exampleStyle = style({
  color: fallbackVar(colorVar, 'blue');
});

Multiple fallbacks are also supported.

import { createVar, fallbackVar, style } from '@vanilla-extract/css';

export const primaryColorVar = createVar();
export const secondaryColorVar = createVar();

export const exampleStyle = style({
  color: fallbackVar(primaryColorVar, secondaryColorVar, 'blue');
});

fontFace

Creates a custom font attached to a locally scoped font name.

import { fontFace, style } from '@vanilla-extract/css';

const myFont = fontFace({
  src: 'local("Comic Sans MS")'
});

export const text = style({
  fontFamily: myFont
});

globalFontFace

Creates a globally scoped custom font.

import {
  globalFontFace,
  style
} from '@vanilla-extract/css';

globalFontFace('MyGlobalFont', {
  src: 'local("Comic Sans MS")'
});

export const text = style({
  fontFamily: 'MyGlobalFont'
});

keyframes

Creates a locally scoped set of keyframes.

import { keyframes, style } from '@vanilla-extract/css';

const rotate = keyframes({
  '0%': { rotate: '0deg' },
  '100%': { rotate: '360deg' },
});

export const animated = style({
  animation: `3s infinite ${rotate}`,
});

globalKeyframes

Creates a globally scoped set of keyframes.

import { globalKeyframes, style } from '@vanilla-extract/css';

globalKeyframes('rotate', {
  '0%': { rotate: '0deg' },
  '100%': { rotate: '360deg' },
});

export const animated = style({
  animation: `3s infinite rotate`,
});

Dynamic API

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

npm install @vanilla-extract/dynamic

createInlineTheme

Implements a theme contract at runtime as an inline style object.

import { createInlineTheme } from '@vanilla-extract/dynamic';
import { vars, exampleStyle } from './styles.css.ts';

const customTheme = createInlineTheme(vars, {
  small: '4px',
  medium: '8px',
  large: '16px'
});

document.write(`
  <section style="${customTheme}">
    <h1 class="${exampleStyle}">Hello world!</h1>
  </section>
`);

setElementTheme

Implements a theme contract on an element.

import { setElementTheme } from '@vanilla-extract/dynamic';
import { vars } from './styles.css.ts';

const element = document.getElementById('myElement');
setElementTheme(element, vars, {
  small: '4px',
  medium: '8px',
  large: '16px'
});

๐Ÿ’ก All variables passed into this function must be assigned or itโ€™s a type error.

setElementVar

Sets a single var on an element.

import { setElementVar } from '@vanilla-extract/dynamic';
import { vars } from './styles.css.ts';

const element = document.getElementById('myElement');
setElementVar(element, vars.color.brand, 'darksalmon');

Utility functions

We also provide a standalone package of optional utility functions to make it easier to work with CSS in TypeScript.

๐Ÿ’ก This package can be used with any CSS-in-JS library.

npm install @vanilla-extract/css-utils

calc

Streamlines the creation of CSS calc expressions.

import { calc } from '@vanilla-extract/css-utils';

const styles = {
  height: calc.multiply('var(--grid-unit)', 2)
};

The following functions are available.

  • calc.add
  • calc.subtract
  • calc.multiply
  • calc.divide
  • calc.negate

The calc export is also a function, providing a chainable API for complex calc expressions.

import { calc } from '@vanilla-extract/css-utils';

const styles = {
  marginTop: calc('var(--space-large)')
    .divide(2)
    .negate()
    .toString()
};

Thanks

  • Nathan Nam Tran for creating css-in-js-loader, which served as the initial starting point for treat, the precursor to this library.
  • Stitches for getting us excited about CSS-Variables-in-JS.
  • SEEK for giving us the space to do interesting work.

License

MIT.

vanilla-extract's People

Contributors

brendan-csel avatar fnky avatar graup avatar jahredhope avatar jossmac avatar jsoref avatar kossnocorp avatar kyleamathews avatar madebyae avatar markdalgleish avatar mattcompiles avatar michaeltaranto avatar mxmul avatar ntkoopman avatar saartje87 avatar samrobbins85 avatar seek-oss-ci avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.