vanilla-extract is a brand new framework-agnostic CSS-in-TypeScript library. It’s a light-weight, strong, and intuitive option to write your types. vanilla-extract isn’t a prescriptive CSS framework, however a versatile piece of developer tooling. CSS tooling has been a comparatively secure house over the previous few years with PostCSS, Sass, CSS Modules, and styled-components all popping out earlier than 2017 (some lengthy earlier than that) they usually stay widespread in the present day. Tailwind is one of some instruments that has shaken issues up in CSS tooling over the previous few years.

vanilla-extract goals to shake issues up once more. It was launched this 12 months and has the good thing about having the ability to leverage some current tendencies, together with:

  • JavaScript builders switching to TypeScript
  • Browser assist for CSS customized properties
  • Utility-first styling

There are a complete bunch of intelligent improvements in vanilla-extract that I believe make it an enormous deal.

Zero runtime

CSS-in-JS libraries normally inject types into the doc at runtime. This has advantages, together with vital CSS extraction and dynamic styling.

However as a basic rule of thumb, a separate CSS file goes to be extra performant. That’s as a result of JavaScript code must undergo dearer parsing/compilation, whereas a separate CSS file might be cached whereas the HTTP2 protocol lowers the price of the additional request. Additionally, customized properties can now present plenty of dynamic styling totally free.

So, as a substitute of injecting types at runtime, vanilla-extract takes after Linaria and astroturf. These libraries allow you to creator types utilizing JavaScript features that get ripped out at construct time and used to assemble a CSS file. Though you write vanilla-extract in TypeScript, it doesn’t have an effect on the general dimension of your manufacturing JavaScript bundle.

TypeScript

A giant vanilla-extract worth proposition is that you simply get typing. If it’s necessary sufficient to maintain the remainder of your codebase type-safe, then why not do the identical along with your types?

TypeScript offers a number of advantages. First, there’s autocomplete. For those who sort “fo” then, in a TypeScript-friendly editor, you get an inventory of font choices in a drop down — fontFamily, fontKerning, fontWeight, or no matter else matches — to select from. This makes CSS properties discoverable from the consolation of your editor. For those who can’t bear in mind the title of fontVariant however comprehend it’s going to begin with the phrase “font” you sort it and scroll by the choices. In VS Code, you don’t must obtain any extra tooling to get this to occur.

This actually quickens the authoring of types:

It additionally means your editor is watching over your shoulder to be sure you aren’t making any spelling errors that might trigger irritating bugs.

vanilla-extract varieties additionally present a proof of the syntax of their sort definition and a hyperlink to the MDN documentation for the CSS property you’re modifying. This removes a step of frantically Googling when types are behaving unexpectedly.

Image of VSCode with cursor hovering over fontKerning property and a pop up describing what the property does with a link to the Mozilla documentation for the property

Writing in TypeScript means you’re utilizing camel-case names for CSS properties, like backgroundColor. This may be a little bit of a change for builders who’re used common CSS syntax, like background-color.

Integrations

vanilla-extract offers first-class integrations for all the most recent bundlers. Right here’s a full listing of integrations it at present helps:

  • webpack
  • esbuild
  • Vite
  • Snowpack
  • NextJS
  • Gatsby

It’s additionally fully framework-agnostic. All it’s essential do is import class names from vanilla-Extract, which get transformed right into a string at construct time.

Utilization

To make use of vanilla-Extract, you write up a .css.ts file that your parts can import. Calls to those features get transformed to hashed and scoped class title strings within the construct step. This would possibly sound much like CSS Modules, and this isn’t by coincidence: one of many creators of vanilla-Extract, Mark Dalgleish, can also be co-creator of CSS Modules.

type()

You’ll be able to create an mechanically scoped CSS class utilizing the type() operate. You go within the aspect’s types, then export the returned worth. Import this worth someplace in your consumer code, and it’s transformed right into a scoped class title.

// title.css.ts
import {type} from "@vanilla-extract/css";

