Creating and managing design variants in Nx is a fundamental capability for teams building scalable, multi-experience applications within a monorepo. Design variants—whether for A/B testing, feature flags, brand theming, or user-personalized interfaces—require a structured approach to avoid code duplication, maintain consistency, and keep build times fast. Nx, as a smart monorepo tool with advanced build orchestration and dependency graph awareness, provides several proven techniques to handle design variants efficiently. This article explores the best methods for creating and managing design variants in Nx, with practical examples and actionable best practices.

Understanding Design Variants in Nx

Design variants refer to multiple versions of a UI component, style set, or layout that can be switched dynamically or at build time. In a typical Nx workspace, you might have a shared UI library used by several applications. Without a solid variant strategy, you risk either duplicating code across apps or introducing complex conditional logic that becomes brittle.

Design variants enable use cases such as:

  • A/B testing – Serving different button styles or layouts to user cohorts.
  • White-labeling – Each client gets a custom color scheme and logo.
  • Feature previews – Rolling out a new design to a percentage of users.
  • Platform-specific UIs – Mobile vs. desktop, or light/dark mode.

Nx’s architecture—with its project boundaries, dependency graph, and affected commands—makes it well-suited to manage these scenarios without breaking the build or bloating your codebase.

Methods for Creating Design Variants

1. Using Environment Files and Build-Time Variables

One of the simplest and most reliable methods is to inject design variant information via environment files. Nx supports environment-specific configurations using .env files and the environments object in your project.json or workspace.json.

For example, you might have:

  • .env.production – Contains APP_THEME=corporate
  • .env.staging – Contains APP_THEME=startup

Then in your component or CSS, reference process.env['NX_APP_THEME'] (or the Nx-compatible NX_ prefix). This approach is clean and works with any frontend framework. For style variants, you can conditionally import a theme stylesheet:

if (theme === 'corporate') {
  import('./corporate-theme.scss');
} else {
  import('./startup-theme.scss');
}

Nx’s build system will tree-shake unused styles, ensuring only the needed variant code is bundled. This method is ideal when variants are known at build time and do not need to switch at runtime.

2. Theming and Style Overrides with CSS Custom Properties

For runtime-switchable variants, CSS custom properties (CSS variables) are a powerful, low-cost solution. Define a set of base variables in a shared stylesheet, then override them per variant. In an Nx workspace, you can create a themes library that exports theme objects (e.g., corporate.css, startup.css).

Integrate with Nx’s build process by importing the appropriate theme in your application’s entry point. For React or Angular, you can use a context/provider to dynamically apply a theme class to the root element:

.theme-corporate {
  --primary-color: #0055a5;
  --secondary-color: #ff6600;
}
.theme-startup {
  --primary-color: #6c63ff;
  --secondary-color: #ff6584;
}

Then in components, reference var(--primary-color). This approach is lightweight and works beautifully with Tailwind CSS if you use the darkMode: 'class' strategy—extend it to support multiple themes.

For more complex CSS-in-JS setups (e.g., styled-components or Emotion), create a theme object and pass it via React context or Vue provide/inject. Nx’s library boundaries let you share this theme logic across apps without duplication.

3. Component Variants through Props and Slots

When design differences go beyond colors and spacing—such as layout rearrangement or additional elements—leveraging component variants via props (React) or slots (Vue) is effective. For example, a Button component can accept a variant prop:

function Button({ variant, children }) {
  const className = variant === 'primary' ? styles.primary : styles.secondary;
  return <button className={className}>{children}</button>;
}

Nx encourages you to keep such components in a shared UI library. When variants become numerous, consider using a variant registry pattern: store variant configurations in a JSON object and map them to component props. This method is clean and testable.

For larger differences, composition is better than conditionals. Create separate sub-components (e.g., PrimaryButton, GhostButton) that share a common base. Use Nx’s dependency graph to ensure the base library is shared and only changed when necessary.

