Help us improve
Share bugs, ideas, or general feedback.
From nextjs-supabase-ai-sdk-dev
Defines TypeScript contracts first, then implements compound React Server Components with Tailwind CSS styling. Use after wireframing for new UI components or libraries.
npx claudepluginhub constellos/claude-code --plugin nextjs-supabase-ai-sdk-devHow this skill is triggered — by the user, by Claude, or both
Slash command
/nextjs-supabase-ai-sdk-dev:ui-designThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Contract-first static UI methodology using TypeScript interfaces and compound components.
Builds production-quality UIs with accessible, performant components using composition patterns, proper file structure, and simple state management. Use for creating or modifying user interfaces.
Enforces React UI consistency using shadcn/ui primitives, semantic Tailwind tokens, and responsive layouts. Guides building/refactoring components, screens, forms, dialogs for design system adherence.
Designs props interfaces, composition models, and public APIs for reusable React/UI components using atomic design and inversion of control patterns.
Share bugs, ideas, or general feedback.
Contract-first static UI methodology using TypeScript interfaces and compound components.
UI Design is the second step in the UI development workflow, following wireframing. Define TypeScript interfaces first (the "contract"), then implement compound components with Tailwind CSS styling. Server Components are the default.
Define TypeScript interfaces before implementation:
// 1. Define the contract first
interface FeatureCardProps {
title: string;
description: string;
icon: IconName;
href?: string;
}
// 2. Then implement the component
function FeatureCard({ title, description, icon, href }: FeatureCardProps) {
// Implementation
}
All components are React Server Components unless they need:
// Server Component (default) - no "use client" directive
export function FeatureCard({ title, description }: FeatureCardProps) {
return (
<article className="rounded-lg border p-6">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="text-muted-foreground">{description}</p>
</article>
);
}
Structure complex components with composable parts:
// Root component provides context
function Card({ children, className }: CardProps) {
return (
<div className={cn("rounded-lg border bg-card", className)}>
{children}
</div>
);
}
// Sub-components for each section
function CardHeader({ children, className }: CardHeaderProps) {
return (
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
{children}
</div>
);
}
function CardTitle({ children, className }: CardTitleProps) {
return (
<h3 className={cn("text-2xl font-semibold", className)}>
{children}
</h3>
);
}
function CardContent({ children, className }: CardContentProps) {
return (
<div className={cn("p-6 pt-0", className)}>
{children}
</div>
);
}
function CardFooter({ children, className }: CardFooterProps) {
return (
<div className={cn("flex items-center p-6 pt-0", className)}>
{children}
</div>
);
}
// Export compound component
export { Card, CardHeader, CardTitle, CardContent, CardFooter };
Start with the component contract:
// types.ts
import type { ReactNode } from 'react';
export interface CardProps {
children: ReactNode;
className?: string;
variant?: 'default' | 'outlined' | 'elevated';
}
export interface CardHeaderProps {
children: ReactNode;
className?: string;
}
export interface CardTitleProps {
children: ReactNode;
className?: string;
as?: 'h1' | 'h2' | 'h3' | 'h4';
}
export interface CardDescriptionProps {
children: ReactNode;
className?: string;
}
export interface CardContentProps {
children: ReactNode;
className?: string;
}
export interface CardFooterProps {
children: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end' | 'between';
}
Implement each part of the compound component:
// card.tsx
import { cn } from '@/lib/utils';
import type {
CardProps,
CardHeaderProps,
CardTitleProps,
CardDescriptionProps,
CardContentProps,
CardFooterProps,
} from './types';
const variantStyles = {
default: 'bg-card text-card-foreground',
outlined: 'border-2 bg-transparent',
elevated: 'bg-card shadow-lg',
} as const;
export function Card({ children, className, variant = 'default' }: CardProps) {
return (
<div
className={cn(
'rounded-lg border',
variantStyles[variant],
className
)}
>
{children}
</div>
);
}
export function CardHeader({ children, className }: CardHeaderProps) {
return (
<div className={cn('flex flex-col space-y-1.5 p-6', className)}>
{children}
</div>
);
}
export function CardTitle({
children,
className,
as: Tag = 'h3',
}: CardTitleProps) {
return (
<Tag className={cn('text-2xl font-semibold leading-none tracking-tight', className)}>
{children}
</Tag>
);
}
export function CardDescription({ children, className }: CardDescriptionProps) {
return (
<p className={cn('text-sm text-muted-foreground', className)}>
{children}
</p>
);
}
export function CardContent({ children, className }: CardContentProps) {
return (
<div className={cn('p-6 pt-0', className)}>
{children}
</div>
);
}
const alignStyles = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between',
} as const;
export function CardFooter({
children,
className,
align = 'start',
}: CardFooterProps) {
return (
<div className={cn('flex items-center p-6 pt-0', alignStyles[align], className)}>
{children}
</div>
);
}
Organize exports in index.ts:
// index.ts
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from './card';
export type {
CardProps,
CardHeaderProps,
CardTitleProps,
CardDescriptionProps,
CardContentProps,
CardFooterProps,
} from './types';
Apply mobile-first responsive styling:
export function FeatureGrid({ features }: FeatureGridProps) {
return (
<div className={cn(
// Mobile: single column
'grid grid-cols-1 gap-4',
// Tablet: two columns
'md:grid-cols-2 md:gap-6',
// Desktop: three columns
'lg:grid-cols-3 lg:gap-8'
)}>
{features.map((feature) => (
<Card key={feature.id}>
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
);
}
Install and use Shadcn components as the foundation:
npx shadcn@latest add button card input
Reference: https://ui.shadcn.com/docs/components
Extend Shadcn components when needed:
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface LoadingButtonProps extends React.ComponentProps<typeof Button> {
loading?: boolean;
}
export function LoadingButton({
loading,
children,
disabled,
className,
...props
}: LoadingButtonProps) {
return (
<Button
disabled={disabled || loading}
className={cn(loading && 'cursor-wait', className)}
{...props}
>
{loading ? <Spinner className="mr-2 h-4 w-4" /> : null}
{children}
</Button>
);
}
Use Radix for accessible, unstyled primitives:
import * as Dialog from '@radix-ui/react-dialog';
export function Modal({ open, onOpenChange, children }: ModalProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Reference: https://www.radix-ui.com/primitives/docs/overview/introduction
Use AI Elements for AI-specific UI patterns:
import { Chat, Message, MessageInput } from '@ai-elements/react';
export function ChatInterface({ messages, onSend }: ChatInterfaceProps) {
return (
<Chat>
{messages.map((msg) => (
<Message key={msg.id} role={msg.role}>
{msg.content}
</Message>
))}
<MessageInput onSubmit={onSend} />
</Chat>
);
}
Organize component files consistently:
components/
└── feature-card/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── feature-card.tsx # Main component
├── feature-card.test.tsx # Tests
└── WIREFRAME.md # Layout documentation
For compound components:
components/
└── card/
├── index.ts # Exports all parts
├── types.ts # All interfaces
├── card.tsx # Root component
├── card-header.tsx # Sub-component
├── card-content.tsx # Sub-component
├── card-footer.tsx # Sub-component
└── card.test.tsx # Tests
Prefer composable components over prop-heavy ones:
// Avoid: Too many props
<Card
title="Feature"
description="Description"
footer={<Button>Action</Button>}
headerIcon={<Icon />}
/>
// Prefer: Composable structure
<Card>
<CardHeader>
<Icon />
<CardTitle>Feature</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
Use discriminated unions for variants:
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
}
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
};
Support ref forwarding for DOM access:
import { forwardRef } from 'react';
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border bg-background px-3 py-2',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
any, prefer unknown for generic inputstext-foreground, bg-background, not raw colorsBefore proceeding to interaction implementation:
After static UI is complete, proceed to the ui-interaction skill for adding client-side events, local state, and form validation.