Slide 1

Slide 1 text

Gabriele Petronella Design Systems adventures in React

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Pedigree • Co-founder and CTO @ buildo • 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-ds.com

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

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

Slide 11

Slide 11 text

Topics • Vanilla Extract • Testing • Accessibility • One weird TypeScript technique

Slide 12

Slide 12 text

Make your own talk! https:/ /slido.com Code: #8798085

Slide 13

Slide 13 text

Vanilla Extract

Slide 14

Slide 14 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 15

Slide 15 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 16

Slide 16 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 17

Slide 17 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 18

Slide 18 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 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 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 22

Slide 22 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 23

Slide 23 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 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Accessibility

Slide 28

Slide 28 text

Accessibility • Accessibility is hard • Accessibility is important • Accessibility is being regulated at the European level • 2025: all new apps must be accessible • 2030: all existing apps must be accessible

Slide 29

Slide 29 text

Accessibility and Design Systems Design Systems are well positioned to centralize and prevent some common accessibility issues. However: accessibility is ultimately a concern of the final product.

Slide 30

Slide 30 text

Examples where Design Systems doesn't help much • Tab order (can keyboard and screen reader users jump around the content effectively?) • Link purpose Go to Google by clicking this link Click here to go to Google

Slide 31

Slide 31 text

Examples where Design Systems clearly help • Color contrast • Ensuring text is readable wrt its background and size • Operating via mouse, keyboard, touch, screen reader • Ensuring the interactions work across difference devices

Slide 32

Slide 32 text

Examples where Design Systems clearly help • Focus management • Ensuring the focus is always visible • Ensuring modal content does not let the focus escape • Form interactions • Ensuring labels/errors are linked to fields • Ensuring error state is not only a color

Slide 33

Slide 33 text

Ok, but how?

Slide 34

Slide 34 text

Using TypeScript for a greater good // ❌ // ^^^^^^^^ // Property 'label' is missing // ✅ Here label is used for screen readers, but it's also useful for mouse users (hover tooltip).

Slide 35

Slide 35 text

react-aria A library of React Hooks that provides accessible UI primitives for your design system. Immensely useful for building accessible components. If you think you can do this from scratch: good luck!

Slide 36

Slide 36 text

Examples: building a Button import { useButton } from "react-aria"; import { useRef } from "react"; function Button(props) { const ref = useRef(); const { buttonProps } = useButton(props, ref); const { children } = props; return ( {children} ); }

Slide 37

Slide 37 text

Examples: focus management FocusScope is a utility to manage the focus of its descendants. This is crucial to implement focus traps (e.g. modals) correctly.

Slide 38

Slide 38 text

Examples: building a number field Have you every tried to create a number field supporting multiple languages and locale? Yeah, I have PTSD too... useNumberField to the rescue! Why not ? Let's take a look

Slide 39

Slide 39 text

• Support for internationalized number formatting and parsing including decimals, percentages, currency values, and units • Support for the Latin, Arabic, and Han positional decimal numbering systems in over 30 locales • Automatically detects the numbering system used and supports parsing numbers not in the default numbering system for the locale • Support for multiple currency formats including symbol, code, and name in standard or accounting notation

Slide 40

Slide 40 text

• Validates keyboard entry as the user types so that only valid numeric input according to the locale and numbering system is accepted • Handles composed input from input method editors, e.g. Pinyin • Automatically selects an appropriate software keyboard for mobile according to the current platform and allowed values • Supports rounding to a configurable number of fraction digits

Slide 41

Slide 41 text

• Support for clamping the value between a configurable minimum and maximum, and snapping to a step value • Support for stepper buttons and arrow keys to increment and decrement the value according to the step value • Supports pressing and holding the stepper buttons to continuously increment or decrement • Handles floating point rounding errors when incrementing, decrementing, and snapping to step

Slide 42

Slide 42 text

• Supports using the scroll wheel to increment and decrement the value • Exposed to assistive technology as a text field with a custom localized role description using ARIA • Follows the spinbutton ARIA pattern • Works around bugs in VoiceOver with the spinbutton role • Uses an ARIA live region to ensure that value changes are announced • Support for description and error message help text linked to the input via ARIA

Slide 43

Slide 43 text

react-aria components Pre-made components, built on top of react-aria hooks, ready to be styled. import { Button } from "react-aria-components"; alert("Hello world!")}>Press me;

Slide 44

Slide 44 text

The styling can happen in multiple ways: • className • style • public CSS API via classes (e.g. .react-aria-Button)

Slide 45

Slide 45 text

Testing

Slide 46

Slide 46 text

Which kind of tests? • Unit tests? • Integration tests? • e2e tests?

Slide 47

Slide 47 text

Why do we write tests? • To ensure the code works as expected? • Because a book told me so? • So that I can feel good about test coverage reports?

Slide 48

Slide 48 text

Why do we write tests? To prevent bugs that the user perceives Preventing bugs != testing the code

Slide 49

Slide 49 text

Which kind of bugs? The most common type of bugs for a UI library is visual bugs. So, we write snapshot tests using Jest Vitest, right?

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

Why not snapshot test? You're still testing the code, hoping a code change will affect the UI. This may or may not be true. • Code changes may not affect the UI • The snapshot may not catch UI changes that indirectly affect other components Can we do better?

Slide 52

Slide 52 text

Visual testing

Slide 53

Slide 53 text

Why? In our experience the most frequent bugs in a DS library manifest as visual bugs. Other bugs can arise, but this is are by far the most common ones. You want a lot of visual tests, think of it as the base of the pyramid.

Slide 54

Slide 54 text

Chromatic • visual regression tool by the same company behind Storybook • (unsurprisingly) based on Storybook • not free (149$/m for the Starter plan, billed by the snapshot)

Slide 55

Slide 55 text

Why Chromatic • real browser snapshots • cross-browser support • multiple viewport support • integration with GitHub PRs • works well for design review

Slide 56

Slide 56 text

Example https:/ /github.com/buildo/bento-design-system/pull/929

Slide 57

Slide 57 text

Testing ! Development • Storybook is a helpful tool for development • With Chromatic, Storybook is used for testing and CI • Aligned incentives in keeping Storybook healthy and up-to-date

Slide 58

Slide 58 text

What about other kinds of tests? • Storybook and Chromatic also support interaction testing • Write tests for stories using Storybooks' play function • It uses the testing-library API • Chromatic runs them automatically and reports failures alongside visual tests

Slide 59

Slide 59 text

Getting to a specific state export const CalendarOpen = { // ... play: async () => { const button = screen.getByRole("button"); await waitFor(() => button.click()); }, };

Slide 60

Slide 60 text

Test assertions export const NonDefaultType = { args: { value: "bento@example.com" type: "email", }, play: async ({ canvasElement }) => { const textField = within(canvasElement).getByRole("textbox"); await expect(textField).toHaveAttribute("type", "email"); }, };

Slide 61

Slide 61 text

Type-safe Localization

Slide 62

Slide 62 text

Problem

Slide 63

Slide 63 text

Root cause A string is a string !

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 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 68

Slide 68 text

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

Slide 69

Slide 69 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 70

Slide 70 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 71

Slide 71 text

! Buckle up, TS wizardry ahead

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 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 75

Slide 75 text

One typical example declare module global { interface Window { logPageView: () => void; } }

Slide 76

Slide 76 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 77

Slide 77 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 78

Slide 78 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 79

Slide 79 text

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

Slide 80

Slide 80 text

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