Component Development Guide
Complete guide to developing and maintaining UI components for the Cascadia Marquee website
Component Development Guide
Complete guide to developing and maintaining UI components for the Cascadia Marquee website.
Component Architecture
The website uses a layered component architecture:
- UI Components - Base components from shadcn/ui
- Layout Components - Page structure and navigation
- Feature Components - Business logic and functionality
- Content Components - MDX and content-specific components
Component Structure
src/components/
├── ui/ # shadcn/ui base components
├── layout/ # Layout and navigation
│ ├── layouts/ # Page layouts
│ ├── partials/ # Header, footer, sidebar
│ └── sections/ # Page sections
├── contact/ # Contact form components
├── charts/ # Data visualization
├── icons/ # Custom icons
├── text/ # Typography components
└── container/ # Layout containersAdding New UI Components
Using shadcn/ui (Recommended)
-
Install a component:
pnpm dlx shadcn@latest add button pnpm dlx shadcn@latest add card pnpm dlx shadcn@latest add dialog -
Components are automatically added to:
src/components/ui/[component-name].tsxsrc/components/ui/index.ts(export)
-
Use the component:
import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card";
Available shadcn/ui Components
- Button - Interactive buttons with variants
- Card - Content containers
- Dialog - Modal dialogs
- Input - Form inputs
- Label - Form labels
- Select - Dropdown selects
- Textarea - Multi-line text inputs
- Badge - Status indicators
- Alert - Alert messages
- Accordion - Collapsible content
- Tabs - Tabbed interfaces
- Tooltip - Hover tooltips
- Dropdown Menu - Context menus
- Command - Command palette
- Form - Form handling
- Checkbox - Checkbox inputs
- Switch - Toggle switches
- Slider - Range sliders
- Progress - Progress bars
- Separator - Visual separators
- Aspect Ratio - Responsive containers
- Hover Card - Hover content
- Collapsible - Collapsible sections
- Resizable - Resizable panels
- Drawer - Mobile-friendly drawers
- Sonner - Toast notifications
Creating Custom Components
Component Template
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
interface ComponentProps {
className?: string;
children?: React.ReactNode;
// Add other props as needed
}
const Component = forwardRef<HTMLDivElement, ComponentProps>(
({ className, children, ...props }, ref) => {
return (
<div ref={ref} className={cn("base-styles", className)} {...props}>
{children}
</div>
);
}
);
Component.displayName = "Component";
export { Component };Component Best Practices
- Use forwardRef for proper ref forwarding
- Include className prop for customization
- Use cn() utility for conditional classes
- Export with displayName for debugging
- Follow TypeScript interfaces for type safety
Example: Custom Card Component
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
interface CustomCardProps {
className?: string;
children?: React.ReactNode;
variant?: "default" | "outlined" | "elevated";
size?: "sm" | "md" | "lg";
}
const CustomCard = forwardRef<HTMLDivElement, CustomCardProps>(
({ className, children, variant = "default", size = "md", ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"bg-card text-card-foreground rounded-lg border",
{
"border-border": variant === "default",
"border-primary border-2": variant === "outlined",
"shadow-lg": variant === "elevated",
"p-4": size === "sm",
"p-6": size === "md",
"p-8": size === "lg",
},
className
)}
{...props}
>
{children}
</div>
);
}
);
CustomCard.displayName = "CustomCard";
export { CustomCard };Component Registry System
The website uses an automated component registry for dynamic component loading:
Registry Structure
// src/__registry__/index.tsx
export const Index = {
charts: {
"bar-chart": {
name: "bar-chart",
component: React.lazy(() => import("@/components/charts/bar-chart")),
},
"shopify-loading-speed-to-conversion-chart": {
name: "shopify-loading-speed-to-conversion-chart",
component: React.lazy(
() => import("@/components/charts/shopify-loading-speed-to-conversion-chart")
),
},
},
examples: {
// Example components
},
};Adding Components to Registry
-
Place component in appropriate directory:
- Charts:
src/components/charts/ - Examples:
src/components/examples/
- Charts:
-
Run registry build:
pnpm run build:registry -
Component is automatically registered and available for dynamic loading
Using Registry Components
import { Index } from "@/__registry__";
// Dynamic component loading
const ChartComponent = Index.charts["bar-chart"].component;
// Use in JSX
<ChartComponent data={chartData} />;Layout Components
Page Layouts
// src/components/layout/layouts/page-layout.tsx
import { Container } from "@/components/container";
import { Section } from "@/components/section";
interface PageLayoutProps {
children: React.ReactNode;
title?: string;
description?: string;
}
export function PageLayout({ children, title, description }: PageLayoutProps) {
return (
<Container>
<Section>
{title && <h1>{title}</h1>}
{description && <p>{description}</p>}
{children}
</Section>
</Container>
);
}Section Components
// src/components/section/section.tsx
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
interface SectionProps {
className?: string;
children?: React.ReactNode;
id?: string;
}
const Section = forwardRef<HTMLElement, SectionProps>(
({ className, children, id, ...props }, ref) => {
return (
<section ref={ref} id={id} className={cn("py-16", className)} {...props}>
{children}
</section>
);
}
);
Section.displayName = "Section";
export { Section };Feature Components
Contact Form Components
// src/components/contact/contact-form.tsx
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
export function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Handle form submission
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<Input
placeholder="Your Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<Input
type="email"
placeholder="Your Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<Textarea
placeholder="Your Message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
/>
<Button type="submit">Send Message</Button>
</form>
);
}Chart Components
// src/components/charts/bar-chart.tsx
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
interface BarChartProps {
data: Array<{
name: string;
value: number;
}>;
}
export function BarChart({ data }: BarChartProps) {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
);
}Styling Components
Using Tailwind CSS
// Utility-first approach
<div className="flex items-center justify-between rounded-lg bg-white p-4 shadow-md">
<h2 className="text-xl font-semibold text-gray-900">Title</h2>
<Button variant="outline" size="sm">
Action
</Button>
</div>Using CSS Variables
// Using design system variables
<div className="bg-background text-foreground border-border rounded-lg border p-4">
<h2 className="text-primary text-2xl font-bold">Title</h2>
<p className="text-muted-foreground">Description</p>
</div>Custom CSS Classes
/* src/app/globals.css */
.custom-component {
@apply rounded-lg border border-border bg-background p-4;
}
.custom-component:hover {
@apply shadow-lg transition-shadow duration-200;
}Component Testing
Unit Testing
// __tests__/components/button.test.tsx
import { render, screen } from "@testing-library/react";
import { Button } from "@/components/ui/button";
describe("Button", () => {
it("renders with correct text", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("applies variant classes", () => {
render(<Button variant="destructive">Delete</Button>);
expect(screen.getByRole("button")).toHaveClass("bg-destructive");
});
});Integration Testing
// __tests__/components/contact-form.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { ContactForm } from "@/components/contact/contact-form";
describe("ContactForm", () => {
it("submits form with correct data", async () => {
render(<ContactForm />);
fireEvent.change(screen.getByPlaceholderText("Your Name"), {
target: { value: "John Doe" },
});
fireEvent.click(screen.getByText("Send Message"));
// Assert form submission
});
});Performance Optimization
Lazy Loading
import { lazy, Suspense } from "react";
const HeavyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}Memoization
import { memo, useMemo } from "react";
const ExpensiveComponent = memo(({ data }: { data: any[] }) => {
const processedData = useMemo(() => {
return data.map((item) => ({
...item,
processed: true,
}));
}, [data]);
return <div>{/* Render processed data */}</div>;
});Code Splitting
// Dynamic imports for route-based code splitting
const BlogPage = lazy(() => import("@/app/blog/page"));
const DocsPage = lazy(() => import("@/app/docs/page"));Accessibility
ARIA Attributes
<button aria-label="Close dialog" aria-expanded={isOpen} aria-controls="dialog-content">
<CloseIcon />
</button>Keyboard Navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
};
<button onKeyDown={handleKeyDown} onClick={handleClick}>
Action
</button>;Focus Management
import { useRef, useEffect } from "react";
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const firstFocusableRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen && firstFocusableRef.current) {
firstFocusableRef.current.focus();
}
}, [isOpen]);
return (
<div role="dialog" aria-modal="true">
<button ref={firstFocusableRef} onClick={onClose}>
Close
</button>
</div>
);
}Component Documentation
JSDoc Comments
/**
* A customizable button component with multiple variants and sizes.
*
* @param variant - The visual style variant of the button
* @param size - The size of the button
* @param children - The content to display inside the button
* @param className - Additional CSS classes to apply
*/
interface ButtonProps {
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
size?: "default" | "sm" | "lg" | "icon";
children?: React.ReactNode;
className?: string;
}Storybook Integration
// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";
const meta: Meta<typeof Button> = {
title: "UI/Button",
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: "Button",
},
};
export const Destructive: Story = {
args: {
variant: "destructive",
children: "Delete",
},
};Troubleshooting
Common Issues
Component not rendering:
- Check import paths
- Verify component is exported
- Check for TypeScript errors
Styling not applying:
- Verify Tailwind classes are correct
- Check CSS variable definitions
- Ensure proper className prop usage
Performance issues:
- Use React DevTools Profiler
- Check for unnecessary re-renders
- Implement proper memoization
Accessibility issues:
- Use axe-core for testing
- Check ARIA attributes
- Test keyboard navigation
Best Practices
- Follow the component hierarchy - UI → Layout → Feature → Content
- Use TypeScript interfaces for all component props
- Implement proper error boundaries for robust error handling
- Write comprehensive tests for critical components
- Document components with JSDoc and Storybook
- Optimize for performance with lazy loading and memoization
- Ensure accessibility with proper ARIA attributes and keyboard support
- Follow design system patterns and conventions
For questions about component development, check the other documentation sections or contact the development team.