← All posts
·12 min read

Claude Code with Chakra UI: Accessible Components Done Fast

Claude CodeChakra UIReactAccessibility
Claude Code with Chakra UI: Accessible component-driven React

Why Chakra UI without CLAUDE.md generates inconsistent design systems

Chakra UI is a component library that pairs accessibility primitives with a style-prop API and a theme system. Out of the box you get accessible modals, menus, popovers, form controls, and toasts that handle focus management, keyboard navigation, and ARIA attributes correctly. The pitch is "accessible by default" and Chakra delivers on it for components used as documented.

The problem is that Claude Code does not know which Chakra patterns preserve the accessibility contract and which break it. Without explicit constraints, Claude generates Chakra code that compiles and renders but ships an inconsistent visual system: hardcoded hex colours instead of theme tokens, raw HTML elements wrapped in Chakra Box instead of semantic Chakra primitives, missing aria-labels on icon-only buttons, and components composed in ways that break Chakra's focus management.

The Chakra v3 migration (released late 2024 and now the recommended version) compounds this. Claude's training data spans both v2 and v3, and the two APIs differ enough that mixed code does not run. v3 introduced snippet-based component installation, slot recipes for theme customisation, and a new toaster API. Claude sometimes generates v2 imports against v3 packages and vice versa.

This guide covers the CLAUDE.md configuration that locks Claude Code into Chakra UI's correct model: theme tokens over hardcoded values, accessible composition, Chakra v3 conventions, provider setup that survives App Router, and the patterns that block the most common Chakra mistakes. For the React layer that Chakra components live inside, Claude Code with React covers component composition fundamentals. If you are choosing between Chakra and an alternative styling system, Claude Code with Tailwind covers the utility-class approach.

The Chakra UI CLAUDE.md template

# Chakra UI rules

## Stack
- @chakra-ui/react ^3.x (v3, NOT v2)
- @emotion/react ^11.x (Chakra v3 peer dependency)
- next-themes ^0.3.x for dark mode toggle (Chakra v3 pattern)
- React 18.x with Next.js 14.x App Router

## Project structure
- src/app/provider.tsx        ChakraProvider wrapper, used in layout.tsx
- src/theme/index.ts          Custom theme tokens (colors, fonts, spacing)
- src/theme/recipes/          Component-level slot recipes
- src/components/ui/          Snippet components from chakra-ui/cli
- src/components/             Project-specific compositions

## Chakra v3 specifics (CRITICAL)
- Components are installed via snippets: npx @chakra-ui/cli@latest snippet add toaster
- Snippets are checked into src/components/ui/, NOT imported from a package
- Theme customisation uses createSystem and slot recipes
- NO useDisclosure, NO useColorMode (v2 hooks), use:
  - useDisclosure from @chakra-ui/react still works in v3
  - useColorMode replaced by useTheme + next-themes
- ChakraProvider takes a `value` prop with system, not a theme prop

## Hard rules
- NEVER use hardcoded hex colors in component style props
- NEVER use raw HTML elements (<div>, <button>) inside Chakra components
- NEVER skip aria-label on IconButton or icon-only buttons
- NEVER mix Chakra v2 imports (@chakra-ui/react v2 patterns) with v3 patterns
- NEVER wrap content in Box just for layout, use VStack, HStack, Flex, Grid
- ALWAYS use Chakra primitives (Button, Input, Text) over raw HTML
- ALWAYS reference theme tokens: color="fg.default", NOT color="#1a1a1a"
- ALWAYS test components with keyboard navigation

The theme tokens rule prevents the most visible drift. Claude trained on inline styles defaults to passing literal values: <Box color="#1a1a1a" bg="white" />. The result works but breaks the moment dark mode is enabled because the hardcoded values do not switch. The correct pattern uses tokens that Chakra resolves against the active theme: <Box color="fg.default" bg="bg.canvas" />.

