The ui package is a React component library built with shadcn/ui and Tailwind CSS. It provides pre-built, accessible components that can be used across applications in the monorepo.
Location: packages/ui/
Features:
- 🎨 Built with shadcn/ui
- ♿ Accessible components (built on Radix UI)
- 🎯 Tailwind CSS integration
- 📦 Bundled and distributable
- 🔧 Customizable via Tailwind config
- 📱 Responsive by default
packages/ui/
├── src/
│ ├── components/ # React components
│ │ ├── button.tsx # Button component
│ │ ├── card.tsx # Card component
│ │ └── index.ts # Component exports
│ ├── tailwind/ # Tailwind configuration
│ │ └── theme.css # Theme styles
│ ├── utils/ # Utility functions
│ │ └── cn.ts # Class name utility
│ ├── index.ts # Main exports
│ └── main.tsx # Dev preview entry
├── components.json # shadcn/ui configuration
├── package.json
├── tsconfig.json
├── tsup.config.ts # Build configuration
└── vite.config.ts # Dev server configuration
The webapp already has the UI package installed. For new apps:
pnpm add @react-router-gospel-stack/ui --filter=@react-router-gospel-stack/your-appimport { Button, Card, CardHeader, CardContent } from "@react-router-gospel-stack/ui";
export function MyComponent() {
return (
<Card>
<CardHeader>
<h2>Welcome</h2>
</CardHeader>
<CardContent>
<p>This is a card component</p>
<Button>Click me</Button>
</CardContent>
</Card>
);
}The package includes common shadcn/ui components:
- Button - Interactive button with variants
- Card - Container for content with header/footer
- More components can be added as needed
See the shadcn/ui documentation for component APIs.
The easiest way to add new components:
cd packages/ui
pnpm dlx shadcn-ui@latest add <component-name>Example:
cd packages/ui
pnpm dlx shadcn-ui@latest add dialogThis will:
- Download the component source
- Add it to
src/components/ - Install any required dependencies
- Update the component registry
If you prefer to add components manually:
-
Create the component file:
// src/components/my-component.tsx import * as React from "react"; import { cn } from "../utils/cn"; export interface MyComponentProps extends React.HTMLAttributes<HTMLDivElement> { variant?: "default" | "outline"; } export const MyComponent = React.forwardRef< HTMLDivElement, MyComponentProps >(({ className, variant = "default", ...props }, ref) => { return ( <div ref={ref} className={cn( "base-styles", variant === "outline" && "outline-styles", className )} {...props} /> ); }); MyComponent.displayName = "MyComponent";
-
Export from components index:
// src/components/index.ts export * from "./my-component";
-
Export from package root:
// src/index.ts export * from "./components";
-
Rebuild the package:
pnpm run build --filter=@react-router-gospel-stack/ui
The UI package exports a Tailwind preset that includes the theme configuration.
In your app's tailwind.config.ts:
import type { Config } from "tailwindcss";
import uiPreset from "@react-router-gospel-stack/ui/tailwind";
export default {
content: [
"./app/**/*.{ts,tsx}",
// Include UI package components
"./node_modules/@react-router-gospel-stack/ui/dist/**/*.js",
],
presets: [uiPreset],
theme: {
extend: {
// Your custom theme extensions
},
},
} satisfies Config;To customize the theme, edit the CSS variables in src/tailwind/theme.css:
@layer base {
:root {
/* Update these values to customize your theme */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* ... more variables */
}
.dark {
/* Dark mode variables */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... more variables */
}
}These CSS variables are used by the components for theming.
The cn utility combines class names and handles Tailwind conflicts:
import { cn } from "@react-router-gospel-stack/ui";
// Merge classes with proper precedence
const className = cn(
"base-styles",
isActive && "active-styles",
"override-styles",
);It uses clsx and tailwind-merge under the hood.
The UI package includes a Vite dev server for developing components in isolation:
pnpm run dev --filter=@react-router-gospel-stack/uiThis starts a dev server at http://localhost:5173 (or next available port) where you can preview components.
Edit src/app.tsx to preview your components:
import { Button, Card } from "./components";
export function App() {
return (
<div className="p-8 space-y-4">
<h1 className="text-2xl font-bold">UI Component Preview</h1>
<Card>
<h2>Buttons</h2>
<div className="flex gap-2">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
</Card>
</div>
);
}pnpm run build --filter=@react-router-gospel-stack/uiThis uses tsup to:
- Bundle TypeScript to JavaScript
- Generate type definitions
- Output to
dist/
The build is configured in tsup.config.ts:
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
index: "src/index.ts",
components: "src/components/index.ts",
tailwind: "src/tailwind/index.ts",
},
format: ["cjs", "esm"],
dts: true,
clean: true,
external: ["react", "react-dom"],
});This creates:
dist/index.js- Main entry pointdist/components/- Component exportsdist/tailwind/- Tailwind config exports- Type definitions for all exports
shadcn/ui components are built on Radix UI, which provides excellent accessibility out of the box:
- Keyboard navigation
- Screen reader support
- Focus management
- ARIA attributes
Always maintain these features when customizing:
// ✅ Good - preserves accessibility
<Button onClick={handleClick} aria-label="Close dialog">
Close
</Button>
// ❌ Bad - removes accessibility features
<div onClick={handleClick}>Close</div>For components with multiple variants, consider using class-variance-authority:
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../utils/cn";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input bg-background",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4",
lg: "h-11 px-8 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
);Always export component prop types:
export interface MyComponentProps {
title: string;
description?: string;
onClose?: () => void;
}
export const MyComponent: React.FC<MyComponentProps> = ({ ... }) => {
// ...
};This allows consumers to:
import type { MyComponentProps } from "@react-router-gospel-stack/ui";
// Use the type in their code
const props: MyComponentProps = { ... };Use React.forwardRef for components that need ref access:
export const MyComponent = React.forwardRef<
HTMLDivElement,
MyComponentProps
>((props, ref) => {
return <div ref={ref} {...props} />;
});
MyComponent.displayName = "MyComponent";Prefer Tailwind utility classes over custom CSS:
// ✅ Good
<div className="flex items-center gap-4 p-4 rounded-lg bg-white">
// ❌ Avoid
<div style={{ display: 'flex', alignItems: 'center', ... }}>Use Tailwind's responsive prefixes:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Responsive grid */}
</div>Use Tailwind's dark mode classes:
<div className="bg-white dark:bg-slate-900 text-black dark:text-white">
{/* Supports dark mode */}
</div>Dark mode is configured in tailwind.config.ts:
export default {
darkMode: ["class"], // Uses class-based dark mode
// ...
};Toggle dark mode by adding/removing the dark class on the root element.
Write unit tests using Vitest and Testing Library:
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { Button } from "./button";
describe("Button", () => {
it("renders with text", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await userEvent.click(screen.getByText("Click me"));
expect(handleClick).toHaveBeenCalledOnce();
});
});For visual regression testing, consider:
If you want to publish the UI package to npm:
-
Update package.json:
{ "name": "@your-org/ui", "version": "1.0.0", "publishConfig": { "access": "public" } } -
Build the package:
pnpm run build --filter=@react-router-gospel-stack/ui
-
Publish:
cd packages/ui npm publish
When shadcn/ui releases updates:
-
Update the CLI:
pnpm dlx shadcn-ui@latest
-
Check for component updates:
cd packages/ui pnpm dlx shadcn-ui@latest diff -
Update individual components:
pnpm dlx shadcn-ui@latest add <component-name> --overwrite
-
Test thoroughly - Updates may include breaking changes
If Tailwind classes aren't being applied:
- Verify the content paths in your app's
tailwind.config.ts - Ensure the UI package is built:
pnpm run build --filter=ui - Restart your dev server
If you see type errors:
# Rebuild the UI package
pnpm run build --filter=@react-router-gospel-stack/ui
# Rebuild consuming apps
pnpm run build --filter=@react-router-gospel-stack/webapp...If imports aren't working:
- Check the component is exported in
src/components/index.ts - Check it's re-exported in
src/index.ts - Rebuild:
pnpm run build --filter=ui
- Browse shadcn/ui components to add more
- Customize the theme in
src/tailwind/theme.css - Build your own custom components using the patterns shown
- Check the Architecture Guide to understand how packages fit together