Version 1.0.0
January 2026
Note:
This document is for agents and LLMs to follow when maintaining, generating,
or refactoring code in this repository. Humans may also find it useful, but
guidance here is optimized for automation and consistency by AI-assisted workflows.
Standards and conventions for the User Interface Wiki, a Next.js documentation site focused on UI/UX design principles. Contains guidelines for components, styling, content, and code organization to ensure consistency and maintainability.
- Project Overview
- Naming Conventions
- File Structure
- Component Standards
- CSS Standards
- Content Standards
- TypeScript Standards
- Animation Standards
- Accessibility Standards
- Performance Standards
- External Skills
| Technology | Purpose |
|---|---|
| Next.js 16 | Framework with App Router |
| React 19 | UI library with React Compiler |
| TypeScript | Type safety |
| Biome | Linting and formatting |
| CSS Modules | Scoped styling |
| Motion (Framer Motion) | Animation library |
| Fumadocs | MDX documentation framework |
| Zustand | State management |
| Base UI | Headless component primitives |
/app → Next.js pages and routes
/components → Reusable UI components
/content → MDX articles and demos
/icons → SVG icon components
/lib → Utilities, types, and stores
/public → Static assets
/skills → AI skill definitions (SKILL.md files)
/styles → Global CSS and theme
All names use kebab-case throughout the project.
| Item | Convention | Example |
|---|---|---|
| Directories | kebab-case | components/button-group/ |
| Files | kebab-case | use-audio.ts, styles.module.css |
| CSS classes | kebab-case | .button-primary, .nav-item |
| CSS variables | kebab-case | --font-weight-medium |
| URL slugs | kebab-case | /12-principles-of-animation |
Incorrect:
.buttonPrimary { }
.NavItem { }
.button_primary { }Correct:
.button-primary { }
.nav-item { }Exception: React component function names use PascalCase (e.g., function Button()).
Each component lives in its own directory with colocated files:
components/
button/
index.tsx # Component implementation
styles.module.css # Scoped styles
Rules:
- Export components from
index.tsxusing named exports - Colocate CSS modules with components
- Use
index.tsbarrel files only for multi-file exports (e.g., hooks)
Incorrect:
// button.tsx - wrong filename
export default function Button() { ... }Correct:
// index.tsx
export function Button() { ... }MDX content lives in /content/ with demos colocated:
content/
12-principles-of-animation/
index.mdx # Article content
demos/
index.ts # Demo barrel export
squash-stretch/
index.tsx
styles.module.css
Rules:
- Each article is a directory with
index.mdx - Demos are colocated in
demos/subdirectory - Export all demos from
demos/index.ts
Use function declarations with explicit prop interfaces:
Incorrect:
const Button = (props: any) => { ... }Correct:
interface ButtonProps {
variant?: "primary" | "secondary" | "ghost" | "text";
size?: "small" | "medium" | "large";
children: ReactNode;
}
function Button({ variant = "primary", size = "medium", children }: ButtonProps) {
return (
<button className={clsx(styles.button, styles[variant], styles[size])}>
{children}
</button>
);
}
export { Button };Add "use client" directive only when necessary:
When to use "use client":
- Using React hooks (
useState,useEffect, etc.) - Using browser APIs (
window,localStorage, etc.) - Using event handlers
- Using motion/animation libraries
Incorrect:
"use client"; // Unnecessary - no client features used
function StaticCard({ title }: { title: string }) {
return <div>{title}</div>;
}Use data attributes for variants instead of multiple className conditionals:
Correct:
function Callout({ type = "info", children }: CalloutProps) {
return (
<div className={styles.callout} data-variant={type}>
{children}
</div>
);
}.callout[data-variant="info"] {
background: var(--blue-a3);
}
.callout[data-variant="error"] {
background: var(--red-a3);
}Use motion.create() for Base UI components:
Correct:
import { Button as BaseButton } from "@base-ui/react/button";
import { motion } from "motion/react";
const MotionBaseButton = motion.create(BaseButton);
function Button(props: ButtonProps) {
return (
<MotionBaseButton
whileTap={{ scale: 0.98 }}
{...props}
/>
);
}All component styles use CSS Modules with .module.css extension:
Rules:
- Use kebab-case for all class names
- Leverage CSS custom properties from theme
- Use
clsxfor conditional class composition
Incorrect:
/* Hardcoded values */
.button {
background: #333;
font-size: 14px;
}Correct:
/* Using theme variables */
.button {
background: var(--gray-12);
font-size: 14px;
font-weight: var(--font-weight-medium);
letter-spacing: var(--font-letter-spacing-14px);
}Use variables from /styles/styles.theme.css:
| Category | Example Variables |
|---|---|
| Colors | --gray-1 through --gray-12, --gray-a1 through --gray-a12 |
| Typography | --font-weight-normal, --font-weight-medium, --font-weight-bold |
| Letter Spacing | --font-letter-spacing-13px, --font-letter-spacing-14px |
| Shadows | --shadow-1, --shadow-2, --shadow-3 |
| Layout | --page-padding-inline, --page-max-width |
Use consistent size scales:
.small {
height: 28px;
padding: 5px 8px;
font-size: 13px;
}
.medium {
height: 32px;
padding: 6px 12px;
font-size: 14px;
}
.large {
height: 40px;
padding: 8px 16px;
font-size: 15px;
}Use consistent transition timing:
.element {
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.18s ease;
}Every MDX article requires frontmatter:
---
title: "Article Title"
description: "Brief description for SEO and previews"
date: "2025-04-08"
tags: ["tag1", "tag2"]
author: "raphael-salaja"
icon: "code"
---Import demos from colocated directory:
import { DemoComponent } from "./demos";
<Figure>
<DemoComponent />
<Caption>Description of the demo</Caption>
</Figure>Wrap demos in <Figure> with optional <Caption>:
Correct:
<Figure>
<InteractiveDemo />
<Caption>Caption text with optional footnote[^1]</Caption>
</Figure>Playground demos use Sandpack and must be self-contained (no @/components imports).
Button Labels for Boolean Toggles:
For buttons that toggle boolean state, use "Toggle" as the label instead of swapping between two words.
Incorrect:
<Button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "Hide" : "Show"}
</Button>Correct:
<Button onClick={() => setIsVisible(!isVisible)}>Toggle</Button>Inline Components:
Playground demos must define Button and Controls inline since they can't import from the main codebase:
function Button({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
return <button className={styles.button} onClick={onClick}>{children}</button>;
}
function Controls({ children }: { children: React.ReactNode }) {
return <div className={styles.controls}>{children}</div>;
}The /skills/ directory contains a unified skill following the Vercel agent-skills pattern:
skills/
SKILL.md # Compact quick-reference (one-liner per rule)
AGENTS.md # Full compiled doc with all rules expanded
metadata.json # Version, author, abstract, references
rules/
_sections.md # Category definitions + ordering
_template.md # Template for adding new rules
timing-under-300ms.md
spring-for-gestures.md
... # 89 individual rule files
To add a new rule, copy rules/_template.md, fill in the frontmatter, and add the rule ID to SKILL.md and AGENTS.md.
Define types in component files or /lib/types.ts:
Component-specific types:
// In component file
interface ButtonProps {
variant?: "primary" | "secondary";
}Shared types:
// In /lib/types.ts
export interface Author {
name: string;
avatar: string;
}Use path aliases from tsconfig.json:
Incorrect:
import { Button } from "../../../components/button";Correct:
import { Button } from "@/components/button";Avoid any type. Use specific types or unknown:
Incorrect:
function process(data: any) { ... }Correct:
function process(data: unknown) {
if (isValidData(data)) { ... }
}Use motion/react for animations:
import { motion } from "motion/react";
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>| Interaction Type | Duration |
|---|---|
| Micro-interactions | 100-200ms |
| Standard transitions | 200-300ms |
| Page transitions | 300-400ms |
| Complex animations | 400-600ms |
Never exceed 300ms for user-initiated actions.
Use appropriate easing:
| Easing | Use Case |
|---|---|
ease-out |
Entrances (arrive fast, settle gently) |
ease-in |
Exits (build momentum before departure) |
ease-in-out |
Deliberate movements |
| Spring | Organic overshoot-and-settle |
Apply scale transform on interaction:
.button:active {
transform: scale(0.98);
}Use appropriate semantic elements:
Incorrect:
<div onClick={handleClick}>Click me</div>Correct:
<button onClick={handleClick}>Click me</button>Add ARIA attributes when semantic HTML is insufficient:
<div role="tablist" aria-label="Content tabs">
<button role="tab" aria-selected={isActive}>Tab 1</button>
</div>Ensure visible focus states:
.button:focus-visible {
outline: 2px solid var(--gray-12);
outline-offset: 2px;
}Respect reduced motion preferences:
@media (prefers-reduced-motion: reduce) {
.animated {
animation: none;
transition: none;
}
}Include skip-to-content link in layout:
<a href="#main-content" className="skip-to-content">
Skip to content
</a>Use Next.js Image component:
import Image from "next/image";
<Image
src="/content/image.jpg"
alt="Descriptive alt text"
width={640}
height={360}
/>Lazy-load heavy components:
import dynamic from "next/dynamic";
const HeavyComponent = dynamic(
() => import("./heavy-component").then(m => m.HeavyComponent),
{ ssr: false }
);Avoid barrel imports for large libraries:
Incorrect:
import { motion, AnimatePresence } from "motion/react";Correct (if tree-shaking fails):
import { motion } from "motion/react";
import { AnimatePresence } from "motion/react";Default to Server Components. Only add "use client" when needed.
Before submitting changes, verify:
- All names use kebab-case (files, directories, CSS classes)
- Components use named exports from
index.tsx - CSS uses theme variables, not hardcoded values
- TypeScript types are explicit, no
any - Imports use
@/path aliases - MDX has complete frontmatter
- Animations respect
prefers-reduced-motion - Interactive elements are keyboard accessible
-
"use client"is only used when necessary - No linter errors (run
npm run lint)
This project follows additional guidelines from Vercel's agent-skills repository. Fetch and apply these when working on the codebase:
https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/react-best-practices/SKILL.md
Covers performance optimization, bundle size, server-side rendering, re-render optimization, and advanced React patterns. Apply when writing or refactoring React components.
https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/web-design-guidelines/SKILL.md
Covers UI/UX best practices, accessibility, and design compliance. Apply when reviewing UI code, checking accessibility, or auditing design.