4. Feature Flags and Runtime Toggles

For design variants that need to be switched server-side or for a subset of users, integrating a feature flag service (like LaunchDarkly or Unleash) with Nx is a robust solution. Create a dedicated feature-flags library that abstracts the flag provider. Each application imports this library and checks flags to render different designs.

Example using a simple React hook:

import { useFeatureFlag } from '@myorg/feature-flags';
function HomePage() {
  const newLayout = useFeatureFlag('new-layout');
  return newLayout ? <NewLayout /> : <OldLayout />;
}

Nx’s project configuration allows you to mock flags during development and testing. You can create separate Nx targets for different flag scenarios:

"targets": {
  "serve-with-flags": { ... },
  "test-flags": { ... }
}

This keeps your variant logic isolated and easy to toggle without redeploying the entire app.

Managing Design Variants Effectively

Organize Variants with a Consistent Folder Structure

Keep your workspace tidy by grouping variant-related files. For example:

libs/
  ui/
    button/
      src/
        lib/
          variants/
            primary/
            secondary/
            ghost/
      index.ts

Each variant folder contains its own styles, tests, and stories. This approach makes it easy to run nx affected:test only on the changed variant. Nx’s tags (e.g., type:ui, scope:shared) let you enforce boundaries so that an app using “primary” cannot accidentally depend on “ghost” internals.

Leverage Nx’s Affected Commands for Variant Changes

When you modify one variant, you don’t want to rebuild or test every app. Nx’s nx affected:build, nx affected:test, and nx affected:lint automatically detect which projects are impacted based on the dependency graph. This is especially powerful in a monorepo with many design variants—only the variant that changed triggers its pipeline.

For example, if you update only the “primary” button variant, Nx will schedule builds for libraries and applications that depend on that variant, leaving others untouched. This saves significant CI time.

Name Variants Consistently and Document Differences

Standard naming conventions such as primary, secondary, ghost, or brand-a, brand-b make variants predictable. Use a README.md inside each variant folder to explain the purpose, visual differences, and when to use each. For shared design tokens, maintain a single source of truth—like a design-tokens library—that all variants reference.

Automate Variant Testing

Use Nx’s testing generators to create unit tests for each variant. Integrate visual regression testing tools like Chromatic or Percy. In your CI pipeline, use nx affected to run visual tests only for changed variants. Configure Lighthouse CI to compare performance across variants.

For example, add a separate target for variant tests:

"test:variant": {
  "executor": "@nrwl/jest:jest",
  "options": {
    "jestConfig": "libs/ui/button/variants/primary/jest.config.ts"
  }
}

Then orchestrate with a shell script or Nx run-commands to test all variants.

Best Practices for Managing Design Variants

  • Maintain a shared design token library for colors, spacing, typography. Variants override tokens, not hardcoded values.
  • Use Nx’s project graph to visualize dependencies between variants and apps. Avoid circular dependencies.
  • Version control your variant configurations. Use tags in Git (e.g., variant-corporate) if you need to rollback a specific variant.
  • Document variant lifecycle – When is a variant deprecated? How long does it stay active? Automate cleanup with Nx generators (e.g., nx generate @myorg/schematics:remove-variant).
  • Keep variant logic out of core business code. Use higher-order components, mixins, or decorators to separate concerns.
  • Choose the right granularity – Not every minor style change needs a variant. Reserve variants for meaningful divergences (client branding, experimental features).

Conclusion

Design variants are a reality in modern web development, and Nx provides the tooling to manage them without sacrificing build speed or code quality. Whether you opt for build-time environment files, runtime CSS custom properties, component props, or feature flags, the key is to stay consistent and leverage Nx’s monorepo capabilities—affected commands, project boundaries, and dependency graphs. By adopting these methods and best practices, you can create scalable, flexible applications that adapt to different audiences and business needs. For further reading, explore the Nx environment variables documentation, Tailwind CSS theming, and LaunchDarkly feature flags.