Design System Documentation for KanaDojo
A comprehensive guide to UI development, theming, and best practices for our TypeScript Next.js application using Tailwind CSS.
- Project Stack
- Current Approach
- Theming System
- Accessibility Guidelines
- Component Patterns
- shadcn/ui Adoption Strategy
- Best Practices
- Code Examples
KanaDojo is built with modern web technologies optimized for performance and developer experience:
- TypeScript - Type-safe development
- Next.js 15 - App Router with React 19
- Tailwind CSS - Utility-first styling
- shadcn/ui - High-quality, accessible component library (slow adoption in progress)
- Framer Motion - Smooth animations via
motionpackage - Zustand - Lightweight state management
- Lucide React - Icon system
clsx+tailwind-merge- Conditional class management viacn()utilityclass-variance-authority- Component variant management@radix-ui- Accessible component primitives (via shadcn/ui)
KanaDojo leverages Tailwind CSS as the primary styling solution with a heavy emphasis on CSS custom properties (CSS variables) for theming. This approach provides:
- Dynamic theming - Runtime theme switching without rebuilding styles
- Consistency - Centralized color and spacing values
- Flexibility - Easy theme creation and customization
✅ DO:
<div className='bg-[var(--card-color)] text-[var(--main-color)]'>Content</div>❌ DON'T:
<div className='bg-gray-100 text-black'>Content</div>We use the cn() utility from lib/utils.ts to merge Tailwind classes intelligently:
import { cn } from '@/lib/utils';
<button
className={cn(
'rounded-lg px-4 py-2',
isActive && 'bg-[var(--main-color)]',
disabled && 'cursor-not-allowed opacity-50',
)}
>
Click me
</button>;Common patterns are extracted to shared/lib/styles.ts:
// shared/lib/styles.ts
import clsx from 'clsx';
export const cardBorderStyles = clsx('rounded-xl bg-[var(--card-color)]');
export const buttonBorderStyles = clsx(
'rounded-xl bg-[var(--card-color)] hover:cursor-pointer',
'duration-250 transition-all ease-in-out',
'hover:bg-[var(--border-color)]',
);Usage:
import { buttonBorderStyles } from '@/shared/lib/styles';
<button className={buttonBorderStyles}>Click me</button>;Use Tailwind's responsive prefixes consistently:
<div className='flex flex-col gap-4 md:flex-row md:gap-6 lg:gap-8'>
{/* Content adapts to screen size */}
</div>Breakpoints:
sm: 640pxmd: 768pxlg: 1024pxxl: 1280px2xl: 1536pxxs: 30rem (custom)3xl: 110rem (custom)
- Glass Mode Documentation: Detailed guide for wallpaper-based transparent themes.
KanaDojo uses a 5-variable color system defined in app/globals.css:
:root {
/* Layout Colors */
--background-color: hsla(210, 17%, 100%, 1); /* Page background */
--card-color: hsla(210, 17%, 91%, 1); /* Card/elevated surfaces */
--border-color: hsla(210, 17%, 76%, 1); /* Borders and dividers */
/* Content Colors */
--main-color: hsl(0, 0%, 0%); /* Primary text and actions */
--secondary-color: hsl(0, 0%, 35%); /* Secondary text and icons */
}Themes are defined in features/Preferences/data/themes.ts with TypeScript interfaces:
interface Theme {
id: string;
backgroundColor: string; // Page background
cardColor: string; // Cards, modals, elevated surfaces
borderColor: string; // Borders, dividers, hover states
mainColor: string; // Primary text, icons, CTAs
secondaryColor: string; // Secondary text, subtle elements
}- Theme Definition - Themes are organized into groups (Base, Light, Dark) in
features/Preferences/data/themes.ts - Theme Storage - Selected theme ID is persisted via Zustand in
features/Preferences/store/usePreferencesStore.ts - Theme Application - The
applyTheme()function dynamically updates CSS variables:
export function applyTheme(themeId: string) {
const theme = themeMap.get(themeId);
if (!theme) return;
const root = document.documentElement;
root.style.setProperty('--background-color', theme.backgroundColor);
root.style.setProperty('--card-color', theme.cardColor);
root.style.setProperty('--border-color', theme.borderColor);
root.style.setProperty('--main-color', theme.mainColor);
root.style.setProperty('--secondary-color', theme.secondaryColor);
root.setAttribute('data-theme', theme.id);
}To add a new theme:
- Define the theme in
features/Preferences/data/themes.ts:
{
id: 'my-custom-theme',
backgroundColor: 'hsla(220, 20%, 12%, 1)',
cardColor: 'hsla(220, 20%, 18%, 1)',
borderColor: 'hsla(220, 20%, 30%, 1)',
mainColor: 'hsla(280, 80%, 65%, 1)',
secondaryColor: 'hsla(180, 70%, 55%, 1)'
}- Test contrast ratios (see Accessibility section)
- Add to appropriate theme group (Base, Light, or Dark)
The sumi theme is a minimal, sumi-e (Japanese ink) inspired dark theme added in features/Preferences/data/themes.ts. It's designed for low visual distraction, high focus, and a neutral, high-clarity UI where content stands out against an almost-black page background.
- Palette (from
features/Preferences/data/themes.ts):
{
id: 'sumi',
backgroundColor: 'hsla(0, 0%, 10%, 1)', // deep charcoal / paper black
cardColor: 'hsla(0, 0%, 14%, 1)', // slightly lighter surface
borderColor: 'hsla(0, 0%, 22%, 1)', // subtle divider / hover
mainColor: 'hsla(0, 0%, 72%, 1)', // warm off-white for primary text
secondaryColor: 'hsla(45, 35%, 88%, 1)', // soft warm accent (paper/cream)
}-
Usage guidance:
- Use
--background-colorfor large page surfaces and overlays. - Use
--card-colorfor cards, panels, and elevated UI pieces. - Use
--border-colorfor separators, subtle hover/backdrop outlines and focus rings when appropriate. - Use
--main-colorfor primary text, icons, and CTAs that need high readability. - Use
--secondary-colorfor subtle accents, tertiary text, dividers or decorative strokes.
- Use
-
Tailwind example (recommended pattern):
<div className='min-h-screen bg-[var(--background-color)] text-[var(--main-color)]'>
<div className='rounded-xl border border-[var(--border-color)] bg-[var(--card-color)] p-6'>
<h1 className='text-2xl font-bold text-[var(--main-color)]'>
Sumi — Focus Mode
</h1>
<p className='text-sm text-[var(--secondary-color)]'>
Subtle helper text and accents
</p>
</div>
</div>-
Accessibility / contrast notes:
- The
sumitheme is intentionally high-contrast for primary content:--main-color(near-white) on--background-color(very dark charcoal) is appropriate for body text and passes typical AA thresholds for normal text in most sizes — still run specific checks for bright UI elements and CTA buttons. - For smaller UI chrome (icons, borders), ensure
--border-coloron--card-colormeets at least a 3:1 ratio for interactive affordances, or increase border opacity when used as the primary focus indicator. - When using
--secondary-colorfor secondary text, verify it maintains adequate contrast on both--background-colorand--card-colorfor the sizes you use (tooling: WebAIM, axe, Lighthouse).
- The
-
Developer checklist when adding/using
sumi:- Copy the theme object into
features/Preferences/data/themes.tsexactly (IDs must be kebab-case). - Verify
applyTheme('sumi')updates CSS variables anddata-themeattribute correctly. - Test interactive components (buttons, inputs, dialogs) visually and via automated contrast checks.
- Consider providing a slightly lighter variant of
--main-colorfor disabled/low-emphasis states to avoid blending with--card-color.
The
momijitheme is inspired by autumn maple leaves — warm, cozy, and subtly vibrant. It pairs a deep, neutral page background with amber and yellow-green accents for highlights and CTAs. Use this theme when you want a seasonal, warm-dark aesthetic that still prioritizes readability and clear affordances.- Palette (from
features/Preferences/data/themes.ts):
{ id: 'momiji', backgroundColor: 'hsla(15, 35%, 11%, 1)', cardColor: 'hsla(15, 33%, 15%, 1)', borderColor: 'hsla(15, 30%, 23%, 1)', mainColor: 'hsla(5, 85%, 58%, 1)', secondaryColor: 'hsla(45, 88%, 62%, 1)', }
-
Usage guidance:
- Use
--background-colorfor full-page backgrounds and large overlays. - Use
--card-colorfor cards, panels, and elevated UI surfaces to create subtle separation from the page background. - Use
--border-colorfor separators, subtle hover outlines, and focus affordances. - Use
--main-colorfor primary text, icons, and CTAs that need emphasis. - Use
--secondary-colorfor accent highlights, badges, and secondary CTAs.
- Use
-
Tailwind example:
<div className='min-h-screen bg-[var(--background-color)] text-[var(--main-color)]'> <div className='rounded-xl border border-[var(--border-color)] bg-[var(--card-color)] p-6'> <h1 className='text-2xl font-bold text-[var(--main-color)]'> Momiji — Autumn Warmth </h1> <p className='text-sm text-[var(--secondary-color)]'> Accent and supportive text </p> </div> </div>
-
Accessibility / contrast notes:
--main-color(warm amber) on--background-color(deep charcoal) should provide strong contrast for body text; still validate with WebAIM, axe, or Lighthouse.--secondary-color(yellow-green) is an accent — confirm contrast on both--background-colorand--card-colorwhen used for small or secondary text; reduce saturation or increase lightness if below AA.- Ensure
--border-coloron--card-colormeets at least a 3:1 contrast ratio for interactive affordances; increase opacity if necessary when used as a primary focus indicator.
-
Developer checklist when adding/using
momiji:- Add the theme object to
features/Preferences/data/themes.tsunder theDarktheme group. - Verify
applyTheme('momiji')updates CSS variables anddata-themeattribute correctly. - Run automated contrast checks (axe, Lighthouse) and manual spot checks for CTAs and small text.
- Test interactive components (buttons, inputs, dialogs) visually across breakpoints and accessibility modes.
- Add the theme object to
- Copy the theme object into
- Use HSLA for flexibility:
hsla(hue, saturation%, lightness%, alpha) - HSL makes it easier to create harmonious color schemes
- Theme IDs should be descriptive:
'midnight-purple','sunset-orange' - Use kebab-case for consistency
backgroundColor→ Lightest/darkest (depending on light/dark theme)cardColor→ Slightly elevated from backgroundborderColor→ More prominent than card, used for hover statesmainColor→ High contrast with backgroundsecondaryColor→ Medium contrast, complementary to main
Example Hierarchy (Dark Theme):
backgroundColor: hsl(220, 15%, 10%) ← Darkest
cardColor: hsl(220, 15%, 15%) ← Slightly lighter
borderColor: hsl(220, 15%, 25%) ← More prominent
mainColor: hsl(280, 80%, 70%) ← Vibrant, high contrast
secondaryColor: hsl(180, 60%, 60%) ← Complementary accent
All themes MUST meet WCAG 2.1 Level AA standards:
- Normal text (< 18pt): Contrast ratio ≥ 4.5:1
- Large text (≥ 18pt or ≥ 14pt bold): Contrast ratio ≥ 3:1
- UI components (buttons, borders): Contrast ratio ≥ 3:1
Tools for Testing:
- WebAIM Contrast Checker
- Chrome DevTools Lighthouse
- Browser extension: WAVE or axe DevTools
Example Validation:
// Test main text readability
mainColor (hsl(280, 80%, 70%)) on backgroundColor (hsl(220, 15%, 10%))
→ Contrast ratio: 8.2:1 ✅ Passes AA and AAA
// Test secondary text readability
secondaryColor (hsl(180, 60%, 60%)) on backgroundColor (hsl(220, 15%, 10%))
→ Contrast ratio: 5.1:1 ✅ Passes AAAll interactive elements MUST have visible focus indicators:
// Example from components/ui/button.tsx
const buttonVariants = cva(
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--main-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background-color)]',
// ... other styles
);Focus Ring Guidelines:
- Use
focus-visible:ring-2for keyboard navigation - Ring color:
ring-[var(--main-color)] - Ring offset:
ring-offset-2for separation - Ring offset color:
ring-offset-[var(--background-color)]
Use proper HTML elements for their intended purpose:
✅ DO:
<button onClick={handleClick}>Submit</button>
<a href="/about">Learn More</a>❌ DON'T:
<div onClick={handleClick}>Submit</div>
<span onClick={navigate}>Learn More</span>Provide context for screen readers when visual cues aren't sufficient:
<button aria-label="Close modal">
<X size={20} />
</button>
<input
type="checkbox"
aria-checked={isSelected}
aria-label="Select all Hiragana characters"
/>- Ensure all interactive elements are keyboard accessible
- Support
Tab,Enter,Space,Escape, and arrow keys where appropriate - Maintain logical tab order
Follow these patterns for consistency:
'use client'; // Add for client components (state, effects, etc.)
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { usePreferencesStore } from '@/features/Preferences';
interface MyComponentProps {
title: string;
isActive?: boolean;
onClick?: () => void;
}
const MyComponent = ({
title,
isActive = false,
onClick,
}: MyComponentProps) => {
const [state, setState] = useState('');
const theme = useThemeStore(state => state.theme);
return (
<div
className={cn(
'rounded-xl p-4',
'bg-[var(--card-color)] text-[var(--main-color)]',
isActive && 'border-2 border-[var(--main-color)]',
)}
>
<h3 className='text-xl font-semibold'>{title}</h3>
{/* ... */}
</div>
);
};
export default MyComponent;<div className='container mx-auto max-w-7xl px-4 md:px-6 lg:px-8'>
{/* Responsive container with proper padding */}
</div><div className='rounded-xl bg-[var(--card-color)] p-6 shadow-sm'>
{/* Card content */}
</div><button
className={cn(
'rounded-lg px-6 py-3',
'bg-[var(--main-color)] text-[var(--background-color)]',
'hover:brightness-110 active:brightness-95',
'transition-all duration-200',
'focus-visible:ring-2 focus-visible:ring-[var(--main-color)] focus-visible:ring-offset-2',
)}
>
Action
</button><h1 className="text-4xl md:text-5xl font-bold text-[var(--main-color)]">
Main Heading
</h1>
<h2 className="text-2xl md:text-3xl font-semibold text-[var(--main-color)]">
Subheading
</h2>
<p className="text-base text-[var(--secondary-color)]">
Body text with secondary color
</p><div
className={cn(
'rounded-lg p-4',
'bg-[var(--card-color)]',
'hover:cursor-pointer hover:bg-[var(--border-color)]',
'transition-all duration-200',
)}
>
{/* Hoverable content */}
</div>Use Framer Motion (motion package) for complex animations:
import { motion } from 'motion/react';
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* Animated content */}
</motion.div>;Use Tailwind transitions for simple interactions:
<button className='transition-all duration-200 hover:scale-105 active:scale-95'>
Click me
</button>KanaDojo is gradually adopting shadcn/ui for consistent, accessible components.
- ✅
Button(components/ui/button.tsx) - ✅
Select(components/ui/select.tsx)
shadcn/ui components are customized to use our CSS variable system:
// components/ui/button.tsx
const buttonVariants = cva(
'bg-[var(--main-color)] text-[var(--background-color)]', // Uses our theme
// ...
);Use the shadcn CLI to add components:
npx shadcn@latest add [component-name]After installation:
- Review the generated component
- Replace hardcoded colors with CSS variables:
bg-primary→bg-[var(--main-color)]text-primary-foreground→text-[var(--background-color)]border→border-[var(--border-color)]
- Test with multiple themes to ensure compatibility
Example: Customizing Button variants
// components/ui/button.tsx
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-transparent text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--main-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background-color)] disabled:pointer-events-none disabled:opacity-60',
{
variants: {
variant: {
default:
'bg-[var(--main-color)] text-[var(--background-color)] shadow-[0_10px_30px_-12px_rgba(0,0,0,0.45)] hover:brightness-110',
outline:
'border border-[var(--border-color)] bg-transparent text-[var(--main-color)] hover:bg-[var(--card-color)]',
ghost:
'bg-transparent text-[var(--main-color)] hover:bg-[var(--card-color)]',
// Add custom variants as needed
},
size: {
default: 'h-10 px-5',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-12 rounded-xl px-8 text-base',
icon: 'h-10 w-10',
},
},
},
);Phase 1: High-Priority Components (Current)
- ✅ Button
- ✅ Select
- 🔄 Input (next)
- 🔄 Checkbox (next)
Phase 2: Form Components
- 🔜 Form wrapper
- 🔜 Label
- 🔜 Textarea
- 🔜 Switch
Phase 3: Overlay Components
- 🔜 Dialog/Modal
- 🔜 Dropdown Menu
- 🔜 Tooltip
Migration Guidelines:
- Don't break existing UI - Migrate incrementally, one component at a time
- Test thoroughly - Verify all game modes and features still work
- Maintain theme compatibility - Always use CSS variables
- Update documentation - Document new shadcn components in this file
DO:
- ✅ Use CSS variables consistently
- ✅ Follow Radix UI patterns (shadcn is built on Radix)
- ✅ Use the
cn()utility for class management - ✅ Maintain semantic HTML structure
DON'T:
- ❌ Hardcode colors or theme values
- ❌ Create custom components that duplicate shadcn functionality
- ❌ Override Radix UI accessibility features
✅ DO:
// Use CSS variables for dynamic theming
<div className="bg-[var(--card-color)] text-[var(--main-color)]" />
// Use cn() for conditional classes
<button className={cn(
"px-4 py-2",
isActive && "bg-[var(--main-color)]"
)} />
// Extract repeated patterns to shared/lib/styles.ts
import { buttonBorderStyles } from '@/shared/lib/styles';
// Use responsive prefixes consistently
<div className="flex flex-col md:flex-row lg:gap-8" />❌ DON'T:
// Don't hardcode colors
<div className="bg-gray-100 text-black" />
// Don't use string concatenation for classes
<button className={"px-4 py-2 " + (isActive ? "bg-blue-500" : "")} />
// Don't repeat complex class strings
<button className="rounded-xl bg-[var(--card-color)] hover:bg-[var(--border-color)] transition-all duration-200" />
<div className="rounded-xl bg-[var(--card-color)] hover:bg-[var(--border-color)] transition-all duration-200" />
// Don't use arbitrary breakpoint values
<div className="flex flex-col min-[850px]:flex-row" /> // Use md: instead✅ DO:
// Use semantic heading levels
<h1 className="text-4xl font-bold">Main Title</h1>
<h2 className="text-2xl font-semibold">Section Title</h2>
// Use secondary color for less prominent text
<p className="text-[var(--secondary-color)]">Helper text</p>
// Use responsive text sizes
<h1 className="text-3xl md:text-4xl lg:text-5xl">Responsive Title</h1>❌ DON'T:
// Don't skip heading levels
<h1>Title</h1>
<h3>Skipped h2</h3>
// Don't use hardcoded gray values
<p className="text-gray-500">Text</p>
// Don't use fixed sizes that don't scale
<p className="text-[14px]">Fixed size text</p>✅ DO:
// Use Tailwind spacing scale
<div className="p-4 md:p-6 lg:p-8" />
<div className="space-y-4" /> // Consistent vertical spacing
<div className="gap-4 md:gap-6" /> // Responsive gaps in flex/grid
// Use consistent spacing within components
const spacing = "p-6 space-y-4";❌ DON'T:
// Don't use arbitrary values unnecessarily
<div className="p-[17px] space-y-[23px]" />
// Don't mix spacing units
<div className="p-4 mb-[2rem]" /> // Inconsistent✅ DO:
// Add hover states for interactive elements
<button className="hover:brightness-110 transition-duration-200" />
// Use focus-visible for keyboard navigation
<button className="focus-visible:ring-2 focus-visible:ring-[var(--main-color)]" />
// Provide feedback on active/pressed states
<button className="active:brightness-95" />
// Use appropriate cursor styles
<div className="cursor-pointer" onClick={handler} />❌ DON'T:
// Don't forget hover states
<button onClick={handler}>No hover feedback</button>
// Don't use focus instead of focus-visible (affects mouse users)
<button className="focus:ring-2" />
// Don't make non-interactive elements look clickable
<div className="cursor-pointer">Not actually clickable</div>✅ DO:
// Group related components
components/
Settings/
Themes.tsx
Preferences.tsx
Dojo/
Kana/
Kanji/
Vocab/
// Use TypeScript interfaces from lib/interfaces.ts
import { KanaCharacter } from '@/lib/interfaces';
// Use custom hooks from lib/hooks/
import { useClick } from '@/lib/hooks/useAudio';❌ DON'T:
// Don't create deeply nested component structures
components / Settings / ThemeSection / ThemesList / ThemeItem / index.tsx; // Too deep
// Don't duplicate interface definitions
interface KanaCharacter {} // Already in lib/interfaces.ts- Use
'use client'directive wisely - Only add to components that need client-side features (state, effects, event handlers) - Optimize images - Use Next.js Image component for automatic optimization
- Lazy load heavy components - Use dynamic imports for large components
- Memoize expensive calculations - Use
useMemoanduseCallbackappropriately - Minimize re-renders - Use Zustand selectors to subscribe to specific state slices
'use client';
import { cn } from '@/lib/utils';
import { cardBorderStyles } from '@/shared/lib/styles';
interface CardProps {
title: string;
description?: string;
children?: React.ReactNode;
className?: string;
}
const Card = ({ title, description, children, className }: CardProps) => {
return (
<div className={cn(cardBorderStyles, 'space-y-4 p-6', className)}>
<div className='space-y-2'>
<h3 className='text-xl font-semibold text-[var(--main-color)]'>
{title}
</h3>
{description && (
<p className='text-sm text-[var(--secondary-color)]'>{description}</p>
)}
</div>
{children}
</div>
);
};
export default Card;'use client';
import { cn } from '@/lib/utils';
import { buttonBorderStyles } from '@/shared/lib/styles';
import { useClick } from '@/lib/hooks/useAudio';
import { Check } from 'lucide-react';
interface SelectionButtonProps {
label: string;
isSelected: boolean;
onToggle: () => void;
}
const SelectionButton = ({
label,
isSelected,
onToggle,
}: SelectionButtonProps) => {
const { playClick } = useClick();
const handleClick = () => {
playClick();
onToggle();
};
return (
<button
onClick={handleClick}
className={cn(
buttonBorderStyles,
'flex items-center justify-between gap-3 p-4',
'transition-all duration-200',
isSelected && 'border-2 border-[var(--main-color)]',
)}
aria-pressed={isSelected}
>
<span className='font-medium text-[var(--main-color)]'>{label}</span>
{isSelected && (
<Check className='text-[var(--secondary-color)]' size={20} />
)}
</button>
);
};
export default SelectionButton;<div className='container mx-auto px-4 py-8 md:px-6 lg:px-8'>
<h1 className='mb-8 text-3xl font-bold text-[var(--main-color)] md:text-4xl'>
Character Selection
</h1>
<div className='grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-6 lg:grid-cols-4'>
{characters.map(char => (
<CharacterCard key={char.id} character={char} />
))}
</div>
</div>'use client';
import { motion, AnimatePresence } from 'motion/react';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
const Modal = ({ isOpen, onClose, title, children }: ModalProps) => {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Overlay */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className='fixed inset-0 z-40 bg-black/50'
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className={cn(
'fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
'max-h-[85vh] w-full max-w-md overflow-y-auto',
'rounded-2xl bg-[var(--background-color)] shadow-2xl',
'p-6',
)}
>
{/* Header */}
<div className='mb-6 flex items-center justify-between'>
<h2 className='text-2xl font-bold text-[var(--main-color)]'>
{title}
</h2>
<button
onClick={onClose}
className={cn(
'rounded-lg p-2',
'hover:bg-[var(--card-color)]',
'transition-colors duration-200',
'focus-visible:ring-2 focus-visible:ring-[var(--main-color)]',
)}
aria-label='Close modal'
>
<X className='text-[var(--secondary-color)]' size={24} />
</button>
</div>
{/* Content */}
<div className='space-y-4'>{children}</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default Modal;The project uses custom checkbox styling in app/globals.css:
/* Custom styled checkbox using CSS variables */
input[type='checkbox'] {
appearance: none;
-webkit-appearance: none;
background-color: var(--card-color);
border: 2px solid var(--border-color);
width: 1.1em;
height: 1.1em;
border-radius: 0.25em;
display: inline-block;
position: relative;
vertical-align: middle;
cursor: pointer;
transition:
border-color 0.2s,
background-color 0.2s;
}
input[type='checkbox']:checked {
background-color: var(--main-color);
border-color: var(--main-color);
}
input[type='checkbox']:checked::after {
content: '';
display: block;
position: absolute;
left: 0.28em;
top: 0.05em;
width: 0.3em;
height: 0.6em;
border: solid var(--background-color);
border-width: 0 0.18em 0.18em 0;
transform: rotate(45deg);
}Usage in JSX:
<label className='flex cursor-pointer items-center gap-2'>
<input
type='checkbox'
checked={isSelected}
onChange={e => setIsSelected(e.target.checked)}
className='focus-visible:ring-2 focus-visible:ring-[var(--main-color)]'
/>
<span className='text-[var(--main-color)]'>Option label</span>
</label>import { Button } from '@/components/ui/button';
// Default variant - primary action
<Button onClick={handleSubmit}>
Submit
</Button>
// Outline variant - secondary action
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
// Ghost variant - tertiary action
<Button variant="ghost" onClick={handleReset}>
Reset
</Button>
// Different sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<Settings size={20} />
</Button>
// With custom classes
<Button className="w-full md:w-auto">
Responsive Width
</Button>CLAUDE.md- Project overview and architectureCONTRIBUTING.md- Contribution guidelines and code stylefeatures/Preferences/data/themes.ts- Theme definitions and managementlib/interfaces.ts- TypeScript interfacesshared/lib/styles.ts- Reusable style constants
- Tailwind CSS Documentation
- shadcn/ui Documentation
- Next.js Documentation
- Framer Motion Documentation
- WCAG 2.1 Guidelines
- Radix UI Documentation
- WebAIM Contrast Checker
- Coolors.co - Color palette generator
- Realtime Colors - Theme visualization
- HSL Color Picker
This document should be updated whenever:
- New theming patterns are established
- shadcn/ui components are added or customized
- CSS variable naming conventions change
- New accessibility requirements are identified
- Major design system changes are implemented
Last Updated: January 2025 Maintained By: KanaDojo Team
Questions or Suggestions?
Please open an issue or discussion on GitHub to improve this documentation.