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

Design System Adventures in React

Design System Adventures in React

Creating a Design System is a challenge that involves many different disciplines. In this talk, we will specifically explore the technical aspects and see what tools can help implement a React-based Design System. During the talk, we will also see a concrete use case which is the Bento Design System, created and maintained by buildo (https://bento.buildo.io/).

Gabriele Petronella

April 27, 2023
Tweet

More Decks by Gabriele Petronella

Other Decks in Programming

Transcript

  1. Gabriele Petronella
    Design Systems in
    React

    View Slide

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

    View Slide

  3. 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

    View Slide

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

    View Slide

  5. Design System?

    View Slide


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

    View Slide


  7. Raise your hand if you could explain
    it to a colleague

    View Slide

  8. 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

    View Slide

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

    View Slide

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

    View Slide

  11. Vanilla Extract

    View Slide

  12. 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/)

    View Slide

  13. 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)

    View Slide

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

    View Slide

  15. 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",
    },
    },
    });

    View Slide

  16. 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 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);

    View Slide

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

    {children}

    );

    View Slide

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

    {children}

    );

    View Slide

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

    View Slide

  25. Type-safe Localization

    View Slide

  26. Problem

    View Slide

  27. Root cause
    A string is a string
    !


    View Slide

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

    function MyComponent({ label }: { label: LocalizedString }) {
    // ...
    }

    View Slide

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

    View Slide

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

    View Slide

  31. 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

    View Slide

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


    View Slide

  33. 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.

    View Slide

  34. 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

    View Slide

  35. !
    Buckle up, TS wizardry ahead

    View Slide

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

    View Slide

  37. Declaration merging cannot
    override members
    interface Box {
    height: number;
    }
    interface Box {
    height: string;
    //

    Subsequent property declarations
    // must have the same type
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide