Slide 1

Slide 1 text

Gabriele Petronella Design Systems in React

Slide 2

Slide 2 text

Context "Are you't that Scala dude who likes functional programming?" Sure, but I spent the last 3 years primarily working on Design Systems!

Slide 3

Slide 3 text

Pedigree • Built two design systems for Big Enterprise ™ • Built several others for smaller companies • Distilled that knowledge in Bento, a general purpose design system for the web

Slide 4

Slide 4 text

Bento • Design System for the web • React + TypeScript for the dev library • Figma for the design library • OSS + commercial support • https:/ /bento.buildo.io

Slide 5

Slide 5 text

Design System?

Slide 6

Slide 6 text

✋ Raise your hand if you are familiar with the term "design system"

Slide 7

Slide 7 text

✋ Raise your hand if you could explain it to a colleague

Slide 8

Slide 8 text

A design system is a set of standards to manage design at scale by reducing redundancy while creating a shared language and visual consistency across different pages and channels. -- Nielsen Norman Group

Slide 9

Slide 9 text

Design System shopping list • Foundations • Components • UX patterns • Governance • ...whatever works for your context

Slide 10

Slide 10 text

Tonight Let's talk about the technical side of building a Design System with React

Slide 11

Slide 11 text

Vanilla Extract

Slide 12

Slide 12 text

Vanilla Extract • CSS-in-TS library • CSS fully extracted at build time (no runtime overhead) • Similar to CSS modules in spirit • Originally created by Seek for their own DS (Braid) • Successor of treat (https:/ /seek-oss.github.io/treat/)

Slide 13

Slide 13 text

Why Vanilla Extract (in general)? • Static CSS generation • Fewer moving parts, CSS integrates well with any stack • No runtime overhead • Seems more future proof, for example when it comes to SSR (see https:/ /github.com/reactwg/react-18/discussions/108)

Slide 14

Slide 14 text

Why Vanilla Extract (in general)? • Editor tooling integration • It's just TypeScript, no need for special plugins • Styles are TypeScript objects, imports just work

Slide 15

Slide 15 text

Why Vanilla Extract (for Design Systems)? • Opinionated towards encapsulated components • No child selectors allowed const list = style({ padding: 8, selectors: { "& > li": { // ! a style cannot target its children color: "red", }, }, });

Slide 16

Slide 16 text

Why Vanilla Extract (for Design Systems)? • Styles are objects • Easy to manipulate and compose (example: https:/ /github.com/ buildo/bento-design-system/blob/main/packages/bento-design- system/src/Layout/Column.css.ts)

Slide 17

Slide 17 text

Why Vanilla Extract (for Design Systems)? • Recipes API • maps perfectly to how UI components are represented in a design tool

Slide 18

Slide 18 text

export const buttonRecipe = recipe({ base: { borderRadius: 6, }, variants: { color: { neutral: { background: "whitesmoke" }, accent: { background: "slateblue" }, }, size: { medium: { padding: 16 }, large: { padding: 24 }, }, }, });

Slide 19

Slide 19 text

import { buttonRecipe } from "./button.css.ts"; type Props = { color: "neutral" | "accent"; size: "medium" | "large"; children: ReactNode; }; function Button({ color, size, children }: Props) { return {children}; }

Slide 20

Slide 20 text

Why Vanilla Extract (for Design Systems)? • Sprinkles API • generate utility CSS classes • basically a roll-your-own Tailwind... • ...but with a nice API

Slide 21

Slide 21 text

import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles"; const space = { none: 0, small: "4px", medium: "8px" /*...*/ }; const responsiveProperties = defineProperties({ conditions: { mobile: {}, tablet: { "@media": "screen and (min-width: 768px)" }, desktop: { "@media": "screen and (min-width: 1024px)" }, }, defaultCondition: "mobile", properties: { paddingTop: space, paddingBottom: space, // ... }, }); export const sprinkles = createSprinkles(responsiveProperties);

Slide 22

Slide 22 text

type Props = { children: ReactNode } function Card({ children }: Props) { const className = sprinkles({ paddingTop: 'medium' }); return (
{children}
);

Slide 23

Slide 23 text

type Props = { children: ReactNode } function Card({ children }: Props) { const className = sprinkles({ paddingTop: { desktop: 'large', tablet: 'medium', mobile: 'small' } }); return (
{children}
);

Slide 24

Slide 24 text

Why Vanilla Extract (for Design Systems)? • Sprinkles API • nice to expose foundations to consumers

Slide 25

Slide 25 text

Type-safe Localization

Slide 26

Slide 26 text

Problem

Slide 27

Slide 27 text

Root cause A string is a string !

Slide 28

Slide 28 text

Can we do better? function MyComponent({ label }: { label: string }) { // ... } ⬇ function MyComponent({ label }: { label: LocalizedString }) { // ... }

Slide 29

Slide 29 text

LocalizedString? This doesn't work: type LocalizedString = string // transparent type alias We need something "more" than string: type LocalizedString = string & ???

Slide 30

Slide 30 text

LocalizedString as a branded type type LocalizedString = string & { readonly __brand: unique symbol }; The type is string but also something more.

Slide 31

Slide 31 text

LocalizedString as a branded type declare const normalString: string; declare const label: LocalizedString; declare function giveMeAString(s: string); declare function giveMeALocalizedString(s: LocalizedString); giveMeAString(normalString); // OK giveMeAString(label); // OK giveMeALocalizedString(label); // OK giveMeALocalizedString(normalString); // Error

Slide 32

Slide 32 text

Back to our component function MyComponent({ label }: { label: LocalizedString }) { // ... }

Slide 33

Slide 33 text

How do we create a LocalizedString? Well... by casting! Isn't casting bad? In general, yes. In this case, it's fine if confined to a single function. In practice you wrap the localization function of a library like react- i18next or react-intl, to change the return type to LocalizedString instead of string.

Slide 34

Slide 34 text

Ok, but I don't want any of this! What if I'm a library and I don't want to force this mechanism on my users? Ideal scenario: - the library accepts string by default - the consume can opt-in into stricter type-safety via LocalizedString

Slide 35

Slide 35 text

! Buckle up, TS wizardry ahead

Slide 36

Slide 36 text

Declaration merging interface Box { height: number; } interface Box { width: number; } same as interface Box { height: number; width: number; }

Slide 37

Slide 37 text

Declaration merging cannot override members interface Box { height: number; } interface Box { height: string; // ❌ Subsequent property declarations // must have the same type }

Slide 38

Slide 38 text

Module augmentation Imagine mymodule exports a Box interface like: interface Box { height: number; } We can "augment it" with a module augmentation: declare module "mymodule" { // module augmentation interface Box { // declaration merging width: number; } }

Slide 39

Slide 39 text

Putting it all together In our library we define an empty interface meant to be "merged": export interface TypeOverrides {} we then define the "base" types: interface BaseTypes { LocalizedString: string; }

Slide 40

Slide 40 text

Putting it all together We derive our final types as import { O } from "ts-toolbelt" type ConfiguredTypes = O.Overwrite // This is the type we use in our library export type LocalizedString = BaseTypes['LocalizedString'] & ConfiguredTypes['LocalizedString'] // Our beloved stricter type export type StrictLocalizedString = string & { readonly __brand: unique symbol }

Slide 41

Slide 41 text

Putting it all together Now a consumer of the library can opt-in into stricter type-safety by augmenting the TypeOverrides interface: import { StrictLocalizedString } from "mylibrary"; declare module "mylibrary" { interface TypeOverrides { LocalizedString: StrictLocalizedString } }

Slide 42

Slide 42 text

To recap • Branded type to avoid mixing strings and localized strings • Declaration merging + module augmentation to allow configuring library type from outside

Slide 43

Slide 43 text

Thank you ! Work with us " buildo.io ! Let's chat " twitter.com/gabro27