export const titleStyle = type({
  backgroundColor: "hsl(210deg,30%,90%)",
  fontFamily: "helvetica, Sans-Serif",
  coloration: "hsl(210deg,60%,25%)",
  padding: 30,
  borderRadius: 20,
});
// title.ts
import {titleStyle} from "./title.css";

doc.getElementById("root").innerHTML = `<h1 class="${titleStyle}">Vanilla Extract</h1>`;

Media queries and pseudo selectors might be included inside type declarations, too:

// title.css.ts
backgroundColor: "hsl(210deg,30%,90%)",
fontFamily: "helvetica, Sans-Serif",
coloration: "hsl(210deg,60%,25%)",
padding: 30,
borderRadius: 20,
"@media": {
  "display screen and (max-width: 700px)": {
    padding: 10
  }
},
":hover":{
  backgroundColor: "hsl(210deg,70%,80%)"
}

These type operate calls are a skinny abstraction over CSS — the entire property names and values map to the CSS properties and values you’re aware of. One change to get used to is that values can generally be declared as a quantity (e.g. padding: 30) which defaults to a pixel unit worth, whereas some values should be declared as a string (e.g. padding: "10px 20px 15px 15px").

The properties that go contained in the type operate can solely have an effect on a single HTML node. This implies you possibly can’t use nesting to declare types for the youngsters of a component — one thing you may be used to in Sass or PostCSS. As a substitute, it’s essential type kids individually. If a toddler aspect wants completely different types primarily based on the mum or dad, you should use the selectors property so as to add types which are depending on the mum or dad:

// title.css.ts
export const innerSpan = type({
  selectors:{[`${titleStyle} &`]:{
    coloration: "hsl(190deg,90%,25%)",
    fontStyle: "italic",
    textDecoration: "underline"
  }}
});
// title.ts
import {titleStyle,innerSpan} from "./title.css";
doc.getElementById("root").innerHTML = 
`<h1 class="${titleStyle}">Vanilla <span class="${innerSpan}">Extract</span></h1>
<span class="${innerSpan}">Unstyled</span>`;

Or you too can use the Theming API (which we’ll get to subsequent) to create customized properties within the mum or dad aspect which are consumed by the kid nodes. This would possibly sound restrictive, but it surely’s deliberately been left this option to enhance maintainability in bigger codebases. It signifies that you’ll know precisely the place the types have been declared for every aspect in your challenge.

Theming

You should utilize the createTheme operate to construct out variables in a TypeScript object:

// title.css.ts
import {type,createTheme } from "@vanilla-extract/css";

// Creating the theme
export const [mainTheme,vars] = createTheme({
  coloration:{
    textual content: "hsl(210deg,60%,25%)",
    background: "hsl(210deg,30%,90%)"
  },
  lengths:{
    mediumGap: "30px"
  }
})

// Utilizing the theme
export const titleStyle = type({
  backgroundColor:vars.coloration.background,
  coloration: vars.coloration.textual content,
  fontFamily: "helvetica, Sans-Serif",
  padding: vars.lengths.mediumGap,
  borderRadius: 20,
});

Then vanilla-extract permits you to make a variant of your theme. TypeScript helps it be sure that your variant makes use of all the identical property names, so that you get a warning in case you neglect so as to add the background property to the theme.

Image of VS Code where showing a theme being declared but missing the background property causing a large amount of red squiggly lines to warn that the property’s been forgotten

That is the way you would possibly create a daily theme and a darkish mode:

// title.css.ts
import {type,createTheme } from "@vanilla-extract/css";

export const [mainTheme,vars] = createTheme({
  coloration:{
    textual content: "hsl(210deg,60%,25%)",
    background: "hsl(210deg,30%,90%)"
  },
  lengths:{
    mediumGap: "30px"
  }
})
// Theme variant - be aware this half doesn't use the array syntax
export const darkMode = createTheme(vars,{
  coloration:{
    textual content:"hsl(210deg,60%,80%)",
    background: "hsl(210deg,30%,7%)",
  },
  lengths:{
    mediumGap: "30px"
  }
})
// Consuming the theme 
export const titleStyle = type({
  backgroundColor: vars.coloration.background,
  coloration: vars.coloration.textual content,
  fontFamily: "helvetica, Sans-Serif",
  padding: vars.lengths.mediumGap,
  borderRadius: 20,
});

Then, utilizing JavaScript, you possibly can dynamically apply the category names returned by vanilla-extract to modify themes:

// title.ts
import {titleStyle,mainTheme,darkMode} from "./title.css";

doc.getElementById("root").innerHTML = 
`<div class="${mainTheme}" id="wrapper">
  <h1 class="${titleStyle}">Vanilla Extract</h1>
  <button onClick="doc.getElementById('wrapper').className="${darkMode}"">Darkish mode</button>
</div>`

How does this work beneath the hood? The objects you declare within the createTheme operate are was CSS customized properties connected to the aspect’s class. These customized properties are hashed to forestall conflicts. The output CSS for our mainTheme instance seems to be like this:

.src__ohrzop0 {
  --color-brand__ohrzop1: hsl(210deg,80%,25%);
  --color-text__ohrzop2: hsl(210deg,60%,25%);
  --color-background__ohrzop3: hsl(210deg,30%,90%);
  --lengths-mediumGap__ohrzop4: 30px;
}

And the CSS output of our darkMode theme seems to be like this:

.src__ohrzop5 {
  --color-brand__ohrzop1: hsl(210deg,80%,60%);
  --color-text__ohrzop2: hsl(210deg,60%,80%);
  --color-background__ohrzop3: hsl(210deg,30%,10%);
  --lengths-mediumGap__ohrzop4: 30px;
}

So, all we have to change in our consumer code is the category title. Apply the darkmode class title to the mum or dad aspect, and the mainTheme customized properties get swapped out for darkMode ones.

Recipes API

The type and createTheme features present sufficient energy to type an internet site on their very own, however vanilla-extract offers a couple of further APIs to advertise reusability. The Recipes API permits you to create a bunch of variants for a component, which you’ll be able to select from in your markup or consumer code.

First, it must be individually put in:

npm set up @vanilla-extract/recipes

Right here’s the way it works. You import the recipe operate and go in an object with the properties base and variants:

// button.css.ts
import { recipe } from '@vanilla-extract/recipes';

export const buttonStyles = recipe({
  base:{
    // Kinds that get utilized to ALL buttons go in right here
  },
  variants:{
    // Kinds that we select from go in right here
  }
});

Inside base, you possibly can declare the types that can be utilized to all variants. Inside variants, you possibly can present other ways to customise the aspect:

// button.css.ts
import { recipe } from '@vanilla-extract/recipes';
export const buttonStyles = recipe({
  base: {
    fontWeight: "daring",
  },
  variants: {
    coloration: {
      regular: {
        backgroundColor: "hsl(210deg,30%,90%)",
      },
      callToAction: {
        backgroundColor: "hsl(210deg,80%,65%)",
      },
    },
    dimension: {
      giant: {
        padding: 30,
      },
      medium: {
        padding: 15,
      },
    },
  },
});

Then you possibly can declare which variant you need to use within the markup:

// button.ts
import { buttonStyles } from "./button.css";

<button class=`${buttonStyles({coloration: "regular",dimension: "medium",})}`>Click on me</button>

And vanilla-extract leverages TypeScript giving autocomplete for your personal variant names!

You’ll be able to title your variants no matter you want, and put no matter properties you need in them, like so:

// button.css.ts
export const buttonStyles = recipe({
  variants: {
    animal: {
      canine: {
        backgroundImage: 'url("./canine.png")',
      },
      cat: {
        backgroundImage: 'url("./cat.png")',
      },
      rabbit: {
        backgroundImage: 'url("./rabbit.png")',
      },
    },
  },
});

You’ll be able to see how this might be extremely helpful for constructing a design system, as you possibly can create reusable parts and management the methods they differ. These variations change into simply discoverable with TypeScript — all it’s essential sort is CMD/CTRL + House (on most editors) and also you get a dropdown listing of the other ways to customise your part.

Utility-first with Sprinkles

Sprinkles is a utility-first framework constructed on prime of vanilla-extract. That is how the vanilla-extract docs describe it:

Mainly, it’s like constructing your personal zero-runtime, type-safe model of Tailwind, Styled System, and so forth.

So in case you’re not a fan of naming issues (all of us have nightmares of making an outer-wrapper div then realising we have to wrap it with an . . . outer-outer-wrapper ) Sprinkles may be your most popular approach to make use of vanilla-extract.

The Sprinkles API additionally must be individually put in:

npm set up @vanilla-extract/sprinkles

Now we will create some constructing blocks for our utility features to make use of. Let’s create an inventory of colours and lengths by declaring a few objects. The JavaScript key names might be no matter we would like. The values will should be legitimate CSS values for the CSS properties we plan to make use of them for:

// sprinkles.css.ts
const colours = {
  blue100: "hsl(210deg,70%,15%)",
  blue200: "hsl(210deg,60%,25%)",
  blue300: "hsl(210deg,55%,35%)",
  blue400: "hsl(210deg,50%,45%)",
  blue500: "hsl(210deg,45%,55%)",
  blue600: "hsl(210deg,50%,65%)",
  blue700: "hsl(207deg,55%,75%)",
  blue800: "hsl(205deg,60%,80%)",
  blue900: "hsl(203deg,70%,85%)",
};

const lengths = {
  small: "4px",
  medium: "8px",
  giant: "16px",
  humungous: "64px"
};

We will declare which CSS properties these values are going to use to through the use of the defineProperties operate:

  • Move it an object with a properties property.
  • In properties, we declare an object the place the keys are the CSS properties the consumer can set (these should be legitimate CSS properties) and the values are the objects we created earlier (our lists of colours and lengths).
// sprinkles.css.ts
import { defineProperties } from "@vanilla-extract/sprinkles";

const colours = {
  blue100: "hsl(210deg,70%,15%)"
  // and so forth.
}

const lengths = {
  small: "4px",
  // and so forth.
}

const properties = defineProperties({
  properties: {
    // The keys of this object should be legitimate CSS properties
    // The values are the choices we offer the consumer
    coloration: colours,
    backgroundColor: colours,
    padding: lengths,
  },
});

Then the ultimate step is to go the return worth of defineProperties to the createSprinkles operate, and export the returned worth:

// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";

const colours = {
  blue100: "hsl(210deg,70%,15%)"
  // and so forth.
}

const lengths = {
  small: "4px",
  // and so forth. 
}

const properties = defineProperties({
  properties: {
    coloration: colours,
    // and so forth. 
  },
});
export const sprinkles = createSprinkles(properties);

Then we will begin styling inside our parts inline by calling the sprinkles operate within the class attribute and selecting which choices we would like for every aspect.

// index.ts
import { sprinkles } from "./sprinkles.css";
doc.getElementById("root").innerHTML = `<button class="${sprinkles({
  coloration: "blue200",
  backgroundColor: "blue800",
  padding: "giant",
})}">Click on me</button>
</div>`;

The JavaScript output holds a category title string for every type property. These class names match a single rule within the output CSS file.

<button class="src_color_blue200__ohrzop1 src_backgroundColor_blue800__ohrzopg src_padding_large__ohrzopk">Click on me</button>

As you possibly can see, this API permits you to type parts inside your markup utilizing a set of pre-defined constraints. You additionally keep away from the troublesome job of developing with names of courses for each aspect. The result’s one thing that feels lots like Tailwind, but in addition advantages from all of the infrastructure that has been constructed round TypeScript.

The Sprinkles API additionally permits you to write circumstances and shorthands to create responsive types utilizing utility courses.

Wrapping up

vanilla-extract appears like an enormous new step in CSS tooling. Quite a lot of thought has been put into constructing it into an intuitive, strong resolution for styling that makes use of the entire energy that static typing offers.

Additional studying

#CSS #TypeScript #vanillaextract

Leave a Reply

Your email address will not be published. Required fields are marked *