Build, Test, and Push CFDM Docker Image / test (push) Failing after 1m51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
311 lines
8.0 KiB
Plaintext
311 lines
8.0 KiB
Plaintext
---
|
|
description: Definitive best practices for shadcn/ui — organization, TypeScript, performance, and accessible components.
|
|
globs: **/*
|
|
alwaysApply: false
|
|
---
|
|
|
|
# shadcn Best Practices
|
|
|
|
Definitive guidelines for `shadcn/ui` development and integration. Application source lives under `src/`; components under `src/components/`.
|
|
|
|
## 1. Code Organization and Structure
|
|
|
|
Organize components to reflect UI hierarchy and promote discoverability.
|
|
|
|
**Rule:** Place domain-specific components under `src/components/<domain>` and reusable UI primitives under `src/components/ui`. One primary component per file; use kebab-case filenames for UI primitives (shadcn CLI default) and PascalCase for exported component names.
|
|
|
|
❌ BAD:
|
|
```
|
|
// src/components/Button.tsx
|
|
// src/components/profile-card.tsx
|
|
// src/components/user-settings/index.tsx (multiple components in one file)
|
|
```
|
|
|
|
✅ GOOD:
|
|
```
|
|
// src/components/ui/button.tsx
|
|
// src/components/forms/date-picker.tsx
|
|
// src/components/layout/sidebar.tsx
|
|
|
|
// src/components/forms/index.ts
|
|
export * from "./date-picker";
|
|
export * from "./input";
|
|
```
|
|
|
|
## 2. Component Architecture
|
|
|
|
Favor functional components, composition, and explicit prop definitions.
|
|
|
|
**Rule:** Use functional components with `React.forwardRef` and `asChild` for seamless integration with Radix primitives.
|
|
|
|
❌ BAD:
|
|
```tsx
|
|
// No ref forwarding, no asChild
|
|
const Button = ({ children, onClick }) => (
|
|
<button onClick={onClick}>{children}</button>
|
|
);
|
|
```
|
|
|
|
✅ GOOD:
|
|
```tsx
|
|
// src/components/ui/button.tsx
|
|
import * as React from "react";
|
|
import { Slot } from "@radix-ui/react-slot";
|
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
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",
|
|
},
|
|
size: {
|
|
default: "h-10 px-4 py-2",
|
|
sm: "h-9 rounded-md px-3",
|
|
},
|
|
},
|
|
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";
|
|
return (
|
|
<Comp
|
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
Button.displayName = "Button";
|
|
|
|
export { Button, buttonVariants };
|
|
```
|
|
|
|
## 3. TypeScript and API Design
|
|
|
|
Enforce strict TypeScript with clear interfaces and robust validation.
|
|
|
|
**Rule:** Use interfaces for component props. Validate form data with Zod schemas. Avoid `any` and prefer explicit types.
|
|
|
|
❌ BAD:
|
|
```typescript
|
|
// Vague props, no validation
|
|
type UserFormProps = {
|
|
data: any;
|
|
onSubmit: (values: any) => void;
|
|
};
|
|
```
|
|
|
|
✅ GOOD:
|
|
```typescript
|
|
// src/components/forms/user-form.tsx
|
|
import { z } from "zod";
|
|
|
|
export interface UserFormProps {
|
|
initialData?: UserFormData;
|
|
onSubmit: (values: UserFormData) => void;
|
|
}
|
|
|
|
export const userFormSchema = z.object({
|
|
id: z.string().optional(),
|
|
name: z.string().min(2, "Name must be at least 2 characters."),
|
|
email: z.string().email("Invalid email address."),
|
|
});
|
|
|
|
export type UserFormData = z.infer<typeof userFormSchema>;
|
|
```
|
|
|
|
## 4. Theming and Styling
|
|
|
|
Leverage Tailwind CSS and `class-variance-authority` (CVA) for consistent, maintainable styling.
|
|
|
|
**Rule:** Define component variants using CVA. Use the `cn` utility for conditional class merging. Centralize Tailwind configuration and design tokens.
|
|
|
|
❌ BAD:
|
|
```tsx
|
|
// Inconsistent inline styles or direct class manipulation
|
|
<button className={`p-2 ${isActive ? "bg-blue-500" : "bg-gray-200"}`}>
|
|
```
|
|
|
|
✅ GOOD:
|
|
```tsx
|
|
// src/components/ui/badge.tsx
|
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const badgeVariants = cva(
|
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "border-transparent bg-primary text-primary-foreground",
|
|
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
outline: "text-foreground",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
},
|
|
}
|
|
);
|
|
|
|
export interface BadgeProps
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
VariantProps<typeof badgeVariants> {}
|
|
|
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
return (
|
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
);
|
|
}
|
|
|
|
export { Badge, badgeVariants };
|
|
```
|
|
|
|
## 5. Common Patterns and Anti-patterns
|
|
|
|
**Rule:** Use React Hook Form with Zod for all forms. Implement early returns and guard clauses for error handling.
|
|
|
|
❌ BAD:
|
|
```tsx
|
|
// Deeply nested logic, manual form state
|
|
if (data) {
|
|
// ... many lines
|
|
if (isValid) {
|
|
// ... more lines
|
|
}
|
|
}
|
|
```
|
|
|
|
✅ GOOD:
|
|
```tsx
|
|
// Early return for invalid state
|
|
if (!user) {
|
|
return <p>User not found.</p>;
|
|
}
|
|
|
|
// React Hook Form + Zod example
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { userFormSchema, type UserFormData, type UserFormProps } from "./user-form";
|
|
|
|
function UserProfileForm({ initialData, onSubmit }: UserFormProps) {
|
|
const form = useForm<UserFormData>({
|
|
resolver: zodResolver(userFormSchema),
|
|
defaultValues: initialData,
|
|
});
|
|
|
|
return (
|
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
{/* Form fields */}
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 6. Performance Considerations
|
|
|
|
Optimize for fast initial loads and smooth interactions.
|
|
|
|
**Rule:** Lazy-load heavy UI sections (dialogs, data tables) via `React.lazy` or dynamic imports. Memoize expensive components and callbacks.
|
|
|
|
❌ BAD:
|
|
```tsx
|
|
// Always loads heavy component
|
|
import { BigComplexChart } from "./big-complex-chart";
|
|
function Dashboard() {
|
|
return <BigComplexChart data={...} />;
|
|
}
|
|
```
|
|
|
|
✅ GOOD:
|
|
```tsx
|
|
import React from "react";
|
|
const LazyBigComplexChart = React.lazy(() => import("./big-complex-chart"));
|
|
|
|
function Dashboard() {
|
|
const [showChart, setShowChart] = React.useState(false);
|
|
|
|
const handleToggleChart = React.useCallback(() => {
|
|
setShowChart((prev) => !prev);
|
|
}, []);
|
|
|
|
return (
|
|
<div>
|
|
<Button onClick={handleToggleChart}>Toggle Chart</Button>
|
|
{showChart && (
|
|
<React.Suspense fallback={<div>Loading chart...</div>}>
|
|
<LazyBigComplexChart data={...} />
|
|
</React.Suspense>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 7. Accessibility
|
|
|
|
Build inclusive UIs by leveraging Radix primitives and ARIA attributes.
|
|
|
|
**Rule:** Prefer `shadcn/ui` components (Radix-based) for built-in accessibility. Ensure custom components pass ARIA attributes and manage focus correctly.
|
|
|
|
❌ BAD:
|
|
```tsx
|
|
// Custom button without ARIA attributes or proper semantics
|
|
<div role="button" onClick={...}>Click me</div>
|
|
```
|
|
|
|
✅ GOOD:
|
|
```tsx
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
<Button onClick={() => alert("Action!")}>Perform Action</Button>
|
|
```
|
|
|
|
## 8. Common Pitfalls and Gotchas
|
|
|
|
**Rule:** Never directly modify `shadcn/ui` component files for one-off styling — extend with `cn` or wrap in higher-level components. Avoid `dangerouslySetInnerHTML` unless content is sanitized.
|
|
|
|
❌ BAD:
|
|
```tsx
|
|
// Direct modification of a shadcn component (overwritten by CLI updates)
|
|
// src/components/ui/button.tsx (modified for a single use case)
|
|
```
|
|
|
|
```tsx
|
|
// Security vulnerability
|
|
<div dangerouslySetInnerHTML={{ __html: userProvidedContent }} />
|
|
```
|
|
|
|
✅ GOOD:
|
|
```tsx
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
<Button className="bg-red-500 hover:bg-red-600">Custom Red Button</Button>
|
|
```
|
|
|
|
```tsx
|
|
import DOMPurify from "dompurify";
|
|
|
|
const sanitizedContent = DOMPurify.sanitize(userProvidedContent);
|
|
return <div className="prose" dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
|
|
// Prefer rendering text directly when possible:
|
|
// return <p>{userProvidedContent}</p>;
|
|
```
|