How OpenCut Organizes and Customizes shadcn/ui Components: Architecture and Patterns

OpenCut centralizes its shadcn/ui configuration in a single components.json file, uses path aliases for clean imports, and wraps primitives with a cn utility to enable type-safe Tailwind class merging and flexible variant systems.

The OpenCut repository implements shadcn/ui as its foundational design system for reusable UI primitives, creating a maintainable component architecture that separates configuration from implementation. This approach provides a single source of truth for styling while allowing developers to extend and customize components without modifying core library code. Understanding the organization and customization of shadcn/ui components in OpenCut reveals how the project balances consistency with flexibility.

Centralized Configuration via components.json

OpenCut drives its entire shadcn/ui integration through a configuration file located at apps/web/components.json. This JSON schema defines project-wide settings that govern how components are generated, styled, and imported throughout the application.

The configuration specifies style: "base-mira" as the visual preset, enables TypeScript support with tsx: true, and explicitly disables React Server Components via rsc: false. The Tailwind integration points to src/styles.css for global styles and enables CSS variables for theming. By setting rsc: false, OpenCut ensures all UI primitives render exclusively on the client, avoiding the complexity of mixing server and client component boundaries in the current Next.js and React Router setup.

Path Aliases and Import Standards

The aliases section in apps/web/components.json establishes short import paths that eliminate lengthy relative traversals. Developers can import components using patterns like import { Button } from "#/components/ui/button" rather than deep relative paths such as ../../../components/ui/button.

This alias system covers ui, utils, hooks, and other common directories, creating a consistent import experience across the monorepo. The #/ prefix denotes project-root-relative imports, making refactors safer and code more readable.

The cn Utility and Tailwind Class Management

At apps/web/src/lib/utils.ts, OpenCut defines a cn helper function that serves as the backbone of all component styling. This thin wrapper combines clsx for conditional class construction with tailwind-merge for intelligent class deduplication.

Every UI component uses this utility to merge its internal variant classes with consumer-provided className props. The cn function ensures that Tailwind utilities are merged correctly without style duplication, respecting specificity and override order. This pattern allows developers to pass custom Tailwind classes to any component while preserving the base design system's integrity.

Component Architecture

Wrapping Primitives with data-slot

OpenCut wraps each underlying primitive from @base-ui/react/* in a lightweight component that injects a data-slot attribute and applies the cn helper. This pattern appears in files like apps/web/src/components/ui/tooltip.tsx, where the public API remains identical to standard shadcn/ui while allowing OpenCut to attach custom data attributes for styling hooks or automated testing.

The wrapper approach maintains full compatibility with the base shadcn ecosystem while providing extension points for project-specific requirements.

Type-Safe Variants with CVA

Components requiring multiple visual states, such as apps/web/src/components/ui/button.tsx, implement class-variance-authority (CVA) for variant management. The cva function defines variant and size options, mapping each to specific Tailwind class strings within a type-safe object.

Default variants are declared within the same definition, ensuring unstyled components still receive appropriate base classes. This system creates a strict contract between design and implementation, catching invalid variant combinations at compile time while generating clean, deterministic class strings at runtime.

Practical Customization Examples

Basic Usage with Aliases

Import components using the configured path aliases for clean, maintainable code:

import { Button } from "#/components/ui/button"

export default function Example() {
  return <Button>Click me</Button>
}

Applying Variants

Utilize the CVA-defined variants for consistent styling across sizes and intents:

import { Button } from "#/components/ui/button"

export default function Example() {
  return (
    <Button variant="destructive" size="lg">
      Delete
    </Button>
  )
}

Overriding Tailwind Classes

Pass custom classes through the className prop, which the cn utility merges with base styles:

import { Button } from "#/components/ui/button"

export default function Example() {
  return (
    <Button className="bg-green-600 hover:bg-green-700">
      Save
    </Button>
  )
}

Working with Wrapped Primitives

UseTooltip components follow the standard shadcn pattern while respecting OpenCut's wrapper architecture:

import {
  Tooltip,
  TooltipTrigger,
  TooltipContent,
} from "#/components/ui/tooltip"

export default function Example() {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button>Hover me</Button>
      </TooltipTrigger>
      <TooltipContent side="right">
        This is a tooltip
      </TooltipContent>
    </Tooltip>
  )
}

Extending Component Variants

Add project-specific variants by extending the base CVA configuration:

import { buttonVariants } from "#/components/ui/button"
import { cva } from "class-variance-authority"

const extendedButton = cva(buttonVariants(), {
  variants: {
    variant: {
      warning: "bg-yellow-400 text-yellow-900 hover:bg-yellow-500",
    },
  },
  defaultVariants: { variant: "warning" },
})

export function WarningButton(props) {
  return <button className={extendedButton({})} {...props} />
}

Summary

  • Centralized configuration: The apps/web/components.json file controls styling presets, TypeScript settings, and React Server Component behavior for the entire component library.
  • Clean imports: Path aliases like #/components/ui/button eliminate relative path fragility and improve code readability.
  • Safe class merging: The cn utility in apps/web/src/lib/utils.ts uses clsx and tailwind-merge to handle dynamic class combinations without conflicts.
  • Type-safe variants: CVA (class-variance-authority) powers the variant system in components like button.tsx, enabling compile-time checks for style combinations.
  • Extensible wrappers: Components wrap @base-ui/react/* primitives to inject data-slot attributes while maintaining standard shadcn/ui APIs.

Frequently Asked Questions

Where is the shadcn/ui configuration stored in OpenCut?

OpenCut stores its shadcn/ui configuration in apps/web/components.json. This file defines the styling preset (base-mira), Tailwind CSS settings, path aliases, and whether React Server Components are enabled (rsc: false).

How does OpenCut handle Tailwind class merging?

OpenCut uses a cn utility function defined in apps/web/src/lib/utils.ts that combines clsx for conditional classes with tailwind-merge for deduplication. Every UI component passes its className prop through this function, ensuring consumer overrides merge correctly with base styles without duplication.

Can I add custom variants to existing components?

Yes. You can extend the base variant definitions using class-variance-authority. Import the existing buttonVariants (or similar) from the component file, then call cva() with additional variant options. This pattern preserves type safety while adding project-specific styles like warning states or custom sizes.

Why does OpenCut disable React Server Components for UI primitives?

The rsc: false setting in components.json forces all shadcn/ui components to render as client components. This decision aligns with OpenCut's current Next.js and React Router architecture, avoiding the complexity of managing server/client component boundaries and ensuring consistent behavior across interactive elements like tooltips and dropdowns.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →