A design system is not a collection of pretty buttons. It's a contract between your frontend team and the rest of the organization: tested, documented, versioned components. With Angular and Tailwind, you have the tools to do it right, but the pitfalls are real.
Structure: Monorepo Workspace or Standalone Library?
Two approaches dominate. First, the monorepo with Nx: isolate your design system in a library @myorg/ui , versioned independently, consumed by multiple Angular apps. It's scalable, but demands discipline—avoid circular dependencies, manage peer dependencies rigorously. Second approach: a separate npm library, consumed as an external dependency. Heavier to maintain locally, but offers true separation of concerns.
Tailwind adapts well to both. In a monorepo, configure a single tailwind.config.ts at the UI library level, with your color tokens, spacing, typography. Each consuming app extends this config for its own needs. Don't duplicate tokens.
Components: Angular Components + Tailwind Utilities
The temptation here is to inline Tailwind classes directly in templates. Bad idea at scale. Prefer reusable components with inputs for variants: <app-button [variant]="'primary'" [size]="'lg'">Submit</app-button> .
The component encapsulates the Tailwind logic:
@Component({
selector: 'app-button',
template: `<button [ngClass]="buttonClasses"><ng-content></ng-content></button>`
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' = 'primary';
@Input() size: 'sm' | 'md' | 'lg' = 'md';
get buttonClasses(): string {
const baseClasses = 'font-semibold rounded transition';
const variantClasses = this.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-gray-200 text-gray-900';
const sizeClasses = this.size === 'lg' ? 'px-6 py-3 text-lg' : 'px-4 py-2';
return `${baseClasses} ${variantClasses} ${sizeClasses}`;
}
} Verbose, but maintainable. Alternatively, explore Tailwind's @apply directives, but sparingly—they can become an anti-pattern if used to work around composition limitations.
Documentation and Storybook
A design system without documentation is dead code. Storybook is your friend. Set it up with Angular and Tailwind (integration is straightforward), then document every component with interactive stories. Show variants, states (focus, disabled, loading), edge cases.
import { Meta, StoryObj } from '@storybook/angular';
import { ButtonComponent } from './button.component';
const meta: Meta<ButtonComponent> = {
component: ButtonComponent,
title: 'Components/Button'
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: { variant: 'primary', size: 'md' }
};
export const Secondary: Story = {
args: { variant: 'secondary', size: 'lg' }
};Versioning and Consumption
Publish your library with strict semver. If you change a component's API (input rename, variant removal), that's a breaking change. Document migrations. Your consumers must upgrade with confidence.
In your apps, pin the version: @myorg/ui@~1.5.0 . Test new versions in staging before production. Design systems are shared technical debt—neglecting updates creates fragmentation.
Performance and Bundle Size
Tailwind generates a lot of CSS. With PurgeCSS (enabled by default), only used classes are included. But if you publish a library, ensure the consumer also purges against their own code. Document this clearly.
For icons, integrate a lightweight library like Feather Icons or Material Icons, not the entire set. Lazy-load heavy components (modals, datepickers) when possible.
A well-designed design system saves weeks of development. The upfront cost is real, but payoff is fast once two or three apps consume it.