The v3 lock-in rule prevents a class of subtle bugs. Chakra v2 exported useColorMode from @chakra-ui/react. Chakra v3 deprecated it in favour of next-themes. Claude code that imports useColorMode against a v3 package compiles (because the export still exists for backward compatibility in some cases) but does not control the colour mode. Locking the project to v3 patterns in CLAUDE.md eliminates this drift.

The semantic primitive rule preserves accessibility. Chakra's Button includes focus management, keyboard interaction, and pressed states. A raw <button> wrapped in a Chakra Box loses all of that. Telling Claude to default to Chakra primitives over HTML elements maintains the accessibility contract.

Install and provider setup

Install Chakra v3 and its peer dependencies:

npm i @chakra-ui/react @emotion/react
npm i next-themes

Initialise the snippet system for v3 components:

npx @chakra-ui/cli@latest snippet add provider
npx @chakra-ui/cli@latest snippet add color-mode
npx @chakra-ui/cli@latest snippet add toaster

The snippets are copied into src/components/ui/. They are yours to modify, and Chakra updates do not overwrite them.

Wire the provider into your App Router layout. The snippet generates src/components/ui/provider.tsx:

// src/components/ui/provider.tsx
'use client';

import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
import { ThemeProvider, type ThemeProviderProps } from 'next-themes';

export function Provider(props: ThemeProviderProps) {
  return (
    <ChakraProvider value={defaultSystem}>
      <ThemeProvider attribute="class" disableTransitionOnChange {...props} />
    </ChakraProvider>
  );
}

Use it in src/app/layout.tsx:

// src/app/layout.tsx
import { Provider } from '@/components/ui/provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}

The suppressHydrationWarning on <html> is required because next-themes injects a class on the client to set the theme before React hydrates. Without suppressHydrationWarning, React logs a warning for every page load.

Add the provider setup to CLAUDE.md:

## Provider setup (REQUIRED)

- Run: npx @chakra-ui/cli@latest snippet add provider
- The snippet creates src/components/ui/provider.tsx with ChakraProvider + next-themes
- Use <Provider> in src/app/layout.tsx wrapping {children}
- Add suppressHydrationWarning to <html> tag (next-themes requirement)
- NEVER instantiate ChakraProvider inline anywhere else
- NEVER skip the next-themes ThemeProvider, dark mode breaks without it

Theme tokens and the design system

Chakra v3 ships with a default theme that covers most needs. The theme tokens follow a semantic naming scheme:

Token category Examples Use for
Colors (semantic) fg.default, bg.canvas, border.default Foreground, background, border
Colors (palette) blue.500, red.600, gray.200 Brand and accent colors
Spacing 1 (4px), 2 (8px), 4 (16px), 6 (24px) Padding, margin, gap
Font sizes xs, sm, md, lg, xl, 2xl Typography scale
Radii sm, md, lg, full Border radius

Use tokens in component props:

import { Box, Heading, Text, Stack } from '@chakra-ui/react';

function FeatureCard({ title, body }: { title: string; body: string }) {
  return (
    <Box
      p={6}
      borderWidth="1px"
      borderColor="border.default"
      borderRadius="lg"
      bg="bg.canvas"
    >
      <Stack gap={3}>
        <Heading size="md" color="fg.default">
          {title}
        </Heading>
        <Text color="fg.muted" fontSize="sm">
          {body}
        </Text>
      </Stack>
    </Box>
  );
}

Every value here is a token. p={6} is 24px from the spacing scale. color="fg.default" resolves to the appropriate text color for the active theme. borderRadius="lg" is the large radius token. Dark mode flips the values automatically.

The wrong pattern (Claude defaults without CLAUDE.md):

// DO NOT generate this
<Box
  p="24px"
  borderWidth="1px"
  borderColor="#e2e8f0"
  borderRadius="8px"
  bg="white"
>
  ...
</Box>

Every hardcoded value is a future inconsistency. The 24px padding does not respond to spacing-scale changes. The #e2e8f0 border colour does not switch in dark mode. The white background actively breaks dark mode.

Add token enforcement to CLAUDE.md:

## Theme token usage (HARD ENFORCE)

Colors:
- Use semantic tokens for foreground/background/border: fg.default, fg.muted, bg.canvas, bg.subtle, border.default
- Use palette tokens for brand/accent colors: blue.500, green.600, red.500
- NEVER use hex codes (#1a1a1a, #ffffff) in component props
- NEVER use rgb/rgba values inline

Spacing:
- Use scale numbers: p={4} (16px), m={6} (24px), gap={3} (12px)
- NEVER use px values: p="16px" (use p={4})

Typography:
- fontSize: xs | sm | md | lg | xl | 2xl
- NEVER use raw font sizes: fontSize="14px"

Layout primitives

Chakra v3 provides four layout primitives that cover most layout needs: Stack, HStack, VStack, Flex, Grid. Use them instead of Box with manual flex props.

import { HStack, VStack, Flex, Grid, Box, Button, Text } from '@chakra-ui/react';

// Vertical stack with consistent gap
<VStack gap={4} align="stretch">
  <Text>First item</Text>
  <Text>Second item</Text>
  <Text>Third item</Text>
</VStack>

// Horizontal stack with alignment
<HStack gap={3} justify="space-between">
  <Text>Label</Text>
  <Button>Action</Button>
</HStack>

// Flex with explicit direction (when Stack does not fit)
<Flex direction={{ base: 'column', md: 'row' }} gap={6} align="center">
  <Box flex="1">Content</Box>
  <Box width={{ md: '300px' }}>Sidebar</Box>
</Flex>

// Grid for two-dimensional layout
<Grid templateColumns={{ base: '1fr', md: 'repeat(3, 1fr)' }} gap={6}>
  <Box>Card 1</Box>
  <Box>Card 2</Box>
  <Box>Card 3</Box>
</Grid>

The responsive object syntax ({ base: 'column', md: 'row' }) maps directly to Chakra's breakpoints. base is the smallest screen, then sm, md, lg, xl, 2xl. Claude needs explicit instruction to use this pattern instead of CSS media queries.

Add layout primitives to CLAUDE.md:

## Layout primitives

- VStack: vertical layout, equivalent to Flex direction="column"
- HStack: horizontal layout, equivalent to Flex direction="row"
- Flex: when you need direction switching or wrap behaviour
- Grid: for two-dimensional layouts (cards, dashboards)
- Box: ONLY when you need a generic container with style props

NEVER use raw <div> inside Chakra trees, use Box.
NEVER use CSS media queries, use responsive object syntax: { base: 'value', md: 'value' }.

Accessible button patterns

Chakra's Button handles focus rings, keyboard interaction, and pressed states. The accessibility contract requires three rules: icon-only buttons get an aria-label, disabled buttons get an explanation, loading states are announced.

import { Button, IconButton, Spinner } from '@chakra-ui/react';
import { Search, Trash } from 'lucide-react';

// Text button (no aria-label needed, text is the label)
<Button colorPalette="blue" onClick={handleSave}>
  Save changes
</Button>

// Icon button MUST have aria-label
<IconButton aria-label="Delete item" onClick={handleDelete}>
  <Trash />
</IconButton>

// Loading state announces to screen readers
<Button loading={isSaving} colorPalette="blue" onClick={handleSave}>
  Save changes
</Button>

// Disabled with explanation via title or tooltip
<Button disabled title="Complete required fields to save">
  Save changes
</Button>

In Chakra v3, loading={true} (replacing v2's isLoading) automatically renders a spinner, disables the button, and announces "loading" to screen readers via aria-busy. Claude trained on v2 generates isLoading={true} which still works in some cases but is the deprecated form.

Add button accessibility to CLAUDE.md:

## Button accessibility

- Text buttons: text content is the accessible label, no aria-label needed
- Icon-only buttons (IconButton): aria-label="action description" is MANDATORY
- Loading state: use loading={true} (v3), NOT isLoading={true} (v2)
- Disabled state: pair with title or Tooltip explaining why
- colorPalette controls semantic colour: blue (primary), red (destructive), green (success)
- Avoid colorScheme (v2), use colorPalette (v3)

Form controls and field composition

Chakra v3's Field is the wrapper for accessible form inputs. It handles the label-input association, error message announcement, and required indicator.

import { Field, Input, Textarea, Stack, Button } from '@chakra-ui/react';
import { useState } from 'react';

function ContactForm() {
  const [errors, setErrors] = useState<{ email?: string }>({});

  return (
    <form onSubmit={(e) => { e.preventDefault(); /* submit */ }}>
      <Stack gap={4}>
        <Field.Root invalid={!!errors.email}>
          <Field.Label>Email address</Field.Label>
          <Input type="email" name="email" placeholder="you@example.com" />
          {errors.email && <Field.ErrorText>{errors.email}</Field.ErrorText>}
          <Field.HelperText>We will never share your email</Field.HelperText>
        </Field.Root>

        <Field.Root>
          <Field.Label>Message</Field.Label>
          <Textarea name="message" rows={5} />
        </Field.Root>

        <Button type="submit" colorPalette="blue">
          Send message
        </Button>
      </Stack>
    </form>
  );
}

Field.Root is the accessibility wrapper. It generates the label-input association via htmlFor/id, exposes error state via aria-invalid, and announces error text via aria-describedby. Skipping Field.Root and using a raw <label> + Input pair compiles but loses the ARIA wiring.

Add form patterns to CLAUDE.md:

## Form composition

- Wrap every input in Field.Root for ARIA wiring
- Field.Root composes: Field.Label, Input/Textarea/Select, Field.ErrorText, Field.HelperText
- invalid prop on Field.Root toggles error state on the input
- Field.ErrorText is announced to screen readers when invalid={true}
- Field.HelperText is associated via aria-describedby
- NEVER use raw <label> for Chakra inputs, use Field.Label
- Form submissions: prevent default, validate, set errors per-field

Dark mode toggle

Chakra v3 delegates dark mode to next-themes. The pattern uses next-themes hooks for state and Chakra's useTheme for context.

// src/components/ui/color-mode-toggle.tsx
'use client';

import { IconButton } from '@chakra-ui/react';
import { useTheme } from 'next-themes';
import { Moon, Sun } from 'lucide-react';

export function ColorModeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <IconButton
      aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      variant="ghost"
    >
      {theme === 'dark' ? <Sun /> : <Moon />}
    </IconButton>
  );
}

The aria-label includes the destination state, not the current state. A screen reader announces "switch to dark mode" rather than "dark mode toggle", which matches the action the button performs.

Use the toggle anywhere in your layout:

import { HStack, Heading } from '@chakra-ui/react';
import { ColorModeToggle } from '@/components/ui/color-mode-toggle';

<HStack justify="space-between" p={4}>
  <Heading size="lg">Claudify</Heading>
  <ColorModeToggle />
</HStack>

Toaster API for notifications

Chakra v3 replaced the v2 useToast hook with a snippet-based toaster you control. Add it:

npx @chakra-ui/cli@latest snippet add toaster

The snippet generates src/components/ui/toaster.tsx with a typed toaster instance. Use it:

// In your root layout
import { Toaster } from '@/components/ui/toaster';

<Provider>
  <Toaster />
  {children}
</Provider>

// Anywhere in the app
import { toaster } from '@/components/ui/toaster';

function handleSave() {
  try {
    await save();
    toaster.create({
      title: 'Saved successfully',
      type: 'success',
    });
  } catch (err) {
    toaster.create({
      title: 'Save failed',
      description: err instanceof Error ? err.message : 'Unknown error',
      type: 'error',
    });
  }
}

The four toast types success, error, warning, info map to semantic colour palettes. Custom durations, actions, and positions are all supported via the toaster.create options.

Claude trained on v2 generates useToast() hook calls. v3's pattern is import { toaster } from '@/components/ui/toaster' and toaster.create({...}). Lock this in CLAUDE.md so Claude does not regress to v2.

Common Claude Code mistakes with Chakra UI

