How to Customize shadcn/ui Components for a Stitch Project: Complete Guide
shadcn/ui components in Stitch projects are fully editable source files that you customize through CSS variables, Tailwind configuration, and class-variance-authority (cva) variants without touching node_modules.
Stitch projects treat shadcn/ui as a copy-and-own architecture rather than a traditional dependency. This pattern, detailed in the google-labs-code/stitch-skills repository, gives you direct control over component internals while maintaining separation between base components and project-specific extensions.
Understanding the Stitch shadcn/ui Architecture
Unlike conventional UI libraries, shadcn/ui components in Stitch are copied directly into your project's components/ui/ directory. According to the repository's SKILL.md located at plugins/stitch-build/skills/shadcn-ui/SKILL.md, this approach means you own the source code entirely. The customization workflow follows a specific hierarchy: global theme tokens define your design system, Tailwind extends the utility layer, and class-variance-authority (cva) manages component variants.
The repository's customization guide at plugins/stitch-build/skills/shadcn-ui/resources/customization-guide.md emphasizes maintaining original component files intact while extending functionality through wrappers or configuration files.
Method 1: Theming with CSS Variables
Global Design Tokens in globals.css
The first layer of customization happens in app/globals.css (or your project's global styles). Here, CSS custom properties define your color palette, border radius, and spacing scale. These variables power the Tailwind utility classes that components consume.
Update the :root selector to override default values:
/* app/globals.css */
:root {
/* Override the primary brand color */
--primary: 270 91% 65%; /* brand-purple */
--primary-foreground: 0 0% 100%; /* white text */
/* Adjust border radius globally */
--radius: 0.5rem;
}
For dark mode, add a .dark class block with alternate values:
/* app/globals.css */
.dark {
--primary: 270 60% 50%; /* darker purple for dark mode */
--background: 222 47% 11%;
--foreground: 210 40% 98%;
}
Method 2: Extending Tailwind Configuration
Custom Fonts and Spacing
Beyond CSS variables, granular control lives in tailwind.config.js. Extend the theme to inject custom fonts, animation keyframes, or spacing scales that your shadcn/ui components can reference.
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Poppins', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
},
},
};
Method 3: Adding Component Variants with CVA
Modifying Variant Definitions
shadcn/ui uses class-variance-authority (cva) to define variant combinations. As documented in SKILL.md (lines 58-81), each component exports a cva call that maps variant names to Tailwind class strings. You add new variants by extending this configuration in the component file.
For example, to add a "success" variant to the Button component in components/ui/button.tsx:
// components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
/* New variant addition */
success: "bg-green-600 text-white hover:bg-green-700",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
Usage:
<Button variant="success" size="lg">
Save Changes
</Button>
Method 4: Creating Wrapper Components
The customization guide (lines 98-104) recommends a strict boundary: keep components/ui/* files synchronized with the base shadcn/ui patterns, and create thin wrappers in components/ for project-specific enhancements.
This approach prevents upgrade conflicts while allowing deep customization:
// components/custom-button.tsx
import { Button, ButtonProps } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
export function LoadingButton({
loading,
children,
...props
}: ButtonProps & { loading?: boolean }) {
return (
<Button disabled={loading} {...props}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
);
}
Wrappers compose the base component without modifying the underlying cva definitions, making it safe to update the original shadcn/ui files later.
Enabling Dark Mode
next-themes Integration
Stitch projects implement dark mode through CSS class toggling combined with next-themes. First, wrap your application with the provider:
// app/providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
Then implement a toggle component:
// components/theme-toggle.tsx
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function ThemeToggle() {
const { setTheme, theme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}
The .dark class applied to the HTML element triggers the dark mode CSS variables defined in globals.css, instantly updating all shadcn/ui components.
Summary
- CSS variables in
app/globals.csscontrol global design tokens like colors and radius. - Tailwind configuration in
tailwind.config.jsextends utilities for fonts, spacing, and animations. - CVA variants in
components/ui/*.tsxfiles define component-specific style combinations throughclass-variance-authority. - Wrapper components in
components/provide project-specific logic without contaminating base UI files. - Dark mode relies on
next-themesand CSS class switching, with variables defined alongside light mode tokens.
Frequently Asked Questions
Should I edit files in components/ui directly?
You should modify components/ui/* files only to add new cva variants or fix bugs, as noted in the customization-guide.md (lines 98-104). For project-specific logic, styling, or behavior, create wrapper components in your components/ directory. This preserves the ability to diff and update base components when the upstream shadcn/ui skill updates.
How do I add a new color theme to my Stitch project?
Define your colors as HSL values in CSS variables within app/globals.css. Update the :root selector for light mode and add a .dark selector for dark mode values. Then reference these variables in tailwind.config.js if you need to create semantic Tailwind classes like bg-brand or text-brand-foreground.
What is class-variance-authority (cva) and why does shadcn/ui use it?
class-variance-authority is a utility for creating type-safe variant combinations. It allows shadcn/ui components to accept props like variant="destructive" or size="sm" and compute the correct Tailwind class string. The source code in SKILL.md (lines 58-81) demonstrates how cva merges base styles with conditional variant classes, providing TypeScript autocompletion for props.
How do I prevent customizations from breaking during updates?
Follow the wrapper pattern: keep components/ui/ files as close to the original shadcn/ui source as possible, only adding new cva variants when necessary. Place all business logic, additional props, and composite components in separate files within components/. This isolation ensures that skill updates only require reviewing the base component files, not your entire component library.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →