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 full-size 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 full-size 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 full-size 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 full-size slide

  5. Design System?

    View full-size slide


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

    View full-size slide


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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  11. Vanilla Extract

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    {children}

    );

    View full-size slide

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

    {children}

    );

    View full-size slide

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

    View full-size slide

  25. Type-safe Localization

    View full-size slide

  26. Root cause
    A string is a string
    !


    View full-size slide

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

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. 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 full-size slide

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


    View full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. !
    Buckle up, TS wizardry ahead

    View full-size slide

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

    View full-size slide

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

    Subsequent property declarations
    // must have the same type
    }

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

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

    View full-size slide

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

    View full-size slide