CSS in TypeScript mit vanilla-extract

Avatar of Hugh Haworth
Hugh Haworth am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

vanilla-extract ist eine neue Framework-agnostische CSS-in-TypeScript-Bibliothek. Sie ist eine leichtgewichtige, robuste und intuitive Methode, um Ihre Stile zu schreiben. vanilla-extract ist kein präskriptives CSS-Framework, sondern ein flexibles Stück Entwickler-Tooling. CSS-Tooling war in den letzten Jahren ein relativ stabiler Bereich, mit PostCSS, Sass, CSS Modules und styled-components, die alle vor 2017 auf den Markt kamen (einige lange davor) und heute beliebt bleiben. Tailwind ist eines der wenigen Tools, das in den letzten Jahren im CSS-Tooling für Aufsehen gesorgt hat.

vanilla-extract hat das Ziel, erneut für Aufsehen zu sorgen. Es wurde dieses Jahr veröffentlicht und hat den Vorteil, dass es einige aktuelle Trends nutzen kann, darunter:

  • JavaScript-Entwickler, die zu TypeScript wechseln
  • Browserunterstützung für CSS-Custom-Properties
  • Utility-first-Styling

Es gibt eine ganze Reihe cleverer Innovationen in vanilla-extract, die meiner Meinung nach einen großen Unterschied machen.

Null Laufzeit

CSS-in-JS-Bibliotheken injizieren normalerweise Stile zur Laufzeit in das Dokument. Dies hat Vorteile, einschließlich der Extraktion von Critical CSS und dynamischem Styling.

Aber als allgemeine Faustregel gilt: Eine separate CSS-Datei ist performanter. Das liegt daran, dass JavaScript-Code eine aufwendigere Parsing/Kompilierung durchlaufen muss, während eine separate CSS-Datei gecacht werden kann, während das HTTP2-Protokoll die Kosten für die zusätzliche Anfrage senkt. Außerdem bieten Custom Properties jetzt viel dynamisches Styling kostenlos.

Anstatt Stile zur Laufzeit zu injizieren, folgt vanilla-extract dem Ansatz von Linaria und astroturf. Diese Bibliotheken ermöglichen es Ihnen, Stile mit JavaScript-Funktionen zu erstellen, die zur Build-Zeit extrahiert und zum Erstellen einer CSS-Datei verwendet werden. Obwohl Sie vanilla-extract in TypeScript schreiben, wirkt sich dies nicht auf die Gesamtgröße Ihres Produktions-JavaScript-Bundles aus.

TypeScript

Ein großer Mehrwert von vanilla-extract ist, dass Sie Typisierung erhalten. Wenn es wichtig genug ist, den Rest Ihrer Codebasis typsicher zu halten, warum sollten Sie dasselbe nicht auch mit Ihren Stilen tun?

TypeScript bietet eine Reihe von Vorteilen. Erstens gibt es Autovervollständigung. Wenn Sie "fo" eingeben, erhalten Sie in einem TypeScript-freundlichen Editor eine Liste von Schriftoptionen in einem Dropdown-Menü – fontFamily, fontKerning, fontWeight oder alles andere, was passt – zur Auswahl. Dies macht CSS-Eigenschaften bequem von Ihrem Editor aus auffindbar. Wenn Sie sich nicht an den Namen von fontVariant erinnern können, aber wissen, dass er mit dem Wort "font" beginnt, geben Sie ihn ein und scrollen Sie durch die Optionen. In VS Code müssen Sie keine zusätzlichen Tools herunterladen, damit dies geschieht.

Dies beschleunigt die Erstellung von Stilen erheblich

Es bedeutet auch, dass Ihr Editor über Ihre Schulter schaut, um sicherzustellen, dass Sie keine Rechtschreibfehler machen, die zu frustrierenden Bugs führen könnten.

vanilla-extract-Typen bieten auch eine Erklärung der Syntax in ihrer Typdefinition *und* einen Link zur MDN-Dokumentation für die CSS-Eigenschaft, die Sie bearbeiten. Dies erspart Ihnen das hektische Googeln, wenn Stile sich unerwartet verhalten.

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

Beim Schreiben in TypeScript verwenden Sie Camel-Case-Namen für CSS-Eigenschaften, wie backgroundColor. Dies könnte eine Umstellung für Entwickler sein, die an die normale CSS-Syntax wie background-color gewöhnt sind.

Integrationen

vanilla-extract bietet erstklassige Integrationen für alle neuesten Bundler. Hier ist eine vollständige Liste der Integrationen, die es derzeit unterstützt:

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

Es ist auch komplett Framework-agnostisch. Alles, was Sie tun müssen, ist, Klassennamen von vanilla-Extract zu importieren, die zur Build-Zeit in einen String umgewandelt werden.

Verwendung

Um vanilla-Extract zu verwenden, schreiben Sie eine .css.ts-Datei, die Ihre Komponenten importieren können. Aufrufe dieser Funktionen werden im Build-Schritt in gehashte und gekapselte Klassennamen-Strings umgewandelt. Dies mag CSS Modules ähneln, und das ist kein Zufall: Einer der Schöpfer von vanilla-Extract, Mark Dalgleish, ist auch Co-Schöpfer von CSS Modules.

style()

Sie können eine automatisch gekapselte CSS-Klasse mit der Funktion style() erstellen. Sie übergeben die Stile des Elements und exportieren dann den zurückgegebenen Wert. Importieren Sie diesen Wert irgendwo in Ihrem Benutzercode, und er wird in einen gekapselten Klassennamen umgewandelt.

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

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

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

Media-Queries und Pseudoselektoren können ebenfalls in Stil-Deklarationen enthalten sein.

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

Diese style-Funktionsaufrufe sind eine dünne Abstraktion über CSS – alle Eigenschaftsnamen und Werte entsprechen den Ihnen bekannten CSS-Eigenschaften und -Werten. Eine Umstellung, an die Sie sich gewöhnen müssen, ist, dass Werte manchmal als Zahl deklariert werden können (z. B. padding: 30), was standardmäßig eine Pixeleinheit ist, während einige Werte als String deklariert werden müssen (z. B. padding: "10px 20px 15px 15px").

Die Eigenschaften, die innerhalb der style-Funktion stehen, können nur einen einzelnen HTML-Knoten beeinflussen. Das bedeutet, dass Sie keine Verschachtelung verwenden können, um Stile für die Kinder eines Elements zu deklarieren – etwas, an das Sie vielleicht aus Sass oder PostCSS gewöhnt sind. Stattdessen müssen Sie Kinder separat stylen. Wenn ein Kindelement unterschiedliche Stile benötigt, die *vom* Elternteil abhängen, können Sie die selectors-Eigenschaft verwenden, um Stile hinzuzufügen, die vom Elternteil abhängen.

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

Oder Sie können auch die Theming API (zu der wir als Nächstes kommen) verwenden, um Custom Properties im Elternelement zu erstellen, die von den Kindknoten konsumiert werden. Das mag restriktiv erscheinen, aber es wurde absichtlich so gelassen, um die Wartbarkeit in größeren Codebasen zu erhöhen. Das bedeutet, dass Sie genau wissen, wo die Stile für jedes Element in Ihrem Projekt deklariert wurden.

Theming

Sie können die Funktion createTheme verwenden, um Variablen in einem TypeScript-Objekt zu erstellen.

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

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

// Using the theme
export const titleStyle = style({
  backgroundColor:vars.color.background,
  color: vars.color.text,
  fontFamily: "helvetica, Sans-Serif",
  padding: vars.lengths.mediumGap,
  borderRadius: 20,
});

Dann ermöglicht Ihnen vanilla-extract, eine Variante Ihres Themas zu erstellen. TypeScript hilft dabei, sicherzustellen, dass Ihre Variante dieselben Eigenschaftsnamen verwendet, sodass Sie eine Warnung erhalten, wenn Sie vergessen, die background-Eigenschaft zum Thema hinzuzufügen.

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

So könnten Sie ein reguläres Thema und einen Dark Mode erstellen:

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

export const [mainTheme,vars] = createTheme({
  color:{
    text: "hsl(210deg,60%,25%)",
    background: "hsl(210deg,30%,90%)"
  },
  lengths:{
    mediumGap: "30px"
  }
})
// Theme variant - note this part does not use the array syntax
export const darkMode = createTheme(vars,{
  color:{
    text:"hsl(210deg,60%,80%)",
    background: "hsl(210deg,30%,7%)",
  },
  lengths:{
    mediumGap: "30px"
  }
})
// Consuming the theme 
export const titleStyle = style({
  backgroundColor: vars.color.background,
  color: vars.color.text,
  fontFamily: "helvetica, Sans-Serif",
  padding: vars.lengths.mediumGap,
  borderRadius: 20,
});

Dann können Sie mit JavaScript dynamisch die von vanilla-extract zurückgegebenen Klassennamen anwenden, um Themen zu wechseln.

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

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

Wie funktioniert das im Hintergrund? Die Objekte, die Sie in der createTheme-Funktion deklarieren, werden in CSS Custom Properties umgewandelt, die an die Klasse des Elements angehängt werden. Diese Custom Properties werden gehasht, um Konflikte zu vermeiden. Der CSS-Output für unser mainTheme-Beispiel sieht so aus:

.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;
}

Und der CSS-Output unseres darkMode-Themas sieht so aus:

.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;
}

Alles, was wir in unserem Benutzercode ändern müssen, ist der Klassename. Wenden Sie den Klassennamen darkmode auf das Elternelement an, und die mainTheme Custom Properties werden gegen darkMode ausgetauscht.

Recipes API

Die Funktionen style und createTheme bieten genügend Leistung, um eine Website eigenständig zu gestalten, aber vanilla-extract bietet einige zusätzliche APIs zur Förderung der Wiederverwendbarkeit. Die Recipes API ermöglicht es Ihnen, eine Reihe von Varianten für ein Element zu erstellen, aus denen Sie in Ihrem Markup oder Benutzercode wählen können.

Zuerst muss sie separat installiert werden:

npm install @vanilla-extract/recipes

So funktioniert es. Sie importieren die Funktion recipe und übergeben ein Objekt mit den Eigenschaften base und variants.

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

export const buttonStyles = recipe({
  base:{
    // Styles that get applied to ALL buttons go in here
  },
  variants:{
    // Styles that we choose from go in here
  }
});

Innerhalb von base können Sie die Stile deklarieren, die auf *alle* Varianten angewendet werden. Innerhalb von variants können Sie verschiedene Möglichkeiten zur Anpassung des Elements angeben.

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

Dann können Sie im Markup deklarieren, welche Variante Sie verwenden möchten:

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

<button class=`${buttonStyles({color: "normal",size: "medium",})}`>Click me</button>

Und vanilla-extract nutzt TypeScript, um Autovervollständigung für *Ihre eigenen* Variantennamen zu ermöglichen!

Sie können Ihre Varianten beliebig benennen und beliebige Eigenschaften darin platzieren, wie hier:

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

Sie können sehen, wie nützlich dies für den Aufbau eines Designsystems wäre, da Sie wiederverwendbare Komponenten erstellen und die Art und Weise, wie sie variieren, steuern können. Diese Variationen werden mit TypeScript leicht auffindbar – alles, was Sie tippen müssen, ist CMD/CTRL + Leertaste (auf den meisten Editoren), und Sie erhalten eine Dropdown-Liste der verschiedenen Möglichkeiten, Ihre Komponente anzupassen.

Utility-first mit Sprinkles

Sprinkles ist ein Utility-first-Framework, das auf vanilla-extract aufbaut. So beschreiben es die vanilla-extract-Dokumente dazu.

