UI Components

22 shadcn/ui components with Radix primitives, CVA variants, and scoped monorepo imports

Kit includes 22 pre-configured shadcn/ui components built on Radix UI primitives with Class Variance Authority (CVA) for type-safe variant management and Tailwind CSS for styling. Components live in a shared packages/ui/ package and are imported through scoped entry points to prevent cross-app leakage.
This page covers the component architecture, scoped imports, available components, and how to add new ones. For color theming, see Color Themes & Dark Mode. For Tailwind configuration, see Customization.

Component Architecture

The component system is configured through components.json, which tells the shadcn/ui CLI how to install new components:
components.json — shadcn/ui Configuration
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "src/app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "registries": {}
}
Key configuration choices:
  • style: "new-york" — Uses the New York variant of shadcn/ui (more opinionated, slightly different default styling than the "default" style)
  • rsc: true — Components are compatible with React Server Components by default
  • cssVariables: true — Colors reference CSS custom properties (the theme system) instead of hardcoded Tailwind colors
  • baseColor: "neutral" — Neutral gray scale as the foundation color

How Components Work

Every component follows the same pattern: a Radix UI primitive provides accessible, unstyled behavior, CVA defines variant classes, and Tailwind applies the actual styles. The cn() utility (from @nextsaas/utils) merges classes with proper conflict resolution via tailwind-merge.
Radix UI Primitive (accessibility, keyboard nav, ARIA)
    |
    v
CVA Variants (variant="default" | "destructive" | "outline", size="sm" | "default" | "lg")
    |
    v
Tailwind Classes (bg-primary, text-sm, h-10, px-6, ...)
    |
    v
cn() merge (className prop overrides + tailwind-merge conflict resolution)

Scoped Imports

Components are organized into three scopes to prevent boilerplate-specific components from leaking into the marketing site and vice versa:
Import PathContentsUsed By
@nextsaas/ui/sharedShared components onlyAny app
@nextsaas/ui/boilerplateShared + boilerplate-onlyapps/boilerplate/
@nextsaas/ui/marketingShared + marketing-onlyapps/marketing/
The shared scope exports the foundational components used by all applications:
packages/ui/src/shared-index.ts — Shared Exports
/**
 * @nextsaas/ui - Shared Components
 *
 * Components available to ALL applications in the monorepo.
 * These are foundational UI elements used across both boilerplate and marketing apps.
 *
 * @scope shared
 */

// Core Form Components
export * from './button'
export * from './input'
export * from './label'
export * from './select'
export * from './textarea'

// Layout Components
export * from './card'
export * from './sheet'

// Feedback Components
export * from './badge'
export * from './sonner'

// Theme Components
export * from './theme-toggle'

// Internationalization Components
export * from './flag-icon'

Component Categories

Shared Components

Available to all applications via @nextsaas/ui/shared (also included in boilerplate and marketing scopes):
ComponentFileDescription
Buttonbutton.tsxPrimary action element with 8 variants and 5 sizes
Inputinput.tsxText input with consistent border and focus styles
Labellabel.tsxForm label with disabled state support
Selectselect.tsxDropdown select with Radix primitives
Textareatextarea.tsxMulti-line text input
Cardcard.tsxContainer with header, content, footer slots
Sheetsheet.tsxSlide-out panel (mobile nav, sidebars)
Badgebadge.tsxStatus indicator with variant colors
Sonnersonner.tsxToast notification system
ThemeToggletheme-toggle.tsxDark/light mode switch
FlagIconflag-icon.tsxCountry flag icons for i18n

Boilerplate-Only Components

Additional components available via @nextsaas/ui/boilerplate:
packages/ui/src/boilerplate-index.ts — Boilerplate Scope
// ============================================================================

// Alert Components (error messages, notifications in app)
export * from './alert'
export * from './alert-dialog'

// Form Utilities (react-hook-form integration)
export * from './form'

// Modal Components (app modals, confirmations)
export * from './dialog'

// Navigation Components (user menu in dashboard)
export * from './dropdown-menu'

// Progress & Loading Components
export * from './progress'
export * from './skeleton'

// Toast Notifications (in-app feedback)
export * from './toast'
export * from './use-toast'
// Note: toaster.tsx available but not exported to avoid conflict with sonner
ComponentDescription
Alert / AlertDialogError messages, confirmation dialogs
Formreact-hook-form integration with Zod validation
DialogModal windows for in-app interactions
DropdownMenuUser menu, action menus in dashboard
ProgressLoading bars, upload progress indicators
SkeletonLoading placeholder animations
Toast / useToastLegacy toast system (Sonner preferred for new code)

Button Deep-Dive

The Button component demonstrates the CVA pattern used across all components:
packages/ui/src/button.tsx — Button with CVA Variants
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'

