engineering-design-and-analysis
How to Manage and Reuse Design Components in Nx
Table of Contents
Managing and reusing design components effectively is one of the most impactful ways to maintain visual consistency, accelerate development velocity, and reduce technical debt in any front-end ecosystem. Within an Nx monorepo, these goals become significantly more attainable because Nx provides built-in mechanisms for sharing code, enforcing boundaries, and scaling teams. This article walks through a production‑grade approach to creating, managing, and reusing design components in an Nx workspace — from initial setup to advanced workflows.
Why Nx Excels for Design Component Management
Nx transforms the often messy task of sharing UI elements into a structured, predictable process. Unlike traditional polyrepos where each application copy‑pastes the same button or input, Nx’s monorepo architecture allows you to centralize design components in a single library. This library can be consumed by any application or other library within the workspace, while Nx’s computation caching and dependency graph ensure that only affected builds and tests are run when a component changes.
Beyond code sharing, Nx enforces module boundaries using tags and lint rules. This means your design system library can explicitly forbid direct imports from application‑specific modules, keeping the design layer pure and reusable. Combined with Nx’s support for modern tooling — including React, Angular, Vue, and Svelte — you get a future‑proof foundation for your UI components.
Setting Up an Nx Workspace for Design Systems
Before you can manage design components, you need a workspace that encourages sharing. If you are starting from scratch, create a new Nx workspace with the framework of your choice. This example uses a React workspace, but the principles apply across all supported frameworks.
npx create-nx-workspace@latest my-design-system-workspace --preset=react-monorepo
Once the workspace is generated, navigate into the directory and verify that the dependency graph is clean.
cd my-design-system-workspace
npx nx graph
Next, generate a dedicated library for your design components. This library will house all reusable UI building blocks. It is a best practice to place it under a shared or design-system directory to reflect its purpose.
nx g @nrwl/react:library --name=ui --directory=shared --importPath=@myworkspace/shared/ui --tags=scope:shared,type:ui
The --tags flag adds metadata that Nx uses for enforcing boundaries. For instance, you can configure a lint rule that prevents applications from depending directly on other applications while allowing them to depend on shared/ui. This keeps the dependency graph healthy as your workspace grows.
Configuring Module Boundaries
Edit .eslintrc.json (or the corresponding ESLint configuration) to define the allowed dependencies. For a design system library, ensure that it only depends on other shared libraries (e.g., shared utility functions) and not on application code.
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:util"]
}
]
}
]
This setup ensures that your design components remain framework‑agnostic and testable in isolation.
Creating and Structuring Components
With the shared/ui library in place, you can start generating components. Use Nx generators to keep the process consistent and to automatically register components in barrel files.
nx g @nrwl/react:component --name=Button --project=shared-ui --export
This command creates a Button component inside the shared/ui library and exports it from the library’s main barrel file (src/index.ts). Repeat for other atomic or composite components such as Input, Card, Header, and Footer.
Organizing Component Files
A well‑structured component folder within a design system library typically contains:
- Component file (e.g.,
Button.tsx) – the main React component with props and logic. - Styles (e.g.,
Button.module.cssor a styled‑components file) – keep styles scoped to avoid global leaks. - Testing file (e.g.,
Button.spec.tsx) – unit and component tests. - Story file (e.g.,
Button.stories.tsx) – for component exploration via Storybook or similar tools. - Type definition file (e.g.,
Button.types.ts) – optional, but useful when props are complex.
By separating concerns, you make each component easier to maintain and review.
Using a Design Token System
For truly reusable design components, hard‑coding colors, spacing, or typography values is a recipe for inconsistency. Instead, adopt a design token system. Create a separate shared library, for example shared/styles, that exports token values as CSS custom properties, JSON, or JavaScript constants.
nx g @nrwl/js:library --name=styles --directory=shared --importPath=@myworkspace/shared/styles --tags=scope:shared,type:util
Inside this library, define tokens like:
// tokens.ts
export const colors = {
primary: '#0F62FE',
secondary: '#6F6F6F',
background: '#FFFFFF',
text: '#161616',
} as const;
export const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
} as const;
Then, within your Button component, import these tokens rather than raw values. This approach makes theming and global updates trivial. When you change a token value, Nx’s dependency graph ensures that every consuming component is rebuilt and retested.
Reusing Components Across Projects
Once your design system library is populated with components, consuming them in any application within the workspace is straightforward. Update the tsconfig.base.json paths to include the library map:
"paths": {
"@myworkspace/shared/ui": ["libs/shared/ui/src/index.ts"],
"@myworkspace/shared/styles": ["libs/shared/styles/src/index.ts"]
}
Now, inside any application (for example, apps/store), import a component:
import { Button } from '@myworkspace/shared/ui';
function CheckoutPage() {
return (
);
}
Because the library is built and distributed internally, you do not need to publish to npm — Nx handles the compilation through its build system. For production, you can bundle the library as a separate artifact or inline it during the application build, depending on your deployment strategy.
Versioning and Breaking Changes
In a monorepo, all code lives in a single repository, which eliminates version mismatch headaches. However, when multiple teams depend on the same design system library, introducing breaking changes must be handled with care.
- Use Nx’s affected commands – Running
nx affected:testornx affected:buildon a feature branch shows exactly which applications and libraries are impacted by a change in a design component. - Semantic versioning through conventional commits – Tag commit messages with
feat:,fix:, orBREAKING CHANGE:. Nx can integrate with tools likesemantic-releaseto automatically version your library if you ever need to publish it externally. - Deprecation warnings – When you must change a component’s API, keep the old version alongside a deprecation notice for one release cycle to give teams time to migrate.
Testing Design Components
Design components are the foundation of your UI, so they must be thoroughly tested. Nx generates a test file for each component by default. Use React Testing Library to write tests that mimic real user interactions.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders children correctly', () => {
render(<Button>Click Me</Button>);
expect(screen.getByText('Click Me')).toBeInTheDocument();
});
it('calls onClick handler when clicked', async () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
await userEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Run tests for only the design system library with:
npx nx test shared-ui
Additionally, consider adding visual regression tests using tools like Chromatic or Percy. These catch unintended style changes that unit tests cannot detect. When a component updates, the CI pipeline can automatically generate snapshots and compare them to the baseline.
Documenting with Storybook
A design system is only as good as its documentation. Storybook is the industry standard for developing and showcasing UI components in isolation. Nx provides first‑class Storybook support.
First, install the Storybook plugin and generate a Storybook configuration for your shared/ui library:
nx g @nrwl/react:storybook-configuration --name=shared-ui --generateStories --configureCypress
This command creates a .storybook folder inside the library and produces a story file for each component. Now you can serve the Storybook locally:
npx nx storybook shared-ui
In the story file, document all prop combinations and edge cases:
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta = {
title: 'Design System/Button',
component: Button,
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
},
};
export default meta;
type Story = StoryObj;
export const Primary: Story = {
args: { variant: 'primary', children: 'Primary Button', size: 'md' },
};
export const Disabled: Story = {
args: { ...Primary.args, disabled: true },
};
Storybook serves as both a living style guide and a reference for other developers. It also integrates with Chromatic to automate visual review and share component demos with designers.
Integrating with Design Tools
To close the loop between design and code, consider exporting design tokens directly from Figma or Sketch. Tools like Style Dictionary can transform design tokens written in JSON into platform‑specific formats (CSS, JS, SCSS). Place these token files in the shared/styles library and re‑export them. When a designer updates a color in Figma, the token file can be regenerated, and Nx will rebuild all affected components and applications.
Another approach is to use Backlight or Supernova to synchronize component metadata between design files and Storybook, but these are optional add‑ons that may incur licensing costs. For most teams, a simple token‑driven system combined with a weekly sync is sufficient.
Best Practices for Scalable Component Management
Based on field experience across large enterprise monorepos, here are actionable best practices to keep your design system healthy:
🔹 Establish a Clear Naming Convention
Consistency in component names reduces cognitive load. Use PascalCase for component names and kebab‑case for file names. For example: PrimaryButton.tsx for the component, primary-button.module.css for styles. Define prefixes for compound components: Card.Header, Card.Body.
🔹 Keep Components Focused
Follow the single‑responsibility principle. If a component grows beyond 200 lines or handles more than two unrelated concerns, split it into smaller sub‑components. This makes them easier to test, reuse, and deprecate independently.
🔹 Write Prop Documentation with TypeScript
Use JSDoc comments on each prop to describe its purpose, allowed values, and any side effects. Modern IDEs will display these hints to developers using the component.
🔹 Version Control All Changes
Treat your design system library like any other codebase. Use feature branches, pull requests, and code reviews. Leverage Nx’s affected:lint and affected:test to ensure nothing is broken before merging.
🔹 Automate Visual Regression Testing
Include Chromatic or Percy in your CI pipeline. Set the build command to npx nx run shared-ui:storybook or directly build the library and run visual snapshots. This catches unintended UI changes before they reach production.
🔹 Plan for Theming
Do not hard‑theme a single look. Build your components to accept a theme object or use CSS custom properties that can be overridden at the application level. This allows white‑label products or dark mode without rewriting the component.
Advanced: Publishing Design Components Outside the Monorepo
In some cases, you may need to share your design system with external teams or the open‑source community. Nx supports building your library as a publishable package. First, convert your library to publishable format:
nx g @nrwl/react:library --name=shared-ui --directory=shared --publishable --importPath=@myworkspace/shared-ui
Then build the library:
nx build shared-ui
The output in dist/libs/shared/ui will contain a package.json, ESM and CJS bundles, and type declarations. You can publish it to a private npm registry or public npm using tools like np or release-it. When external consumers install the package, they get the same tested, documented components you use internally.
Conclusion
Nx provides a robust foundation for managing and reusing design components at any scale. By following the patterns outlined in this article — dedicated shared libraries, token‑driven styles, rigorous testing, Storybook documentation, and module boundary enforcement — you can build a design system that is both maintainable and developer‑friendly. The result is a faster, more consistent development process where teams can focus on building features instead of reinventing UI primitives. Start small with a few core components and iterate as your design system matures. Nx will scale with you, making component management one less problem to worry about.