Im Grunde ist es, als ob Sie Ihre eigene Null-Laufzeit-, typsichere Version von Tailwind, Styled System usw. erstellen würden.

Wenn Sie also keine Fans davon sind, Dinge zu benennen (wir alle haben Albträume, einen outer-wrapper div zu erstellen und dann festzustellen, dass wir ihn mit einem . . . outer-outer-wrapper einpacken müssen), könnte Sprinkles Ihre bevorzugte Art sein, vanilla-extract zu verwenden.

Die Sprinkles API muss ebenfalls separat installiert werden.

npm install @vanilla-extract/sprinkles

Jetzt können wir einige Bausteine für unsere Utility-Funktionen erstellen. Erstellen wir eine Liste von Farben und Längen, indem wir ein paar Objekte deklarieren. Die JavaScript-Schlüsselnamen können beliebig sein. Die Werte müssen gültige CSS-Werte für die CSS-Eigenschaften sein, die wir für sie verwenden möchten.

// sprinkles.css.ts
const colors = {
  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",
  large: "16px",
  humungous: "64px"
};

Wir können deklarieren, auf welche CSS-Eigenschaften diese Werte angewendet werden sollen, indem wir die Funktion defineProperties verwenden.

  • Übergeben Sie ihr ein Objekt mit einer properties-Eigenschaft.
  • In properties deklarieren wir ein Objekt, bei dem die *Schlüssel* die CSS-Eigenschaften sind, die der Benutzer festlegen kann (diese müssen gültige CSS-Eigenschaften sein), und die *Werte* sind die zuvor erstellten Objekte (unsere Listen von colors und lengths).
// sprinkles.css.ts
import { defineProperties } from "@vanilla-extract/sprinkles";

const colors = {
  blue100: "hsl(210deg,70%,15%)"
  // etc.
}

const lengths = {
  small: "4px",
  // etc.
}

const properties = defineProperties({
  properties: {
    // The keys of this object need to be valid CSS properties
    // The values are the options we provide the user
    color: colors,
    backgroundColor: colors,
    padding: lengths,
  },
});

Der letzte Schritt ist dann, den Rückgabewert von defineProperties an die Funktion createSprinkles zu übergeben und den zurückgegebenen Wert zu exportieren.

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

const colors = {
  blue100: "hsl(210deg,70%,15%)"
  // etc.
}

const lengths = {
  small: "4px",
  // etc. 
}

const properties = defineProperties({
  properties: {
    color: colors,
    // etc. 
  },
});
export const sprinkles = createSprinkles(properties);

Dann können wir beginnen, unsere Stile innerhalb unserer Komponenten inline zu erstellen, indem wir die Funktion sprinkles im Klassenattribut aufrufen und die Optionen auswählen, die wir für jedes Element wünschen.

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

Der JavaScript-Output enthält eine Klassennamen-Zeichenfolge für jede Stil-Eigenschaft. Diese Klassennamen entsprechen einer einzelnen Regel in der Ausgabe-CSS-Datei.

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

Wie Sie sehen können, ermöglicht Ihnen diese API, Elemente in Ihrem Markup mit vordefinierten Einschränkungen zu stylen. Sie vermeiden auch die schwierige Aufgabe, Klassennamen für jedes Element zu finden. Das Ergebnis ähnelt stark Tailwind, profitiert aber auch von der gesamten Infrastruktur, die rund um TypeScript aufgebaut wurde.

Die Sprinkles API ermöglicht es Ihnen auch, Conditions und Shorthands zu schreiben, um responsive Stile mit Utility-Klassen zu erstellen.

Zusammenfassung

vanilla-extract fühlt sich wie ein großer neuer Schritt im CSS-Tooling an. Viel Gedanken wurde darauf verwendet, es zu einer intuitiven, robusten Lösung für Styling zu entwickeln, die die volle Leistungsfähigkeit statischer Typisierung nutzt.

Weitere Lektüre