import { cn } from '@nextsaas/utils'

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
  {
    variants: {
      variant: {
        default: 'bg-primary text-background dark:text-foreground/90 hover:text-white',
        destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/90',
        accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
        uncolored: 'bg-card text-card-muted-foreground hover:text-card-foreground',
        outline: 'border hover:border-foreground/75',
        ghost: 'border border-transparent bg-primary bg-clip-text text-transparent hover:border-foreground/75 hover:text-foreground',
        link: 'bg-primary bg-clip-text text-transparent underline-offset-8 hover:text-foreground inline flex-none !px-0 !h-auto !w-auto',
      },
      size: {
        sm: 'h-9 px-5 text-xs',
        default: 'h-10 px-6 text-sm',
        md: 'h-11 px-7 text-base',
        lg: 'h-12 px-8 text-base',
        xl: 'h-14 px-11 text-lg',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : 'button'

    // Ensure data-testid is passed through properly
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = 'Button'

export { Button, buttonVariants }

Variant Reference

VariantAppearanceUse Case
defaultSolid primary backgroundPrimary actions (Submit, Save, Create)
destructiveRed backgroundDangerous actions (Delete, Remove)
secondaryMuted backgroundSecondary actions (Cancel, Back)
accentAccent backgroundHighlighted secondary actions
outlineBorder onlyTertiary actions, toggles
ghostGradient text, border on hoverSubtle actions, navigation links
linkGradient text, underline offsetInline text links
uncoloredCard backgroundNeutral actions in card contexts

Usage Examples

tsx
import { Button } from '@nextsaas/ui/boilerplate'

// Primary action
<Button>Save Changes</Button>

// Destructive with size
<Button variant="destructive" size="sm">Delete Account</Button>

// As a link (renders <a> instead of <button>)
<Button asChild variant="link">
  <a href="/docs">Read the Docs</a>
</Button>

// Icon button
<Button variant="ghost" size="icon">
  <SearchIcon />
</Button>
The asChild prop uses Radix's Slot component to merge the Button's styling onto the child element — useful for rendering styled <a> tags or Next.js <Link> components.

Card Pattern

The Card component uses a compound component pattern with separate sub-components for each section:
packages/ui/src/card.tsx — Card Component
const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      'rounded-xl border bg-card text-card-foreground shadow',
      className
    )}
    {...props}
  />
))
Card.displayName = 'Card'
Each part (Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter) is a forwardRef component with cn() for class merging. Use them together:
tsx
import { Card, CardHeader, CardTitle, CardContent } from '@nextsaas/ui/boilerplate'

<Card>
  <CardHeader>
    <CardTitle>Monthly Usage</CardTitle>
  </CardHeader>
  <CardContent>
    <p>You have used 1,234 of 5,000 credits this month.</p>
  </CardContent>
</Card>

Adding New Components

1

Install the component

Use the shadcn/ui CLI to scaffold a new component. Run this from the boilerplate app directory:
bash
cd apps/boilerplate
npx shadcn@latest add accordion
This generates the component file in the configured ui alias path.
2

Move to the packages/ui directory

Move the generated component from the local components/ui/ directory to packages/ui/src/:
bash
mv src/components/ui/accordion.tsx ../../packages/ui/src/accordion.tsx
3

Update the cn import

The generated component imports cn from the local path. Update it to use the shared package:
typescript
// Before (generated by shadcn/ui CLI)
import { cn } from '@/lib/utils'

// After (monorepo shared utility)
import { cn } from '@nextsaas/utils'
4

Register in the scope

Export the component from the appropriate scope index file. For most boilerplate components, add it to packages/ui/src/boilerplate-index.ts:
typescript
// Accordion Component
export * from './accordion'
For components used across all apps, add to shared-index.ts instead.

Project Components

Beyond the shared packages/ui/ components, the boilerplate app has its own components organized by feature:
src/components/
├── about/          # About page sections (hero, team, values, timeline)
├── ai/             # AI chat interface (chat, messages, input, RAG/LLM)
├── auth/           # Authentication (login forms, user button)
├── blog/           # Blog UI (sidebar, tags, like/share buttons)
├── contact/        # Contact page (hero, info, form)
├── credits/        # Credit system (low balance banner, usage counter)
├── dashboard/      # Dashboard UI (sidebar, header, analytics, subscriptions)
├── demo/           # Demo mode (nav item, demo provider)
├── forms/          # Form components (contact form)
├── home/           # Landing page (hero, features, newsletter)
├── layout/         # App layout (navbar, footer)
├── modals/         # Modal dialogs (credits exhausted)
├── payments/       # Payment UI (plan change dialog, pricing error)
├── shared/         # Cross-cutting (hero background)
├── test/           # Test utilities (no-clerk script, config error)
└── upload/         # File upload (drop zone, preview, hooks)
These are app-specific components — they import from @nextsaas/ui/boilerplate for base UI elements and add business logic on top.

Key Files

FilePurpose
components.jsonshadcn/ui CLI configuration (style, paths, aliases)
packages/ui/src/shared-index.tsShared component exports (11 components)
packages/ui/src/boilerplate-index.tsBoilerplate scope exports (shared + 8 additional)
packages/ui/src/marketing-index.tsMarketing scope exports (shared + navigation)
packages/ui/src/button.tsxButton component with 8 CVA variants
packages/ui/src/card.tsxCard compound component (6 sub-components)
packages/utils/src/index.tscn() utility (clsx + tailwind-merge)