Blog

Part 57 · Running a Design System Across Many Teams

A design system is shared tokens, components, docs and rules. Learn tokens and themes, component APIs that are hard to use wrongly, safe changes with version numbers, and who decides.

In the patterns stage, we built parts that other people use. Part 38 made an accordion from small parts. Part 43 let a button draw a link. Part 45 gave a card named places for content. Then Parts 46 to 48 made them work with a keyboard and a screen reader, and Parts 49 and 50 tested them.

Now picture fifty teams using those parts in fifty apps. One team wants a new color. Another wants to rename a prop. A third team quietly copies the button and changes it. A year later, there are three different buttons.

A design system is how a company stops that from happening. This part covers four things. What goes into a design system. How to build its components so they are easy to use. How to change it without breaking other teams’ apps. And who decides what goes in.

Try this first

Read this code. Don’t press Run yet.

import { useState, type ReactNode } from 'react'

// The tokens: every color and space, in one place.
const colors = {
  light: { page: '#ffffff', text: '#1f2933', brand: '#0f766e', onBrand: '#ffffff' },
  dark: { page: '#111827', text: '#f9fafb', brand: '#5eead4', onBrand: '#111827' },
}
const space = { small: '4px', medium: '8px', large: '16px' }

function vars(prefix: string, values: Record<string, string>) {
  return Object.entries(values).map(([name, value]) => `--${prefix}-${name}: ${value};`).join(' ')
}

const css = `
[data-theme="light"] { ${vars('color', colors.light)} ${vars('space', space)} }
[data-theme="dark"] { ${vars('color', colors.dark)} ${vars('space', space)} }
.ds-page { background: var(--color-page); color: var(--color-text); padding: var(--space-large); }
.ds-card { border: 1px solid var(--color-brand); padding: var(--space-medium); margin-bottom: var(--space-medium); }
.ds-button { background: var(--color-brand); color: var(--color-onBrand); padding: var(--space-small) var(--space-large); border: none; }
`

function Card({ title, children }: { title: string; children: ReactNode }) {
  return (
    <section className="ds-card">
      <h2>{title}</h2>
      {children}
    </section>
  )
}

function Button({ children, onClick }: { children: ReactNode; onClick?: () => void }) {
  return <button className="ds-button" onClick={onClick}>{children}</button>
}

export default function App() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  const next = theme === 'light' ? 'dark' : 'light'

  return (
    <div className="ds-page" data-theme={theme}>
      <style>{css}</style>
      <Card title="Your order">
        <p>Two books, ready to ship.</p>
        <Button>Pay now</Button>
      </Card>
      <Button onClick={() => setTheme(next)}>Switch to {next}</Button>
    </div>
  )
}

There are three components here: Card, Button and App. Make a guess. When you press “Switch to dark”, how many of them get a new prop that tells them the colors?

Press Run, then press “Switch to dark”.

The background, the text, the card’s border and both buttons change. A thin white edge stays around them. That is the result box’s own margin, outside our <div>. But none of the components got a color prop. Only one thing changed: the data-theme attribute on the outer <div>, from light to dark.

The colors live in one object, colors, which the code turns into CSS. The components only ask for names like --color-brand. When the names point to new values, the components change.

What a design system is

A design system is a set of shared things that many teams use to build their apps. It usually has four parts:

  • Tokens: the basic design choices, saved as named values. A color, a space size, a text size.
  • Components: the parts built from those tokens, like Button, Tabs and Accordion.
  • Docs: pages that say when to use each part, and when not to.
  • Rules: how the system changes, and who decides.

It serves two groups. The teams who build apps get parts that already work. The people who use those apps see the same button act the same way everywhere. The GOV.UK Design System’s docs say its contents must “meet user needs”.

Design tokens: design choices as data

A design token is a design choice with a name. The Design Tokens Community Group, a group that meets at the W3C, wrote a shared file format for tokens. Its report says a token is, at least, “a name/value pair”. Its own example is color-text-primary: #000000. The report is not a W3C standard. It is the community group’s own format.

Names that point to other names

The format allows a token’s value to “be a reference to another token”. It calls the second name an alias, which means another name for the same thing.

One way to use this is in layers. GitHub’s design system, called Primer, does this. Its base color tokens “map directly to a raw value”, like a dark teal, a blue-green, #0f766e. Its functional tokens name a job, like the color of text or of a border, and point to base tokens. Primer says base tokens “should never be used directly in code or design”.

So a button uses a name like color-brand. In the light theme, color-brand points to one base color. In the dark theme, it points to another. The button never knows the difference.

From tokens to CSS custom properties

In “Try this first”, vars turns each token into a CSS custom property. That’s a name with two dashes in front, like --color-brand: #0f766e;. CSS reads it back with var(--color-brand).

MDN, a site of web guides, says custom properties made with two dashes get “their value from their parent”. So we set them once, on the outer <div>. Every tag inside it can read them.

Each theme is one block of CSS that sets the same names to different values. Changing the attribute picks the other block. Each value is written in exactly one place, a single source of truth.

We checked this in a real browser, Chromium. We took the HTML that React made for “Try this first”, in both themes. The “Pay now” button’s background was rgb(15, 118, 110) in light and rgb(94, 234, 212) in dark. Those are #0f766e and #5eead4.

Tokens only reach code that uses them. A badge with background: #0f766e written into it stayed rgb(15, 118, 110) in the dark theme. So the system’s components always use a token.

In the community group’s format, tokens live in a JSON file, outside the code. Tools can turn that one file into CSS and into code for other platforms.

Starting with the user’s choice

People can set their phone or computer to light or dark mode. MDN says the prefers-color-scheme check tells you “if a user has requested light or dark color themes”. In React, read it once, for the first value of the theme state:

import { useState } from 'react'

export function useTheme() {
  const [theme, setTheme] = useState<'light' | 'dark'>(() =>
    window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  )
  return [theme, setTheme] as const
}

The function inside useState runs only for the first value (Part 4). We tried the matchMedia line in Chromium. With its color scheme set to dark, the line gave true. Set to light, it gave false.

One more CSS line helps. MDN says the color-scheme property lets the browser change “the default colors of form controls” and scroll bars. So add color-scheme: dark; to the dark block, and the browser’s own parts turn dark too.

A token change, step by step

Tokens ship in the design system’s package, with a version number, like the components do. Here is what happens when the design team changes one token.

1. One token, –color-brand, holds teal. Three components use it. 2. The team changes the token to purple and ships a new version. 3. The components that read it change, and every screen that uses them. 4. One edit reached every app that updates to the new version. –color-brand: #0f766e the token file Button var(–color-brand) Tabs var(–color-brand) Link var(–color-brand) Sign in screen Cart screen Settings screen

One token change reaches every screen. Press play, or step through it.

The same steps in words:

  1. One token, --color-brand, holds teal. Three components use it: Button, Tabs and Link.
  2. The design team changes that one token to purple, and ships a new version.
  3. Every component that reads var(--color-brand) draws purple, and so does every screen that uses them. The browser works this out in one pass. Nobody changed the screens’ code.
  4. One edit reached every screen of every app that updates to the new version.

A token change is not always safe. A new color can make text hard to read. So token changes go through the same checks as code changes. We come back to checks near the end.

Component APIs that are easy to use and hard to use wrongly

A component’s API is what you can pass to it and get back from it. For a React component, that is mostly its props. Many teams learn each API, so small choices matter. Here are six rules, each linked to the part that taught it.

Rule 1: the same names everywhere

If Tabs takes value and Accordion takes selected, every user must remember both. Pick one name for one idea, and use it in every component.

Idea A good name Where we met it
what is chosen now, set by the parent value Part 28
the first value, then the component keeps it defaultValue Part 28
“the value changed” onValueChange Part 38, Part 39
can’t be used right now disabled Part 46

value and defaultValue are React’s own names, from <input>. onValueChange is Radix UI’s name. Part 38 chose it over onChange, so it isn’t mixed up with the browser’s change event on an <input>.

Rule 2: work controlled and uncontrolled

Part 38’s accordion and Part 39’s tabs worked both ways: the component keeps its own state, or the parent does. A design system writes that code once, as a hook, and every component uses it:

import { useState } from 'react'

export function useControllableState<T>(value: T | undefined, defaultValue: T, onValueChange?: (next: T) => void) {
  const [own, setOwn] = useState(defaultValue)
  const isControlled = value !== undefined
  const current = isControlled ? value : own

  function set(next: T) {
    if (Object.is(next, current)) return
    if (!isControlled) setOwn(next)
    onValueChange?.(next)
  }

  return [current, set] as const
}

Object.is checks if two values are the same. If nothing changed, set does nothing, so onValueChange isn’t called for a change that didn’t happen.

Radix UI has a hook with the same name in its own code, in the package @radix-ui/react-use-controllable-state. It also calls its change function only when the value changes. And in development, it warns when a component switches between controlled and uncontrolled.

Rule 3: pass the rest on, and merge className and style

A team will always need one small change, like a class for spacing or an aria-describedby. If Button drops unknown props, the team copies Button and changes the copy.

So a good Button takes every prop a <button> takes. It keeps the ones it needs and passes the rest on with {...rest}. For className and style, it joins its own value with the caller’s, so neither one is lost.

import { useRef, type ComponentProps, type CSSProperties } from 'react'

type ButtonProps = ComponentProps<'button'> & { variant?: 'primary' | 'quiet' }

const base: CSSProperties = { padding: '8px 16px', borderRadius: 6 }

function Button({ variant = 'primary', className, style, type = 'button', ...rest }: ButtonProps) {
  const classes = ['ds-button', `ds-button--${variant}`, className].filter(Boolean).join(' ')
  return <button type={type} className={classes} style={{ ...base, ...style }} {...rest} />
}

export default function App() {
  const saveRef = useRef<HTMLButtonElement>(null)
  return (
    <form onSubmit={(e) => { e.preventDefault(); console.log('The form was sent') }}>
      <Button ref={saveRef} variant="quiet" className="wide" style={{ marginTop: 8 }}
        aria-describedby="save-hint" onClick={() => console.log('Save was clicked')}>
        Save
      </Button>
      <p id="save-hint">Saves your draft.</p>
      <Button onClick={() => saveRef.current?.focus()}>Focus Save</Button>
    </form>
  )
}
Save was clicked

Run it and press “Save”. Only “Save was clicked” appears. Three rules are at work here:

  • Rest props. aria-describedby and onClick aren’t named anywhere in Button. They reach the <button> through {...rest}.
  • Merging. The class is ds-button ds-button--quiet wide: the system’s classes, then the caller’s. The style has the system’s padding and the caller’s marginTop.
  • ref as a prop. Part 14 showed that in React 19, ref is a normal prop. It travels in rest like any other. Press “Focus Save”, and the focus moves to Save. We checked: saveRef.current was the <button> tag.

This Button has no onClick or ref of its own, so the caller’s pass through. With its own, it would need to run both handlers and fill both refs. Part 43’s merging rules did that.

There is one safe default here too: type = 'button'. MDN says a button inside a form is a submit button “if the attribute is not specified”. We tried it in jsdom, a pretend browser. With no type, clicking Save printed “Save was clicked” and then “The form was sent”. With type = 'button', only the first line. A team that wants a submit button can still pass type="submit".

Rule 4: accessible by default

A design system can build Parts 46 to 48 into its API. A button with only an icon has no words, but a screen reader needs a name. So the system’s IconButton makes label a required prop:

import type { ComponentProps } from 'react'

type IconButtonProps = Omit<ComponentProps<'button'>, 'children'> & {
  icon: string
  label: string
}

function IconButton({ icon, label, type = 'button', ...rest }: IconButtonProps) {
  return (
    <button type={type} aria-label={label} {...rest}>
      <span aria-hidden="true">{icon}</span>
    </button>
  )
}

const close = <IconButton icon="✕" />

TypeScript stops here: “Property ‘label’ is missing”. The team can’t forget the name. The icon gets aria-hidden="true", so label is the only name (Part 46 showed how aria-hidden works).

Types can’t catch everything. label="" passes the type check and still gives an empty name. That’s why the system also runs the tests from Part 50.

Rule 5: small parts, not more props

Part 38 showed an accordion with 12 boolean props that still couldn’t do everything. In a design system, fifty teams send requests, so that list grows even faster. Give teams small parts that fit together, and let them write what goes between. But Part 38 also listed when plain props win, and Part 45 compared slots with compound components. A design system usually has both kinds.

Rule 6: one look, many tags

A team needs a link that looks like a button. Without help, they copy the button’s classes onto an <a>. Now there are two buttons. Part 43 showed three ways to share one look across tags: two components, an as prop, or asChild. A design system picks one way and uses it everywhere.

Changing the system without breaking apps

A design system changes all the time. But fifty apps depend on it, and a change that breaks one of them costs the system their trust.

Version numbers that mean something

A design system ships as a package with a version number, like 2.4.1. Semantic Versioning, or semver, gives each number a job. Raise the:

  • MAJOR (first) number “when you make incompatible API changes”. Incompatible means some old code breaks.
  • MINOR (middle) number when you add something new, and old code still works.
  • PATCH (last) number when you fix a bug, and old code still works.

Here is how that works for a Button:

Change Old code still works? New version
Fix the focus ring color yes 2.4.1 → 2.4.2 (patch)
Add size="large" yes 2.4.2 → 2.5.0 (minor)
Mark kind as deprecated, add variant yes, with a warning 2.5.0 → 2.6.0 (minor)
Remove kind no 2.6.0 → 3.0.0 (major)

Deprecated means “still works, but planned for removal” (Part 14). Semver says marking something deprecated MUST raise the minor number. Remove it in a later major version, with “at least one minor release” in between. A deprecation is this whole step.

Semver is a promise the system’s team makes, and no tool checks it. By default, npm saves a package’s version with a ^ in front, like ^2.4.1. The npm rules say ^1.2.3 means at least 1.2.3 but below 2.0.0. So an app with ^2.4.1 takes 2.6.0 the next time it updates. If 2.6.0 breaks something, the number gave no warning.

Primer writes the promise into its docs. A breaking change to one of its “Ready” components means a new major version. Primer will also show teams how to move.

What counts as breaking

A change is breaking when code that worked before stops working, or works differently. For a design system, that’s more than removing a prop:

  • Removing a prop, or changing its name. We gave the Rule 3 Button the old prop kind="quiet". React put kind="quiet" on the <button> tag, printed no warning, and the button lost its quiet class. Apps in TypeScript get a type error. Apps in JavaScript get no message at all.
  • Changing a default. Say Button had no type before, and a new version adds type = 'button'. Every Button that used to send its form now doesn’t. We saw both sides in jsdom, in Rule 3.
  • Making an optional prop required. Say label on IconButton used to be optional. Making it required breaks the type check of every app that left it out. That’s a major version, even though it’s an accessibility fix.
  • Changing a token’s name. Apps use tokens in their own CSS too. We changed the name --color-brand and kept a style that still asked for the old name. In Chromium, the background became rgba(0, 0, 0, 0): no color at all. The token format has a $deprecated property, to mark a token as deprecated before it’s removed.
  • Changing tags or class names. Apps may style or test them. GOV.UK warns that CSS aimed at its classes “may break if you install an update”.

A warning, once, in development only

Primer’s docs say that for a deprecated component, “a warning is shown to the consumer”, the team that uses it. A simple way is a console message, only in development.

Read this. How many warnings do you think the console will show?

import { useEffect, type ReactNode } from 'react'

// In a real package, use process.env.NODE_ENV !== 'production' here (see below).
const DEV = true

const warned = new Set<string>()

function warnOnce(key: string, message: string) {
  if (!DEV || warned.has(key)) return
  warned.add(key)
  console.warn(message)
}

type ButtonProps = {
  variant?: 'primary' | 'quiet'
  /** @deprecated Use `variant` instead. */
  kind?: 'primary' | 'quiet'
  children: ReactNode
}

function Button({ variant, kind, children }: ButtonProps) {
  useEffect(() => {
    if (kind !== undefined) {
      warnOnce('Button.kind', 'Button: the "kind" prop is deprecated. Use "variant" instead. It will be removed in version 3.0.0.')
    }
  }, [kind])

  const look = variant ?? kind ?? 'primary'
  return <button type="button" className={`ds-button ds-button--${look}`}>{children}</button>
}

export default function App() {
  return (
    <div>
      <Button kind="quiet">Old prop</Button>
      <Button kind="quiet">Old prop again</Button>
      <Button variant="quiet">New prop</Button>
    </div>
  )
}

Press Run. The console shows one warning:

Button: the "kind" prop is deprecated. Use "variant" instead. It will be removed in version 3.0.0.

Two buttons use kind, and Strict Mode runs each Effect’s setup twice when the page loads (Part 11). So our lab counted 4 calls to warnOnce. Only the first printed, because warned remembers the key. Without that check, we got 4 warnings, and 200 for 100 buttons. warned is a Set, a list where each value appears only once. It lives outside the component, so every Button shares it.

Why an Effect? Components must be pure, and printing is a side effect (Part 11).

The old prop still works: “Old prop” has the class ds-button--quiet. That’s why this is a minor change.

The /** @deprecated ... */ line is a JSDoc comment, a note for tools. TypeScript’s docs say it is “surfaced in completion lists”, and editors like VS Code cross out kind.

DEV must be true while the app’s team develops, and false for real users. In a design system package, write:

const DEV = process.env.NODE_ENV !== 'production'

Why not Vite’s import.meta.env.DEV? Vite’s docs say a library build replaces every import.meta.env value, but not process.env. So import.meta.env.DEV is fixed when the design system is built, not when the app is. We tried it with Vite 8.3.3, in a small package with both checks. The import.meta.env.DEV warning was gone from the built package. The process.env.NODE_ENV warning showed in the app’s dev server, and was gone from its production build. In an app’s own code, import.meta.env.DEV is fine. The playground can’t read process.env, so the example sets DEV itself.

Codemods, and a changelog

A codemod is a program that changes code, like kind= to variant= in every file. Part 56 ran React’s own, and showed why you still read every change and run your tests. Ship one with the deprecation, so apps can move before the major version. React did the same: Part 56 quoted that React 18.3 is “identical to 18.2 but adds warnings for deprecated APIs”.

A changelog lists the “changes for each version of a project”, in the words of the site Keep a Changelog. Its first rule: “Changelogs are for humans, not machines.” It uses headings like Added, Deprecated and Removed.

## [2.6.0] - 2026-10-07

### Added
- Button: the `variant` prop, with the values `primary` and `quiet`.

### Deprecated
- Button: the `kind` prop. Use `variant`. A codemod changes it for you.
  `kind` will be removed in 3.0.0.

Deciding together

The code is the easy part. The hard part is people.

Who decides

GOV.UK has a Design System team that owns its system. We’ll call it the core team. Other teams can contribute, which means they help build it. GOV.UK writes the steps down in public:

  1. Anyone can suggest a component in an issue, a page on GitHub for one idea or problem. Show “evidence of the user needs”, not code.
  2. To build it, meet the core team and agree what the work covers.
  3. Test it with a “representative sample of users, including those with disabilities”, as the contribution criteria ask.
  4. The core team reviews the work against fixed criteria, and votes on it.

Other systems mark new work clearly. Carbon has Carbon Labs, for work that “has not yet met the definition of done criteria”. Primer marks each component Experimental, Ready or Deprecated. GOV.UK uses Trial and Stable. A trial component becomes stable after 6 months with no negative feedback.

These labels tell teams how much of the version promise applies. Primer’s major-version promise is for its Ready components. GOV.UK says a trial component may “change substantially”.

A new component, or a new variant?

A variant is another look of the same component, picked with a prop. A team asks for a “Danger button”, for deleting things. Is that a new component, or a variant="danger" on Button?

GOV.UK’s first criteria help. A new proposal must be:

  • Useful: “There is evidence that this component or pattern would be useful for many teams or services.”
  • Unique: it doesn’t copy something already in the Design System.

A danger button does a button’s job, with the same tag and keys. Only the look changes. So it isn’t unique: it’s a variant. A date picker has a different job, tags and keys, so it’s a new component. A simple test follows:

  • Same job, same HTML, same keyboard behavior: a variant or a prop.
  • A different job, or a different role for a screen reader: a new component.
  • Needed by one screen in one app: neither, yet. Build it in the app, with the system’s tokens. Suggest it for the system when other teams need it too.

When a team needs to change a part

GOV.UK says that changing its components can “create potential risk”. Its advice, in short:

  • Don’t write CSS that targets the system’s classes. For a small change, add your own extra class next to theirs.
  • Start your own class names with your own word, like app-. GOV.UK’s all start with govuk-, so the two never mix up.
  • For a large change, copy the component, rename it, and own it. Then “you will not receive any future updates from the original component”.

The className merging from Rule 3 is the React version of that extra class.

Checks before every release

Every change can reach every app, so the system runs checks before each release, token changes included.

  • Component tests. Part 49 wrote tests that click and type like a user, with React Testing Library.
  • Accessibility tests. Part 50 used four layers: a linter, role queries, axe-core in real browsers, and a person.
  • Visual regression tests. A regression is something that worked before and broke. These tests take a picture of each component and compare it with the last good picture. Playwright, from Part 50, has one: toHaveScreenshot(). The first run has no picture to compare, so it fails with “A snapshot doesn’t exist” and saves one. Its docs also warn that pictures can differ between computers and browsers. So take them in the same setup every time.
  • Storybook. A tool for showing each component’s examples on its own. Its docs call it a “workshop that lives alongside your app”. Its visual tests run on a web service made by the Storybook team.

A failing check stops the release.

Common mistakes

A prop for one screen

type ButtonProps = {
  variant?: 'primary' | 'quiet'
  extraMarginForTheSalePage?: boolean
}

One team needs more space on one page, so the system adds a prop. Next month, another team wants a different space. Soon nobody knows which of these props are still used.

The fix: say no to props for a single screen. The team passes className or style, which Button already merges (Rule 3). If many teams ask for the same thing, it becomes a real prop, with a general name, like size.

A breaking change in a minor version

The system changes a prop’s name, adds a default, or changes a token’s name, and ships 2.6.0. Apps with ^2.4.1 take it on their next update. Every change in What counts as breaking belongs in a major version.

The fix: keep the old way working, warn once in development, and ship a codemod. Remove the old way only in 3.0.0.

Practice

Press Edit on any example above and try these.

  1. In “Try this first”, add a radius token with the value 6px. Use it as the border-radius of .ds-button.
  2. In the warnOnce example, delete || warned.has(key). How many warnings do you see now? Why that number?
  3. In the same example, set DEV to false. What does the console show? Is “Old prop” still quiet?
  4. In the Rule 3 Button, change type = 'button', ...rest to type, ...rest. Press “Save”. What does the console show, and why?
Answers
  1. Add a new object const shape = { radius: '6px' }. Add ${vars('shape', shape)} to both theme blocks. Then add border-radius: var(--shape-radius); to .ds-button. Both buttons get round corners, in both themes.
  2. 4 warnings. Two buttons use kind. Strict Mode runs each Effect’s setup twice when the page loads. 2 × 2 = 4. The warned.add(key) line still runs, but nothing checks it now.
  3. The console shows nothing. warnOnce returns before it prints. “Old prop” still has the class ds-button--quiet, because variant ?? kind still reads kind. That is how it behaves for real users.
  4. “Save was clicked”, then “The form was sent”. With no default, the <button> has no type attribute. Inside a form, that makes it a submit button. That’s why adding the default would be a breaking change.

Interview questions

Try to answer each one out loud before you open the answer.

What is a design system? How is it more than a component library?

A component library is code. A design system adds three things. Tokens are the basic choices as named values. Docs say when to use each part. Rules say how it changes and who decides.

A strong answer says it serves both the teams that build apps and the people who use them. Without rules, teams copy and change parts, and the system splits apart.

What are design tokens, and why keep them as data?

A token is a design choice with a name, like color-brand for #0f766e. Components use the name, never the raw value. Kept as data, one file can become CSS and code for other platforms.

A strong candidate adds that a token can point to another token, as Primer’s functional tokens point to base tokens. Then a dark theme only changes where they point. Tokens also have versions, like code: a new name breaks apps that use the old one.

What makes a component API hard to use wrongly?

The same names everywhere, like value, defaultValue and onValueChange. Controlled and uncontrolled use. Unknown props and ref passed on, with className and style merged. Required props where accessibility needs them, like label. Safe defaults, like type="button".

A strong candidate adds composition: small parts that fit together, not a new prop for every request.

Which changes to a design system are breaking?

Any change after which working code stops working, or works differently. Removing a prop or changing its name. Changing a default, like adding type="button". Making an optional prop required. Changing a token’s name. Changing tags or class names that apps style or test.

A strong candidate says all of these need a major version. A ^ range takes new minor versions on its own.

How do you remove a prop without breaking fifty apps?

First, a minor version. Add the new prop and keep the old one working. Mark it @deprecated, and warn once in development. Ship a codemod and a changelog entry. Then remove it in the next major version.

A strong candidate uses process.env.NODE_ENV for the warning in a package, since a library build fixes import.meta.env.DEV too early. They also check who still uses the old prop before the major version.

Fifty teams use your design system, and there are already three different buttons. How do you stop it from happening again?

Make the system the easy path. Give Button a flexible API, so teams don’t need to copy it. Write down how to suggest a change, with clear rules and a fast review. Use semver, warnings, codemods and a changelog for every change.

The other two buttons live in the teams’ own apps. You can’t deprecate code you don’t own. So add variants to the system’s Button for what those buttons really do. Then help the teams move: a codemod, a short guide, and time. Track how many teams have moved, and talk to the ones who haven’t.

A strong candidate says the work is mostly about people. A slow review makes teams build their own parts.

Sources

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.