CSS in TypeScript with vanilla-extract

No Comments

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

vanilla-extract goals to shake issues up once more. It was launched this 12 months and has the advantage of with the ability to leverage some current traits, together with:

JavaScript builders switching to TypeScriptBrowser help for CSS customized propertiesUtility-first styling

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

Zero runtime

CSS-in-JS libraries often inject kinds into the doc at runtime. This has advantages, together with crucial CSS extraction and dynamic styling.

However as a normal 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 will be cached whereas the HTTP2 protocol lowers the price of the additional request. Additionally, customized properties can now present loads of dynamic styling without spending a dime.

So, as a substitute of injecting kinds at runtime, vanilla-extract takes after Linaria and astroturf. These libraries allow you to writer kinds 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 vital sufficient to maintain the remainder of your codebase type-safe, then why not do the identical together with your kinds?

TypeScript supplies an a variety of benefits. First, there’s autocomplete. When you sort “fo” then, in a TypeScript-friendly editor, you get a listing 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. When you can’t bear in mind the title of fontVariant however comprehend it’s going to start out with the phrase “font” you sort it and scroll by the choices. In VS Code, you don’t have to obtain any extra tooling to get this to occur.

This actually accelerates the authoring of kinds:

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

vanilla-extract varieties additionally present an evidence 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 kinds are behaving unexpectedly.

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

Integrations

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

webpackesbuildViteSnowpackNextJSGatsby

It’s additionally utterly framework-agnostic. All you want to 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 elements 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: the creator of vanilla-Extract, Mark Dalgleish, can also be co-creator of CSS Modules.

model()

You’ll be able to create an mechanically scoped CSS class utilizing the model() operate. You move within the ingredient’s kinds, 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 {model} from “@vanilla-extract/css”;

export const titleStyle = model({
backgroundColor: “hsl(210deg,30%,90%)”,
fontFamily: “helvetica, Sans-Serif”,
shade: “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 will be included inside model declarations, too:

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

These model operate calls are a skinny abstraction over CSS — the entire property names and values map to the CSS properties and values you’re accustomed to. One change to get used to is that values can typically 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 model operate can solely have an effect on a single HTML node. This implies you’ll be able to’t use nesting to declare kinds for the youngsters of a component — one thing you is likely to be used to in Sass or PostCSS. As an alternative, you want to model kids individually. If a toddler ingredient wants totally different kinds primarily based on the guardian, you should utilize the selectors property so as to add kinds which can be depending on the guardian:

// title.css.ts
export const innerSpan = model({
selectors:{[`${titleStyle} &`]:{
shade: “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 guardian ingredient which can be consumed by the kid nodes. This would possibly sound restrictive, however it’s deliberately been left this method to enhance maintainability in bigger codebases. It implies that you’ll know precisely the place the kinds have been declared for every ingredient in your mission.

Theming

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

// title.css.ts
import {model,createTheme } from “@vanilla-extract/css”;

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

// Utilizing the theme
export const titleStyle = model({
backgroundColor:vars.shade.background,
shade: vars.shade.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 make sure that your variant makes use of all the identical property names, so that you get a warning should you overlook so as to add the background property to the theme.

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

// title.css.ts
import {model,createTheme } from “@vanilla-extract/css”;

export const [mainTheme,vars] = createTheme({
shade:{
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,{
shade:{
textual content:”hsl(210deg,60%,80%)”,
background: “hsl(210deg,30%,7%)”,
},
lengths:{
mediumGap: “30px”
}
})
// Consuming the theme
export const titleStyle = model({
backgroundColor: vars.shade.background,
shade: vars.shade.textual content,
fontFamily: “helvetica, Sans-Serif”,
padding: vars.lengths.mediumGap,
borderRadius: 20,
});

Then, utilizing JavaScript, you’ll be able to 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 below the hood? The objects you declare within the createTheme operate are was CSS customized properties connected to the ingredient’s class. These customized properties are hashed to forestall conflicts. The output CSS for our mainTheme instance appears to be like 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 appears to be like 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 guardian ingredient, and the mainTheme customized properties get swapped out for darkMode ones.

Recipes API

The model and createTheme features present sufficient energy to model an internet site on their very own, however vanilla-extract supplies just a few 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 move in an object with the properties base and variants:

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

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

Inside base, you’ll be able to declare the kinds that might be utilized to all variants. Inside variants, you’ll be able to present alternative ways to customise the ingredient:

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

Then you’ll be able to declare which variant you wish to use within the markup:

// button.ts
import { buttonStyles } from “./button.css”;

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

And vanilla-extract leverages TypeScript giving autocomplete for your individual 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 could be extremely helpful for constructing a design system, as you’ll be able to create reusable elements and management the methods they fluctuate. These variations grow to be simply discoverable with TypeScript — all you want to sort is CMD/CTRL + Area (on most editors) and also you get a dropdown checklist of the alternative 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:

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

So should 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 is likely to be your most popular method 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 a listing of colours and lengths by declaring a few objects. The JavaScript key names will be no matter we wish. 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:

Go 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
shade: colours,
backgroundColor: colours,
padding: lengths,
},
});

Then the ultimate step is to move 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: {
shade: colours,
// and so forth.
},
});
export const sprinkles = createSprinkles(properties);

Then we will begin styling inside our elements inline by calling the sprinkles operate within the class attribute and selecting which choices we wish for every ingredient.

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

The JavaScript output holds a category title string for every model 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’ll be able to see, this API permits you to model components inside your markup utilizing a set of pre-defined constraints. You additionally keep away from the tough process of developing with names of courses for each ingredient. The result’s one thing that feels rather a lot 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 kinds utilizing utility courses.

Wrapping up

vanilla-extract looks like a giant new step in CSS tooling. A variety 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 supplies.

Additional studying

vanilla-extract documentationMark’s discuss on vanilla-extractvanilla-extract DiscordCode examples on CodeSandbox

The publish CSS in TypeScript with vanilla-extract appeared first on CSS-Tips. You’ll be able to help CSS-Tips by being an MVP Supporter.

    About Marketing Solution Australia

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment