Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Design System Adventures in React - ReactJS Day...

Design System Adventures in React - ReactJS Day 2024

Gabriele Petronella

October 25, 2024
Tweet

More Decks by Gabriele Petronella

Other Decks in Technology

Transcript

  1. Context "Are you't that Scala / TypeScript dude who likes

    functional programming?" Sure, but I spent the last 4 years working on Design Systems!
  2. 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
  3. Bento • Design System for the web • React +

    TypeScript for the dev library • Figma for the design library • OSS + commercial support • https:/ /bento-ds.com
  4. 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
  5. Design System shopping list • Foundations • Components • UX

    patterns • Governance • ...whatever works for your context
  6. 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/)
  7. 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)
  8. Why Vanilla Extract (in general)? • Editor tooling integration •

    It's just TypeScript, no need for special plugins • Styles are TypeScript objects, imports just work
  9. 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", }, }, });
  10. 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)
  11. Why Vanilla Extract (for Design Systems)? • Recipes API •

    maps perfectly to how UI components are represented in a design tool
  12. export const buttonRecipe = recipe({ base: { borderRadius: 6, },

    variants: { color: { neutral: { background: "whitesmoke" }, accent: { background: "slateblue" }, }, size: { medium: { padding: 16 }, large: { padding: 24 }, }, }, });
  13. import { buttonRecipe } from "./button.css.ts"; type Props = {

    color: "neutral" | "accent"; size: "medium" | "large"; children: ReactNode; }; function Button({ color, size, children }: Props) { return <button className={buttonRecipe({ color, size })}>{children}</button>; }
  14. Why Vanilla Extract (for Design Systems)? • Sprinkles API •

    generate utility CSS classes • basically a roll-your-own Tailwind... • ...but with a nice API
  15. 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);
  16. type Props = { children: ReactNode } function Card({ children

    }: Props) { const className = sprinkles({ paddingTop: 'medium' }); return ( <div className={className}> {children} </div> );
  17. type Props = { children: ReactNode } function Card({ children

    }: Props) { const className = sprinkles({ paddingTop: { desktop: 'large', tablet: 'medium', mobile: 'small' } }); return ( <div className={className}> {children} </div> );
  18. Why Vanilla Extract (for Design Systems)? • Sprinkles API •

    nice to expose foundations to consumers
  19. 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
  20. 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.
  21. Examples where Design Systems doesn't help much • Tab order

    (can keyboard and screen reader users jump around the content effectively?) • Link purpose <!-- Bad --> Go to Google by clicking <a href="google.com">this link</a> <!-- Good --> Click here to <a href="google.com">go to Google</a>
  22. 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
  23. 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
  24. Using TypeScript for a greater good // ❌ <IconButton size="{16}"

    icon="{IconPencil}" /> // ^^^^^^^^ // Property 'label' is missing // ✅ <IconButton size="{16}" icon="{IconPencil}" label="Edit user" /> Here label is used for screen readers, but it's also useful for mouse users (hover tooltip).
  25. 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!
  26. 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 ( <button {...buttonProps} ref={ref}> {children} </button> ); }
  27. 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. <FocusScope contain restoreFocus> <!-- Modal content goes here --> </FocusScope>
  28. 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 <input type="number">? Let's take a look
  29. • 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
  30. • 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
  31. • 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
  32. • 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
  33. react-aria components Pre-made components, built on top of react-aria hooks,

    ready to be styled. import { Button } from "react-aria-components"; <Button onPress={() => alert("Hello world!")}>Press me</Button>;
  34. The styling can happen in multiple ways: • className •

    style • public CSS API via classes (e.g. .react-aria-Button)
  35. 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?
  36. Why do we write tests? To prevent bugs that the

    user perceives Preventing bugs != testing the code
  37. 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?
  38. 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?
  39. 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.
  40. 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)
  41. Why Chromatic • real browser snapshots • cross-browser support •

    multiple viewport support • integration with GitHub PRs • works well for design review
  42. 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
  43. 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
  44. Getting to a specific state export const CalendarOpen = {

    // ... play: async () => { const button = screen.getByRole("button"); await waitFor(() => button.click()); }, };
  45. Test assertions export const NonDefaultType = { args: { value:

    "[email protected]" type: "email", }, play: async ({ canvasElement }) => { const textField = within(canvasElement).getByRole("textbox"); await expect(textField).toHaveAttribute("type", "email"); }, };
  46. Root cause A string is a string ! <MyComponent label="Foo

    foo foo" /> <MyComponent label={t('Whatever.submitButtonLabel')} />
  47. Can we do better? function MyComponent({ label }: { label:

    string }) { // ... } ⬇ function MyComponent({ label }: { label: LocalizedString }) { // ... }
  48. LocalizedString? This doesn't work: type LocalizedString = string; // transparent

    type alias We need something "more" than string: type LocalizedString = string & ???
  49. LocalizedString as a branded type type LocalizedString = string &

    { readonly __brand: unique symbol }; The type is string but also something more.
  50. 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
  51. Back to our component function MyComponent({ label }: { label:

    LocalizedString }) { // ... } <MyComponent label="Woops, not localized, TypeScript complains" /> <MyComponent label={t('Whatever.submitButtonLabel')} />
  52. 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.
  53. 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
  54. Declaration merging interface Box { height: number; } interface Box

    { width: number; } same as interface Box { height: number; width: number; }
  55. Declaration merging cannot override members interface Box { height: number;

    } interface Box { height: string; // ❌ Subsequent property declarations // must have the same type }
  56. 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; } }
  57. 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; }
  58. Putting it all together We derive our final types as

    import { O } from "ts-toolbelt"; type ConfiguredTypes = O.Overwrite<BaseTypes, TypeOverrides>; // 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; };
  59. 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; } }
  60. To recap • Branded type to avoid mixing strings and

    localized strings • Declaration merging + module augmentation to allow configuring library type from outside
  61. Thank you ! Work with us " buildo.com ! Let's

    chat " twitter.com/gabro27 QA and feedback! !