| name | pinpoint-ui |
| description | shadcn/ui patterns, progressive enhancement, Server Components, Client Components, form handling, Tailwind CSS v4, accessibility. Use when building UI, forms, components, or when user mentions UI/styling/components/forms. |
PinPoint UI Guide
When to Use This Skill
Use this skill when:
- Building or modifying UI components
- Creating forms
- Working with shadcn/ui components
- Styling with Tailwind CSS v4
- Implementing progressive enhancement
- Deciding between Server and Client Components
- User mentions: "UI", "component", "form", "styling", "Tailwind", "shadcn", "button", "input"
Quick Reference
Critical UI Rules
- Server Components first: Default to Server Components, use "use client" only for interactivity
- Progressive enhancement: Forms must work without JavaScript
- shadcn/ui only: No MUI components
- Direct Server Action references: No inline wrappers in forms
- Dropdown Server Actions: Use
onSelect, not forms - Tailwind CSS v4: Use CSS variables, no hardcoded hex colors
Adding Components
npx shadcn@latest add [component]
Detailed Documentation
Read these files for comprehensive UI guidance:
# Primary UI guide - the "Goto" manual for all UI work
cat docs/UI_GUIDE.md
# Specific UI implementation patterns
ls docs/ui-patterns/
cat docs/ui-patterns/*.md
Core UI Patterns
Server vs Client Components
// ✅ Good: Server Component (default)
export default async function MachinesPage() {
const machines = await getMachines();
return (
<div>
{machines.map((machine) => (
<MachineCard key={machine.id} machine={machine} />
))}
</div>
);
}
// ✅ Good: Client Component (only when needed)
"use client";
import { useState } from "react";
export function IssueFilter() {
const [filter, setFilter] = useState("all");
return (
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All Issues</option>
<option value="open">Open</option>
<option value="resolved">Resolved</option>
</select>
);
}
Forms with Progressive Enhancement
// ✅ Good: Direct Server Action reference
import { createIssue } from "~/server/actions/issues";
export function CreateIssueForm() {
return (
<form action={createIssue}>
<input name="title" required />
<textarea name="description" />
<button type="submit">Create Issue</button>
</form>
);
}
// ❌ Bad: Inline wrapper (breaks Next.js form handling)
<form action={async () => { await createIssue(); }}>
Forms with useActionState (React 19)
"use client";
import { useActionState } from "react";
import { createIssue } from "~/server/actions/issues";
export function CreateIssueForm() {
const [state, formAction] = useActionState(createIssue, { message: "" });
return (
<form action={formAction}>
<input name="title" required />
{state.message && <p className="text-red-500">{state.message}</p>}
<button type="submit">Create Issue</button>
</form>
);
}
Dropdowns with Server Actions
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { deleteIssue } from "~/server/actions/issues";
export function IssueActionsMenu({ issueId }: { issueId: string }) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{/* ✅ Good: Use onSelect */}
<DropdownMenuItem
onSelect={async () => {
await deleteIssue(issueId);
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
// ❌ Bad: Form inside dropdown (unmounts before submission)
<DropdownMenuItem>
<form action={deleteIssue}>
<button>Delete</button>
</form>
</DropdownMenuItem>
Styling with Tailwind CSS v4
CSS Variables (No Hardcoded Colors)
// ✅ Good: Use CSS variables from globals.css
<div className="bg-background text-foreground">
<p className="text-muted-foreground">Muted text</p>
</div>
// ❌ Bad: Hardcoded hex colors
<div style={{ backgroundColor: "#ffffff", color: "#000000" }}>
Component Styling
// ✅ Good: Use className with cn() for merging
import { cn } from "~/lib/utils";
export function Button({ className, ...props }: ButtonProps) {
return (
<button
className={cn(
"rounded-md bg-primary px-4 py-2 text-primary-foreground",
className
)}
{...props}
/>
);
}
// ❌ Bad: String concatenation (doesn't handle conflicts)
<button className={`base-classes ${className}`} />
// ❌ Bad: Inline styles
<button style={{ marginTop: '10px' }} />
Global vs Local Styles
// ✅ Global styles (globals.css)
// - Typography (headings, body text)
// - Theme variables (colors, spacing)
// ✅ Local styles (component className)
// - Component-specific layout
// - Responsive design
// - Interactive states (hover, focus)
// ❌ Bad: Hardcoded spacing in reusable components
export function Card({ children }: CardProps) {
return <div className="m-4 p-4">{children}</div>; // Too opinionated
}
// ✅ Good: Allow className override
export function Card({ children, className }: CardProps) {
return <div className={cn("rounded-lg border", className)}>{children}</div>;
}
shadcn/ui Component Patterns
Button Variants
import { Button } from "~/components/ui/button";
<Button variant="default">Primary Action</Button>
<Button variant="secondary">Secondary Action</Button>
<Button variant="destructive">Delete</Button>
<Button variant="ghost">Subtle Action</Button>
<Button variant="link">Link Style</Button>
Dialog (Modal) Pattern
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
export function CreateIssueDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Create Issue</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Issue</DialogTitle>
<DialogDescription>
Report a problem with a machine.
</DialogDescription>
</DialogHeader>
<CreateIssueForm />
</DialogContent>
</Dialog>
);
}
Form with shadcn/ui
import { Label } from "~/components/ui/label";
import { Input } from "~/components/ui/input";
import { Textarea } from "~/components/ui/textarea";
import { Button } from "~/components/ui/button";
export function IssueForm() {
return (
<form action={createIssue} className="space-y-4">
<div>
<Label htmlFor="title">Title</Label>
<Input id="title" name="title" required />
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea id="description" name="description" />
</div>
<Button type="submit">Create Issue</Button>
</form>
);
}
Accessibility Patterns
Semantic HTML
// ✅ Good: Semantic HTML
<nav aria-label="Main navigation">
<ul>
<li><a href="/machines">Machines</a></li>
<li><a href="/issues">Issues</a></li>
</ul>
</nav>
// ❌ Bad: Div soup
<div className="nav">
<div className="nav-item">Machines</div>
<div className="nav-item">Issues</div>
</div>
ARIA Labels
// ✅ Good: ARIA labels for screen readers
<Button aria-label="Delete issue">
<TrashIcon className="h-4 w-4" />
</Button>
// ✅ Good: Label association
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" />
Progressive Enhancement
CSS-Only Patterns
// ✅ Good: CSS-only hover effects
<div className="group">
<Button className="group-hover:bg-primary/90">
Hover Me
</Button>
</div>
// ✅ Good: Peer patterns for form validation
<Input className="peer" />
<p className="peer-invalid:visible invisible text-red-500">
Invalid input
</p>
Fallback for No JS
// ✅ Good: Form works without JavaScript
<form action={createIssue} method="POST">
<input name="title" required />
<button type="submit">Submit</button>
{/* No client-side validation required */}
</form>
// ❌ Bad: Requires JavaScript
<form onSubmit={(e) => {
e.preventDefault();
// Client-side only logic
}}>
Layout Patterns
Page Layout
// ✅ Good: Consistent page structure
export default async function MachinesPage() {
const machines = await getMachines();
return (
<div className="container mx-auto py-8">
<div className="mb-8 flex items-center justify-between">
<h1 className="text-3xl font-bold">Machines</h1>
<Button asChild>
<Link href="/machines/new">Add Machine</Link>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{machines.map((machine) => (
<MachineCard key={machine.id} machine={machine} />
))}
</div>
</div>
);
}
Responsive Grid
// ✅ Good: Responsive grid with Tailwind
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{items.map((item) => (
<Card key={item.id}>{item.name}</Card>
))}
</div>
UI Anti-Patterns
❌ Don't Do These
Global CSS Resets:
/* ❌ Bad: Breaks component internals */
* {
margin: 0;
padding: 0;
}
/* ✅ Good: Use Tailwind's Preflight */
@tailwind base;
Hardcoded Spacing in Components:
// ❌ Bad: Rigid component
export function Card({ children }: CardProps) {
return <div className="m-4 p-4">{children}</div>;
}
// ✅ Good: Flexible component
export function Card({ children, className }: CardProps) {
return <div className={cn("rounded-lg", className)}>{children}</div>;
}
Inline Styles:
// ❌ Bad: Inline styles
<div style={{ marginTop: '10px', color: '#ff0000' }}>
// ✅ Good: Tailwind utilities
<div className="mt-2.5 text-red-500">
UI Checklist
Before committing UI code:
- Server Components by default (only "use client" when needed)
- Forms work without JavaScript
- Direct Server Action references (no inline wrappers)
- Dropdowns use
onSelectfor Server Actions - CSS variables, no hardcoded colors
-
cn()used for className merging - Semantic HTML (nav, main, article, etc.)
- ARIA labels for icon-only buttons
- Responsive design (mobile-first)
- shadcn/ui components only (no MUI)
Additional Resources
- UI guide:
docs/UI_GUIDE.md - UI patterns:
docs/ui-patterns/*.md - shadcn/ui docs: Use Context7 MCP for latest components
- Tailwind CSS v4 docs: Use Context7 MCP for latest utilities