Six patterns Claude generates incorrectly without CLAUDE.md constraints.

1. Hardcoded colour values

Claude generates: <Box bg="#ffffff" color="#1a1a1a" />.

Correct pattern: <Box bg="bg.canvas" color="fg.default" />.

2. v2 imports against v3 packages

Claude generates: import { useToast } from '@chakra-ui/react' (v2 hook against v3 package).

Correct pattern: import { toaster } from '@/components/ui/toaster' (snippet-based v3 API).

3. Missing aria-label on IconButton

Claude generates: <IconButton onClick={handleDelete}><Trash /></IconButton>.

Correct pattern: <IconButton aria-label="Delete item" onClick={handleDelete}><Trash /></IconButton>.

4. Raw HTML inside Chakra components

Claude generates: <Box><button onClick={...}>Click</button></Box>.

Correct pattern: <Button onClick={...}>Click</Button>.

5. CSS media queries instead of responsive objects

Claude generates: a custom CSS media query for breakpoint behaviour.

Correct pattern: <Flex direction={{ base: 'column', md: 'row' }} />.

6. Form input without Field.Root

Claude generates: <label>Email</label><Input type="email" />.

Correct pattern: <Field.Root><Field.Label>Email</Field.Label><Input type="email" /></Field.Root>.

Add these as before/after blocks in CLAUDE.md.

Custom theme tokens

Override the default theme with your brand colours and tokens. Chakra v3 uses createSystem:

// src/theme/index.ts
import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react';

const config = defineConfig({
  theme: {
    tokens: {
      colors: {
        brand: {
          50: { value: '#f0f7ff' },
          100: { value: '#d9e9ff' },
          500: { value: '#0070f3' },
          900: { value: '#001a4d' },
        },
      },
      fonts: {
        heading: { value: 'Geist, sans-serif' },
        body: { value: 'Geist, sans-serif' },
      },
    },
    semanticTokens: {
      colors: {
        'brand.primary': {
          value: { base: '{colors.brand.500}', _dark: '{colors.brand.100}' },
        },
      },
    },
  },
});

export const system = createSystem(defaultConfig, config);

Pass the system to ChakraProvider:

import { ChakraProvider } from '@chakra-ui/react';
import { system } from '@/theme';

<ChakraProvider value={system}>
  {children}
</ChakraProvider>

Reference custom tokens in components: <Button colorPalette="brand">Action</Button>. The semantic token brand.primary switches values between light and dark mode.

For the broader styling architecture decision between Chakra and Tailwind, Claude Code with Tailwind covers the utility-class alternative.

Permission hooks for Chakra workflows

Chakra projects benefit from snippet management and theme generation scripts. Gate the destructive ones.

{
  "permissions": {
    "allow": [
      "Bash(npx @chakra-ui/cli@latest snippet add*)",
      "Bash(npx @chakra-ui/cli@latest snippet list*)",
      "Bash(node scripts/generate-tokens.js*)"
    ],
    "deny": [
      "Bash(npx @chakra-ui/cli@latest snippet remove*)",
      "Bash(rm -rf src/components/ui*)"
    ]
  }
}

Building Chakra UIs that ship accessible

The Chakra UI CLAUDE.md in this guide produces React applications where theme tokens replace hardcoded values, layout primitives replace Box+flex compositions, every icon button has an aria-label, Field.Root wraps every form input, dark mode works without manual CSS, and v3 patterns (loading prop, toaster snippet, colorPalette) replace v2 leftovers.

The underlying principle: Chakra ships an accessibility contract that requires correct composition. Claude has no signal about which composition rules preserve that contract without explicit instruction. Every rule you skip ships a component that looks right and screen-readers cannot parse.

For the React fundamentals Chakra sits on top of, Claude Code with React covers component composition patterns, and Claudify includes a Chakra-specific CLAUDE.md template with v3 conventions, theme token rules, accessibility patterns, and all six common-mistake rules pre-configured.

Get Claudify. Ship Chakra interfaces that stay on brand and accessible.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame