When you hear “Scalable Design System with a Monorepo Ecosystem” it might sound like a bunch of jargon glued together. Let’s simplify:

  • Design system: the building blocks of your product (buttons, inputs, styles, tokens, patterns).
  • Monorepo: one big repo with multiple packages living together, sharing tooling and workflows.

When you combine them, you get modularity, consistency, and a faster development cycle — the dream setup for teams working across web, mobile, and beyond.

In this article, you’ll learn how to build a modular, scalable design system using React and Turborepo — the same approach used by Microsoft, IBM, and Shopify.

Prerequisites

Before you follow along, you’ll want to have a few things in place:

  • Working knowledge of React and TypeScript: You should be comfortable creating components and reading basic type annotations.
  • Familiarity with the command line: You’ll run npx, npm, and similar commands throughout.
  • Node.js installed (v18 or later): Verify with node -v. If you don’t have it, install it from nodejs.org.
  • A package manager: This guide uses npm, but pnpm or yarn will work with minor command tweaks.
  • A code editor of your choice (VS Code is a popular fit for TypeScript work).

You don’t need any prior experience with monorepos or Turborepo. We’ll set everything up from scratch.

Who’s Already Doing This?

Some of the biggest design systems you’ve heard of run inside monorepos:

  1. Microsoft Fluent UI: lives in a multi-package monorepo that ships React components, Web Components, and even design tokens.
  2. IBM Carbon: multiple packages like @carbon/ibm-products come straight out of their Carbon monorepo.
  3. Shopify Polaris: openly describes itself as a monorepo, packaging React components, docs, and even a VS Code extension.
  4. Atlassian Atlaskit: their public @atlaskit/* packages are published from a large internal monorepo.
  5. MUI (Material UI): maintained as a mono-repository to coordinate React components, tooling, and docs.
  6. Elastic EUI: developed and released from a single repo, with discussions about monorepo publishing flows.

Why it Works

When you put all the pieces of your design system in one repository, you get a few specific advantages that are hard to replicate in a split-repo setup. Each of these reinforces the others, which is why teams that adopt this pattern rarely go back.

Here’s what makes it work:

  • Consistency: tokens, styles, and primitives are defined once and flow everywhere.
  • Faster iteration: fix a bug in Button and the updates cascade to mobile, desktop, and docs instantly.
  • Shared tooling: linting, tests, CI pipelines, and release workflows are configured once, and then applied to all packages.
  • Versioning control: with tools like Changesets or Lerna, you can release packages independently but keep them aligned.
  • Cross-platform flexibility: the same building blocks can power React web apps, React Native, Electron apps, SDKs, and documentation sites.

Think of it Like a Ladder 🪜

The cleanest way to picture a monorepo design system is as a series of stacked layers. Each layer builds on the one beneath it, and each layer has a clear job.

New contributors find their way around faster because the relationships between packages are predictable: tokens flow up into primitives, primitives compose into layouts, and layouts assemble into screens.

The diagram below shows this stack visually:

Layered architecture of a monorepo design system: design tokens at the base, then plugins (utility helpers), then layouts, then screens, then navigators at the top, with the app shell consuming a single package that pulls all layers together

At the base, you’ve got primitives (tokens, styles).

Above that: plugins (utility helpers).

Then come layouts, built from plugins + primitives.

Then screens, built from layouts.

Finally, navigators tie screens together.

At the very top, your app imports just one package, and the UI is environment-agnostic.

The Same Design System, Everywhere

The real payoff of this ladder is that you climb it once, then reuse the whole thing across every platform you ship to.

A button defined in your primitives package can render in a web app, a React Native mobile app, an Electron desktop app, or a documentation site without you rewriting it for each environment.

The diagram below shows the same design system flowing into three different app types, with each environment importing the same package and getting consistent styling, behaviour, and accessibility out of the box:

The same design system feeding three different apps from a single import: a web application on a browser, a desktop application in an Electron-style window, and a mobile application on a phone screen. Each app pulls from the shared primitives and tokens packages, ensuring buttons, typography, and spacing look and behave the same everywhere

Whether it’s web, desktop, or mobile, the design system climbs that same ladder.

Should You Go Monorepo?

Not every team needs one. But if you’re building a design system that’s meant to serve multiple apps, stay consistent across platforms, and support lots of contributors, then a monorepo becomes less of a buzzword and more of a sanity-saver.

When a Monorepo Is Not the Right Fit

A quick clarification first, because monorepos sometimes get tangled up with another debate. The “monorepo vs polyrepo” question is not the same as the “monolith vs microservices” question. You can absolutely run microservices out of a monorepo (Google and Facebook do this at massive scale).

The two choices live on different axes: monorepo vs polyrepo is about where the code lives, while monolith vs microservices is about how the runtime is shaped.

With that out of the way, here are a few signs a monorepo may not be the best fit for your situation:

  • You’re a small team shipping a single product. The tooling overhead of a monorepo (workspace config, build pipelines, package boundaries) may slow you down more than it helps. A single React app with no shared libraries probably doesn’t need this layer.
  • Your packages have wildly different release cadences and stakeholders. If two parts of your codebase are owned by teams that need very different deploy pipelines, governance, or security postures, separate repos can reduce friction.
  • You can’t invest in monorepo tooling. Tools like Turborepo, Nx, and Changesets do a lot of heavy lifting, but they have a learning curve. If your team can’t dedicate time to set them up and maintain them, you may struggle.
  • You’re using languages or runtimes that don’t share well. Monorepos shine when most packages live in the same toolchain. Mixing Node, Go, Rust, and Python in one repo is possible, but the build-tool story gets harder.

For most teams building a serious design system, none of these are dealbreakers. But it’s worth checking your situation before committing.

Let’s Build Our Design System

Create Your Turborepo Project

Bootstrap a new Turborepo workspace by running the following command and following the prompts:

npx create-turbo@latest

When asked for a project name, use something like my-design-system. Select npm workspaces as your package manager unless you prefer pnpm or yarn.

Design Your Package Structure

A well-structured monorepo design system separates concerns into distinct packages. Your workspace should look something like this:

my-design-system/
├── packages/
│   ├── tokens/          # Design tokens (colors, spacing, typography)
│   ├── primitives/      # Base React components (Button, Input, etc.)
│   └── layouts/         # Composed layout components
├── apps/
│   └── web/             # A consuming application
├── turbo.json
└── package.json

Each package has its own package.json and can be versioned and published independently.

Build Your Design Tokens Package

Design tokens are the foundation of your system — the single source of truth for colors, spacing, typography, and other visual decisions.

Inside packages/tokens, create an index.ts file:

export const colors = {
  primary: '#0070f3',
  secondary: '#1a1a1a',
  background: '#ffffff',
  text: '#333333',
};

export const spacing = {
  xs: '4px',
  sm: '8px',
  md: '16px',
  lg: '24px',
  xl: '32px',
};

export const typography = {
  fontFamily: "'Inter', sans-serif",
  fontSize: {
    sm: '14px',
    md: '16px',
    lg: '20px',
  },
};

Then update packages/tokens/package.json to declare the entry point:

{
  "name": "@yourds/tokens",
  "version": "0.0.1",
  "main": "index.ts"
}

Create Primitive Components

With tokens in place, build your first primitive component. Inside packages/primitives, create a Button.tsx:

import { colors, spacing, typography } from '@yourds/tokens';
import React from 'react';

type ButtonProps = {
  label: string;
  onClick?: () => void;
  variant?: 'primary' | 'secondary';
};

export const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => {
  const background = variant === 'primary' ? colors.primary : colors.secondary;

  return (
    <button
      onClick={onClick}
      style={{
        background,
        color: '#fff',
        padding: `${spacing.sm} ${spacing.md}`,
        fontFamily: typography.fontFamily,
        fontSize: typography.fontSize.md,
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
      }}
    >
      {label}
    </button>
  );
};

Update packages/primitives/package.json:

{
  "name": "@yourds/primitives",
  "version": "0.0.1",
  "main": "index.ts",
  "dependencies": {
    "@yourds/tokens": "*"
  }
}

Create packages/primitives/index.ts to re-export all components:

export { Button } from './Button';

Configure the Turborepo Pipeline

The turbo.json file at the root of your workspace tells Turborepo how tasks relate to each other and which outputs to cache:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "lint": {},
    "dev": {
      "cache": false
    }
  }
}

The "dependsOn": ["^build"] directive ensures that if primitives depends on tokens, Turborepo builds tokens first — automatically, every time.

Build the @yourds Packages

With your pipeline configured, build all packages from the root:

npm run build

Turborepo resolves the dependency graph and runs builds in the correct order, caching outputs so unchanged packages are never rebuilt unnecessarily.

Use Your Design System in an App

Inside apps/web, add the primitives package as a dependency in package.json:

{
  "dependencies": {
    "@yourds/primitives": "*"
  }
}

Run npm install from the root to link the workspace packages, then use the component in your app:

import { Button } from '@yourds/primitives';

export default function HomePage() {
  return (
    <main>
      <h1>My App</h1>
      <Button label="Get Started" variant="primary" />
    </main>
  );
}

Your app now consumes a fully typed, token-driven component from your shared design system — and any update made to tokens or primitives will automatically be reflected here on the next build.

Wrapping Up

Building a scalable design system inside a monorepo is the approach used by some of the largest engineering teams in the world for good reason: it keeps your tokens, components, and tooling aligned as your product grows. With Turborepo handling the build pipeline and a clear layered architecture separating tokens from primitives from layouts, you have a foundation that scales from a single web app today to a cross-platform product suite tomorrow.