This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Pixel UI is a pixel-art styled React component library built on Base UI with Tailwind CSS v4. This is a pnpm monorepo containing:
packages/pixel-ui- The component library (published to npm as@joacod/pixel-ui)apps/www- Documentation app built with Next.js and Fumadocs (local development only)
# Development
pnpm dev # Run docs app locally with hot reload
pnpm build # Build everything (library + docs)
pnpm build:lib # Build library only
pnpm build:docs # Build docs only
pnpm type-check # Type check library
# Release workflow (uses Changesets)
pnpm changeset # Create a changeset for version management
pnpm release # Build library and publish to npm (CI handles this)IMPORTANT: To keep changelogs clean and separated by package, follow these rules:
- One package affected → Create one changeset file with only that package
- Multiple packages affected → Create separate changeset files for each package
This ensures each package's CHANGELOG only contains changes relevant to that specific package.
When to create separate changesets:
- ✅ Library changes (code, types, exports) → Separate changeset for
@joacod/pixel-ui - ✅ Documentation changes (MDX, examples, guides) → Separate changeset for
www - ✅ Both changed in one PR → Create TWO separate changeset files
Version bump guidelines:
major- Breaking changes to the public APIminor- New features, new components, significant enhancementspatch- Bug fixes, documentation updates, minor tweaks
Single package changeset:
---
'@joacod/pixel-ui': <major|minor|patch>
---
<Title of the change - imperative mood, capitalized>
<Optional: Brief description paragraph>
- <Bullet point describing the change>
- <Bullet point describing the change>
- <Bullet point describing the change>
<Optional: Additional sections for features, fixes, breaking changes, etc.>Example - Library change only:
---
'@joacod/pixel-ui': minor
---
Add Checkbox and CheckboxGroup components
Pixel-art styled form controls with accessibility features.
- New Checkbox component built on Base UI Checkbox primitive
- Compound component pattern with `Checkbox.Root` and `Checkbox.Indicator`
- States: checked, unchecked, indeterminate, disabled, readonly, required
- Pixel-art styling with box-shadow borders and instant transitions
- Full accessibility support with keyboard navigation and ARIA attributes
- New CheckboxGroup component for managing multiple checkbox state
- Supports controlled and uncontrolled modes
- Parent checkbox functionality for "select all" behavior via `allValues` propExample - Documentation change only:
---
'www': patch
---
Add Checkbox component documentation
- Comprehensive MDX documentation with interactive examples
- Usage examples for controlled and uncontrolled modes
- Accessibility guidelines and keyboard navigation examplesMultiple changesets in one PR:
When a PR includes both library and documentation changes, run pnpm changeset twice to create two separate files:
# First changeset for library changes
pnpm changeset
# Select @joacod/pixel-ui, describe library changes
# Second changeset for documentation changes
pnpm changeset
# Select www, describe documentation changesIMPORTANT: When asked about complete or improve current changesets that bump the @joacod/pixel-ui version, you MUST update the documentation app version reference:
- After creating a library changeset that changes the version (major/minor/patch)
- Update
apps/www/lib/config.ts→ ChangeLIBRARY_VERSIONto match the new version - This ensures the version badge in the docs navigation stays synchronized with the published package
Location: The version is displayed in the docs navigation header as a badge linking to npm. It's sourced from apps/www/lib/config.ts which exports:
LIBRARY_VERSION- The version string (e.g., '0.12.1')NPM_PACKAGE_URL- npm package linkGITHUB_REPO_URL- GitHub repository link
- Uses pnpm workspaces with workspace protocol (
workspace:*) for internal dependencies - Root package.json delegates commands to packages via
pnpm --filter packages/pixel-uiis the source of truth for componentsapps/wwwconsumes the library as a workspace dependency
Components follow this pattern:
- TypeScript component file (e.g.,
Button.tsx) using React.forwardRef - Separate styles file (e.g.,
Button.styles.ts) exporting Tailwind class strings organized by base/variants/sizes - Index file (e.g.,
index.ts) for clean exports - Components are built on Base UI primitives for accessibility
Example component structure:
src/components/Button/
├── Button.tsx # Component logic with forwardRef
├── Button.styles.ts # Tailwind classes organized by concern
└── index.ts # Exports
Design tokens exist in two places:
- TypeScript (
src/styles/tokens.ts) - Exports colors, sizes, spacing, fontSize constants and Variant/Size types - CSS (
src/styles/theme.css) - Tailwind v4 @theme directive with CSS custom properties
These should be kept in sync. The CSS version is the source of truth for Tailwind utilities.
Note: Design tokens are primarily for internal use by the library. The colors/sizes/spacing exports exist mainly for the documentation app and edge cases. Users should rely on the variant and size props rather than importing tokens directly.
The library exports a single all-in-one CSS file: components.css
This file includes everything consumers need:
- Design tokens from
theme.css(CSS custom properties via@themedirective) - Tailwind CSS utilities
- Base pixel rendering styles and utilities from
base.css(.pixel-border,.pixel-render, etc.) - Pixel font
- Pre-built component styles extracted from all
*.styles.tsfiles
The components.css file is generated at build time by:
- Scanning all
*.styles.tsfiles for Tailwind class strings - Creating a temporary CSS file that imports theme → tailwind → base → component classes
- Processing with Tailwind CLI to generate the final all-in-one CSS
- This eliminates the need for consumers to scan
node_modulesor use@sourcedirectives
Consumer setup (single import):
@import '@joacod/pixel-ui/components'; /* Everything you need */The library uses a three-step build process:
- generate-components-css.mjs: Extracts Tailwind classes from all
*.styles.tsfiles and generatessrc/styles/components.csswith pre-built component styles - tsdown: Bundles TypeScript to ESM in
dist/ - copy-styles.mjs: Copies all CSS files from
src/styles/todist/styles/and font assets fromsrc/assets/todist/assets/
All three run in sequence via pnpm build:lib
- 8px grid system: All spacing uses 8px increments (overrides Tailwind's default 4px)
- Pixel borders: Use
box-shadowinstead of traditional borders (see.pixel-borderutility) - No transitions:
transition-nonefor instant, retro feel - Pixel-perfect rendering:
image-rendering: pixelated, no font smoothing - Pixel font: Provided by @fontsource npm package, imported in base.css
Philosophy: Pixel UI is a "plug and play" library. Users apply colors through the variant prop, NOT by using color utility classes or customization.
Color Categories:
-
Semantic Colors (User-facing via
variantprop):base- Adapts between black/white for light/dark modeprimary- Main brand color (#3337FE blue)secondary- Alternative accent (#F15BFE pink)accent- Highlight color (#51DF21 green)ghost- Transparent varianterror- Error states (#FE7269 red)success- Success states (#51DF21 green)warning- Warning states (#ADB600 yellow)
-
Systematic Hover Colors (Used internally for interactive states):
- Each variant has a corresponding hover color following the pattern
nes[Variant]Hover nesPrimaryHover(#00237C) - Darker blue for primary hover statesnesSecondaryHover(#C92ED9) - Darker pink for secondary hover statesnesAccentHover(#093E00) - Darker green for accent hover statesnesErrorHover(#D64339) - Darker red for error hover statesnesWarningHover(#7D8400) - Darker yellow for warning hover statesnesSuccessHover(#51DF21) - Darker green for success hover states- Plus neutral colors:
nesBlack,nesGrayDark,nesGray,nesWhite
- Each variant has a corresponding hover color following the pattern
Rules:
- ❌ Don't add colors unless they serve a specific variant or internal styling purpose
- ❌ Don't export unused colors - if it's not used in components, remove it
- ✅ Keep tokens.ts and theme.css in sync - both define the same colors
- ✅ Run
pnpm build:libafter any color changes to regeneratecomponents.css - ✅ Use variant prop in docs - show users how to use variants, not custom colors
Example - Adding a new variant color:
If you need to add a new variant (e.g., "info"), you must:
- Add base and hover colors to
tokens.ts:nesInfo: '#...'andnesInfoHover: '#...' - Add both to
theme.css:--color-nes-info: #...and--color-nes-info-hover: #... - Add to
Varianttype intokens.ts:'info' - Add to component styles (e.g.,
Button.styles.ts):info: 'bg-nes-info hover:bg-nes-info-hover ...' - Rebuild library:
pnpm build:lib - Document in colors.mdx and component docs
When adding or modifying component styles, ensure proper dark mode support:
- Backgrounds: Use
dark:bg-nes-gray-darkfor component backgrounds - Borders: Always pair
border-nes-blackwithdark:border-nes-whitefor visibility - Box shadows: Add dark variants like
dark:shadow-[2px_2px_0_0_theme(colors.nes.white)] - Text colors: Pair
text-nes-blackwithdark:text-nes-white - Focus rings: Use
dark:focus-visible:ring-nes-secondaryfor better contrast - Hover states: Include dark mode shadows for interactive elements
Component-specific shadow guidelines:
-
Buttons (filled style): Use black shadows in light mode, white shadows in dark mode for maximum contrast
- Light mode:
shadow-[2px_2px_0_0_theme(colors.nes.black)] - Dark mode:
dark:shadow-[2px_2px_0_0_theme(colors.nes.white)] - This ensures colored buttons always have visible shadows regardless of theme
- Light mode:
-
Form controls (outline style): Use variant-colored shadows that match borders
- Example:
shadow-[2px_2px_0_0_theme(colors.nes.primary)] - Both border and shadow use the variant color for cohesive outline effect
- Example:
-
Toggles (checkbox, radio, switch): Use black/white shadows in unchecked state, variant-colored shadows when checked
Default dark mode colors:
- Background:
nes-gray-dark(#585858) - Text/Borders:
nes-white(#FFFFFF) - Primary accent colors (primary, secondary, accent, etc.) remain the same in both modes
When adding new components:
- Define props interface extending common patterns (variant, size, disabled, className)
- Use
React.forwardReffor ref forwarding - Create styles object in separate
.styles.tsfile with base/variants/sizes - Use
cn()utility (fromutils/cn.ts) to merge classes - Export component and types from index.ts
- Add to main
src/index.tsexports - Run
pnpm build:libto regeneratecomponents.csswith the new component's styles - Create MDX documentation in
apps/www/content/docs/components/ - Add component to
apps/www/content/docs/components/meta.jsonto make it visible in the documentation sidebar
Important: Component styles in .styles.ts files are extracted at build time and included in components.css. After adding or modifying component styles, always rebuild the library to regenerate this file.
Styling Philosophy:
- ✅ Use
variantprop for color variations (primary, secondary, error, etc.) - ✅ Use
sizeprop for size variations (xs, sm, md, lg, xl) - ✅ Support
classNameprop for layout/spacing overrides only (margin, width, etc.) - ❌ Don't encourage color customization via className - users should use variants
- 📝 In documentation: Show variant/size examples
- All components use TypeScript with strict mode
- Variant and Size types are centralized in
src/styles/tokens.ts - Props interfaces should extend
PixelComponentBasePropsor include standard props (variant, size, disabled, className)
- Built with Next.js 15 and Fumadocs (local development only, not deployed)
- MDX content in
apps/www/content/docs/ - Automatically imports and showcases components from
@joacod/pixel-ui - Run with
pnpm devfrom root