We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide24 min read

Chakra UI, accessible React components in version 3

Chakra UI is an accessible React component library. Version 3, migrating from v2, style props, theming, and a comparison with Mantine.

Chakra UI, accessible React components in version 3

Chakra UI is a React component library where styles are passed as component props and accessibility is built in rather than bolted on later. The current version is 3.36.1 under the MIT licence, built on top of Ark UI. Version 3 is a ground up rewrite, so most material circulating online describes an interface that no longer exists.

How Chakra UI differs from the rest

The styling approach is its defining feature and also what splits people into two camps. Rather than writing a stylesheet or a list of utility classes, you pass styles as props: padding, bg, fontSize. Values come from the theme, so padding="4" refers to a spacing scale rather than a pixel count.

The upside is obvious while writing: no context switch between the component file and a stylesheet, and editor completion offers the theme's available values. The downside shows when reading somebody else's code, because a component carrying fifteen style props gets dense, and conditional styling turns into expressions inside JSX.

The second trait is accessibility. Composite components, meaning dialogs, dropdowns, tabs, and menus, handle the keyboard, manage focus, and carry correct role attributes. That is work nobody enjoys doing by hand and it decides whether an interface can be operated without a mouse. In version 3 this layer moved to Ark UI, an unstyled behaviour library from the same team.

The third is the library's footprint. This is not a set of blocks to copy but a dependency with its own theming system, style generator, and conventions. You install it once and build the whole application on it, unlike shadcn/ui, where component code lands in your repository and stops being a dependency.

Version 3, what changed and why it hurts

This is the most important section here, because it addresses the most common problem: an example found online does not work and the error message does not explain why. Version 3 renamed packages, components, and props at once.

Dependencies got shorter. The @emotion/styled and framer-motion packages are no longer required, and @chakra-ui/icons was removed in favour of external icon sets. The @chakra-ui/next-js package is gone too, since styling Next.js components now goes through the asChild prop.

Theme configuration works differently. extendTheme gave way to createSystem with defaultConfig, and token values require wrapping in an object with a value field. The provider now takes value instead of theme.

Colour mode moved outside the library. useColorMode and ColorModeProvider are gone, their role taken by next-themes, which in practice means you write the theme toggle once and it behaves the same inside and outside Chakra.

Composite components were renamed and restructured. Modal is now Dialog.Root, Divider is Separator, Collapse is Collapsible.Root, Stepper is Steps.Root, and CircularProgress is ProgressCircle. Boolean props lost their prefix: isOpen became open, isDisabled is disabled, isInvalid is invalid. Styling names changed too, colorScheme to colorPalette and noOfLines to lineClamp among them.

The good news is that most of this can be automated with npx @chakra-ui/codemod upgrade. The tool renames components, fixes props and imports, and restructures composite components too, so dialogs and steppers get their new shape from dedicated transforms. What stays manual is the rest: dropping the unused packages, rewriting the theme from extendTheme to createSystem, and swapping the provider for the one from the snippets. On a large application, plan that as its own task rather than a footnote to some other work, since the transform's output still needs reviewing. Node 20 or newer is required for it.

The code examples further down this article still come from version 2. I am leaving them in place, since the concepts and patterns did not change, but check prop and component names against current documentation before pasting anything.

Chakra UI against the alternatives

FeatureChakra UIMantineshadcn/uiTailwind CSS
Styling methodcomponent propsclasses and CSS modulesutility classesutility classes
Distribution modelproject dependencyproject dependencycopied codedependency
Composite component accessibilitybuilt inbuilt inbuilt innone, styles only
Control over component codethrough the themethrough the themecompletenot applicable
Updatesautomaticautomaticmanualautomatic
Entry barrierlowlowmediummedium

The split here runs along one axis: whether you want components to update themselves or prefer their code in your own tree. Chakra and Mantine belong to the first group, so a dialog bug fix arrives with a package update. shadcn/ui belongs to the second, giving you complete freedom and complete responsibility.

Comparing Chakra with Tailwind alone is a category error that keeps resurfacing in discussions. Tailwind is a styling layer with no components, so a dialog with keyboard handling and a focus trap is something you write yourself or add a separate behaviour library for. These are not competitors but different levels of the stack.

When Chakra is the wrong choice

It is fair to name the situations where something else serves better. The first is a marketing site or landing page where download size matters. A library carrying a theming system and a style generator adds considerably more there than it returns.

The second is a project with an established design system built on unusual rules. Chakra assumes a particular way of thinking about spacing, colour, and typography scales, and forcing a system built on other assumptions into it ends with overriding most of the defaults. In that case an unstyled layer, Ark UI or Radix UI, delivers the same accessibility without the imposed aesthetic.

The third is a team already working in utility classes. Mixing two styling models in one application produces arguments about conventions and a codebase where half the components look nothing like the other half.

There is a fourth case, mentioned less often and in practice the most painful. It concerns a project that has to survive several years without major rewrites. Version three showed that this library can rename components and props in a single release, and the migration tool does not cover everything. If an application is meant to live long and the team has no budget for periodic migrations, an unstyled layer or copied component code ages more calmly, because nobody renames those on you remotely.

Installation and setup

Installation

Version 3 needs two packages instead of four. If @emotion/styled and framer-motion linger in your project solely for Chakra's sake, they can go after the migration.

Code
Bash
npm i @chakra-ui/react @emotion/react

Ready made snippets, the provider and theme toggle among them, come from the command line tool. It copies code into your repository, so you are free to change it.

Code
Bash
npx @chakra-ui/cli snippet add

Provider setup

In version 3 the provider composes ChakraProvider for styling and ThemeProvider from next-themes for colour mode.

Code
TypeScript
import { Provider } from '@/components/ui/provider'

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider>{children}</Provider>
}

The snippet below shows the older version 2 arrangement built on extendTheme. I am keeping it as a migration reference, since plenty of projects still look like this.

TSapp/providers.tsx
TypeScript
// app/providers.tsx (Next.js App Router, Chakra v2)
'use client'

import { ChakraProvider, extendTheme } from '@chakra-ui/react'

const theme = extendTheme({
  // Custom theme configuration
})

export function Providers({ children }: { children: React.ReactNode }) {
  return <ChakraProvider theme={theme}>{children}</ChakraProvider>
}
TSapp/layout.tsx
TypeScript
// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pl">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

For Pages Router (Next.js)

TSpages/_app.tsx
TypeScript
// pages/_app.tsx
import { ChakraProvider } from '@chakra-ui/react'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  )
}

Style Props - the heart of Chakra UI

Style Props basics

Code
TypeScript
import { Box, Text, Flex, Stack } from '@chakra-ui/react'

function StylePropsDemo() {
  return (
    <>
      {/* Spacing (margin, padding) */}
      <Box m={4} p={6}>Margin 16px, Padding 24px</Box>
      <Box mt={2} mb={4} px={8}>Top margin 8px, Bottom 16px, Horizontal padding 32px</Box>

      {/* Colors */}
      <Box bg="blue.500" color="white">Blue background, white text</Box>
      <Box bg="gray.100" color="gray.800">Gray shades</Box>
      <Box bgGradient="linear(to-r, blue.500, purple.500)">Gradient</Box>

      {/* Typography */}
      <Text fontSize="xl" fontWeight="bold">Extra large bold</Text>
      <Text fontSize="sm" color="gray.500">Small gray text</Text>
      <Text textAlign="center" textTransform="uppercase">Centered uppercase</Text>

      {/* Borders */}
      <Box border="1px" borderColor="gray.200" borderRadius="md">Border</Box>
      <Box borderWidth="2px" borderStyle="dashed" borderColor="blue.500">Dashed border</Box>

      {/* Layout */}
      <Box w="100%" h="200px">Full width, 200px height</Box>
      <Box maxW="container.md" mx="auto">Centered container</Box>

      {/* Flexbox */}
      <Flex justify="space-between" align="center" gap={4}>
        <Box>Item 1</Box>
        <Box>Item 2</Box>
        <Box>Item 3</Box>
      </Flex>

      {/* Stack (simplified flex) */}
      <Stack spacing={4} direction="row">
        <Box>Item 1</Box>
        <Box>Item 2</Box>
      </Stack>

      {/* Position */}
      <Box position="relative">
        <Box position="absolute" top={0} right={0}>Positioned</Box>
      </Box>

      {/* Shadow */}
      <Box shadow="md">Medium shadow</Box>
      <Box shadow="lg">Large shadow</Box>
      <Box shadow="2xl">Extra large shadow</Box>
    </>
  )
}

Responsive styles

Code
TypeScript
import { Box, Text, Flex, SimpleGrid } from '@chakra-ui/react'

function ResponsiveDemo() {
  return (
    <>
      {/* Array syntax (mobile-first) */}
      <Box
        fontSize={['sm', 'md', 'lg', 'xl']}
        // sm (base), md (480px), lg (768px), xl (992px)
      >
        Responsive font size
      </Box>

      {/* Object syntax */}
      <Box
        fontSize={{ base: 'sm', md: 'lg', xl: '2xl' }}
        p={{ base: 2, md: 4, lg: 8 }}
        bg={{ base: 'blue.100', md: 'green.100', lg: 'purple.100' }}
      >
        Object syntax
      </Box>

      {/* Responsive Flex direction */}
      <Flex
        direction={{ base: 'column', md: 'row' }}
        gap={4}
      >
        <Box flex={1}>Sidebar</Box>
        <Box flex={3}>Main content</Box>
      </Flex>

      {/* SimpleGrid with responsive columns */}
      <SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
        <Box bg="gray.100" p={4}>Card 1</Box>
        <Box bg="gray.100" p={4}>Card 2</Box>
        <Box bg="gray.100" p={4}>Card 3</Box>
      </SimpleGrid>

      {/* Hide/Show on breakpoints */}
      <Box display={{ base: 'none', md: 'block' }}>
        Visible only on md and up
      </Box>
      <Box display={{ base: 'block', md: 'none' }}>
        Visible only on mobile
      </Box>
    </>
  )
}

Pseudo styles

Code
TypeScript
import { Box, Button } from '@chakra-ui/react'

function PseudoDemo() {
  return (
    <>
      {/* Hover */}
      <Box
        bg="blue.500"
        _hover={{ bg: 'blue.600', transform: 'scale(1.05)' }}
        transition="all 0.2s"
      >
        Hover me
      </Box>

      {/* Focus */}
      <Button
        _focus={{ boxShadow: 'outline', outline: 'none' }}
        _focusVisible={{ ring: 2, ringColor: 'blue.500' }}
      >
        Focus me
      </Button>

      {/* Active */}
      <Button
        _active={{ bg: 'blue.700', transform: 'scale(0.98)' }}
      >
        Click me
      </Button>

      {/* Disabled */}
      <Button
        isDisabled
        _disabled={{ opacity: 0.5, cursor: 'not-allowed' }}
      >
        Disabled
      </Button>

      {/* Before/After */}
      <Box
        position="relative"
        _before={{
          content: '""',
          position: 'absolute',
          top: 0,
          left: 0,
          right: 0,
          height: '4px',
          bg: 'blue.500',
        }}
      >
        Box with top border
      </Box>

      {/* Dark mode specific */}
      <Box
        bg="white"
        color="gray.800"
        _dark={{ bg: 'gray.800', color: 'white' }}
      >
        Light/dark mode aware
      </Box>
    </>
  )
}

Components

Button

Code
TypeScript
import { Button, IconButton, ButtonGroup, Stack } from '@chakra-ui/react'
import { AddIcon, DeleteIcon, SettingsIcon } from '@chakra-ui/icons'

function ButtonDemo() {
  return (
    <Stack spacing={4}>
      {/* Variants */}
      <ButtonGroup spacing={2}>
        <Button colorScheme="blue">Solid (default)</Button>
        <Button colorScheme="blue" variant="outline">Outline</Button>
        <Button colorScheme="blue" variant="ghost">Ghost</Button>
        <Button colorScheme="blue" variant="link">Link</Button>
      </ButtonGroup>

      {/* Color schemes */}
      <ButtonGroup spacing={2}>
        <Button colorScheme="gray">Gray</Button>
        <Button colorScheme="red">Red</Button>
        <Button colorScheme="green">Green</Button>
        <Button colorScheme="blue">Blue</Button>
        <Button colorScheme="teal">Teal</Button>
        <Button colorScheme="purple">Purple</Button>
      </ButtonGroup>

      {/* Sizes */}
      <ButtonGroup spacing={2}>
        <Button size="xs">Extra small</Button>
        <Button size="sm">Small</Button>
        <Button size="md">Medium</Button>
        <Button size="lg">Large</Button>
      </ButtonGroup>

      {/* States */}
      <ButtonGroup spacing={2}>
        <Button isLoading>Loading</Button>
        <Button isLoading loadingText="Saving...">With text</Button>
        <Button isDisabled>Disabled</Button>
      </ButtonGroup>

      {/* With icons */}
      <ButtonGroup spacing={2}>
        <Button leftIcon={<AddIcon />} colorScheme="blue">
          Add item
        </Button>
        <Button rightIcon={<DeleteIcon />} colorScheme="red" variant="outline">
          Delete
        </Button>
      </ButtonGroup>

      {/* Icon buttons */}
      <ButtonGroup spacing={2}>
        <IconButton
          aria-label="Add"
          icon={<AddIcon />}
          colorScheme="blue"
        />
        <IconButton
          aria-label="Settings"
          icon={<SettingsIcon />}
          variant="outline"
        />
        <IconButton
          aria-label="Delete"
          icon={<DeleteIcon />}
          colorScheme="red"
          isRound
        />
      </ButtonGroup>
    </Stack>
  )
}

Form Controls

Code
TypeScript
import {
  FormControl,
  FormLabel,
  FormErrorMessage,
  FormHelperText,
  Input,
  InputGroup,
  InputLeftAddon,
  InputRightElement,
  Textarea,
  Select,
  Checkbox,
  CheckboxGroup,
  Radio,
  RadioGroup,
  Switch,
  Slider,
  SliderTrack,
  SliderFilledTrack,
  SliderThumb,
  NumberInput,
  NumberInputField,
  NumberInputStepper,
  NumberIncrementStepper,
  NumberDecrementStepper,
  PinInput,
  PinInputField,
  Stack,
  HStack,
  Button,
} from '@chakra-ui/react'
import { useState } from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'

function FormDemo() {
  const [showPassword, setShowPassword] = useState(false)
  const [email, setEmail] = useState('')
  const isError = email === ''

  return (
    <Stack spacing={6} maxW="md">
      {/* Basic Input */}
      <FormControl>
        <FormLabel>Name</FormLabel>
        <Input placeholder="Enter your name" />
        <FormHelperText>We'll never share your name.</FormHelperText>
      </FormControl>

      {/* Input with error */}
      <FormControl isInvalid={isError}>
        <FormLabel>Email</FormLabel>
        <Input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="you@example.com"
        />
        {isError ? (
          <FormErrorMessage>Email is required.</FormErrorMessage>
        ) : (
          <FormHelperText>Enter your email address.</FormHelperText>
        )}
      </FormControl>

      {/* Password with show/hide */}
      <FormControl>
        <FormLabel>Password</FormLabel>
        <InputGroup>
          <Input
            type={showPassword ? 'text' : 'password'}
            placeholder="Enter password"
          />
          <InputRightElement>
            <Button
              size="sm"
              variant="ghost"
              onClick={() => setShowPassword(!showPassword)}
            >
              {showPassword ? <ViewOffIcon /> : <ViewIcon />}
            </Button>
          </InputRightElement>
        </InputGroup>
      </FormControl>

      {/* Input with addon */}
      <FormControl>
        <FormLabel>Website</FormLabel>
        <InputGroup>
          <InputLeftAddon>https://</InputLeftAddon>
          <Input placeholder="mysite.com" />
        </InputGroup>
      </FormControl>

      {/* Textarea */}
      <FormControl>
        <FormLabel>Description</FormLabel>
        <Textarea placeholder="Enter description" resize="vertical" />
      </FormControl>

      {/* Select */}
      <FormControl>
        <FormLabel>Country</FormLabel>
        <Select placeholder="Select country">
          <option value="pl">Poland</option>
          <option value="de">Germany</option>
          <option value="us">United States</option>
        </Select>
      </FormControl>

      {/* Checkbox */}
      <FormControl>
        <Checkbox colorScheme="blue">
          I agree to terms and conditions
        </Checkbox>
      </FormControl>

      {/* Checkbox group */}
      <FormControl>
        <FormLabel>Interests</FormLabel>
        <CheckboxGroup colorScheme="blue" defaultValue={['react']}>
          <Stack spacing={2}>
            <Checkbox value="react">React</Checkbox>
            <Checkbox value="vue">Vue</Checkbox>
            <Checkbox value="angular">Angular</Checkbox>
          </Stack>
        </CheckboxGroup>
      </FormControl>

      {/* Radio group */}
      <FormControl>
        <FormLabel>Payment method</FormLabel>
        <RadioGroup defaultValue="card">
          <Stack direction="row" spacing={4}>
            <Radio value="card">Credit Card</Radio>
            <Radio value="paypal">PayPal</Radio>
            <Radio value="bank">Bank Transfer</Radio>
          </Stack>
        </RadioGroup>
      </FormControl>

      {/* Switch */}
      <FormControl display="flex" alignItems="center">
        <FormLabel mb={0}>Enable notifications?</FormLabel>
        <Switch colorScheme="blue" />
      </FormControl>

      {/* Slider */}
      <FormControl>
        <FormLabel>Volume</FormLabel>
        <Slider defaultValue={30} min={0} max={100}>
          <SliderTrack>
            <SliderFilledTrack />
          </SliderTrack>
          <SliderThumb />
        </Slider>
      </FormControl>

      {/* Number input */}
      <FormControl>
        <FormLabel>Quantity</FormLabel>
        <NumberInput defaultValue={1} min={1} max={20}>
          <NumberInputField />
          <NumberInputStepper>
            <NumberIncrementStepper />
            <NumberDecrementStepper />
          </NumberInputStepper>
        </NumberInput>
      </FormControl>

      {/* PIN input */}
      <FormControl>
        <FormLabel>Verification code</FormLabel>
        <HStack>
          <PinInput>
            <PinInputField />
            <PinInputField />
            <PinInputField />
            <PinInputField />
          </PinInput>
        </HStack>
      </FormControl>
    </Stack>
  )
}

Modal

Code
TypeScript
import {
  Modal,
  ModalOverlay,
  ModalContent,
  ModalHeader,
  ModalFooter,
  ModalBody,
  ModalCloseButton,
  useDisclosure,
  Button,
  FormControl,
  FormLabel,
  Input,
  VStack,
} from '@chakra-ui/react'
import { useRef } from 'react'

function ModalDemo() {
  const { isOpen, onOpen, onClose } = useDisclosure()
  const initialRef = useRef<HTMLInputElement>(null)

  return (
    <>
      <Button onClick={onOpen} colorScheme="blue">
        Open Modal
      </Button>

      <Modal
        isOpen={isOpen}
        onClose={onClose}
        initialFocusRef={initialRef}
        isCentered
      >
        <ModalOverlay />
        <ModalContent>
          <ModalHeader>Create your account</ModalHeader>
          <ModalCloseButton />

          <ModalBody>
            <VStack spacing={4}>
              <FormControl>
                <FormLabel>Name</FormLabel>
                <Input ref={initialRef} placeholder="Enter your name" />
              </FormControl>
              <FormControl>
                <FormLabel>Email</FormLabel>
                <Input type="email" placeholder="you@example.com" />
              </FormControl>
            </VStack>
          </ModalBody>

          <ModalFooter>
            <Button variant="ghost" mr={3} onClick={onClose}>
              Cancel
            </Button>
            <Button colorScheme="blue" onClick={onClose}>
              Create
            </Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </>
  )
}

// Alert Dialog (for confirmations)
import {
  AlertDialog,
  AlertDialogBody,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogContent,
  AlertDialogOverlay,
} from '@chakra-ui/react'

function AlertDialogDemo() {
  const { isOpen, onOpen, onClose } = useDisclosure()
  const cancelRef = useRef<HTMLButtonElement>(null)

  return (
    <>
      <Button colorScheme="red" onClick={onOpen}>
        Delete Item
      </Button>

      <AlertDialog
        isOpen={isOpen}
        leastDestructiveRef={cancelRef}
        onClose={onClose}
        isCentered
      >
        <AlertDialogOverlay>
          <AlertDialogContent>
            <AlertDialogHeader>Delete Item</AlertDialogHeader>

            <AlertDialogBody>
              Are you sure? You can't undo this action afterwards.
            </AlertDialogBody>

            <AlertDialogFooter>
              <Button ref={cancelRef} onClick={onClose}>
                Cancel
              </Button>
              <Button colorScheme="red" onClick={onClose} ml={3}>
                Delete
              </Button>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialogOverlay>
      </AlertDialog>
    </>
  )
}

// Drawer (side panel)
import {
  Drawer,
  DrawerBody,
  DrawerFooter,
  DrawerHeader,
  DrawerOverlay,
  DrawerContent,
  DrawerCloseButton,
} from '@chakra-ui/react'

function DrawerDemo() {
  const { isOpen, onOpen, onClose } = useDisclosure()

  return (
    <>
      <Button onClick={onOpen}>Open Menu</Button>

      <Drawer isOpen={isOpen} placement="left" onClose={onClose}>
        <DrawerOverlay />
        <DrawerContent>
          <DrawerCloseButton />
          <DrawerHeader>Navigation</DrawerHeader>

          <DrawerBody>
            <VStack align="stretch" spacing={4}>
              <Button variant="ghost" justifyContent="flex-start">Home</Button>
              <Button variant="ghost" justifyContent="flex-start">About</Button>
              <Button variant="ghost" justifyContent="flex-start">Contact</Button>
            </VStack>
          </DrawerBody>

          <DrawerFooter>
            <Button variant="outline" mr={3} onClick={onClose}>
              Close
            </Button>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    </>
  )
}

Toast

Code
TypeScript
import { Button, useToast, Stack } from '@chakra-ui/react'

function ToastDemo() {
  const toast = useToast()

  return (
    <Stack spacing={4}>
      {/* Basic toast */}
      <Button
        onClick={() =>
          toast({
            title: 'Account created.',
            description: "We've created your account for you.",
            status: 'success',
            duration: 5000,
            isClosable: true,
          })
        }
      >
        Show Success Toast
      </Button>

      {/* Error toast */}
      <Button
        colorScheme="red"
        onClick={() =>
          toast({
            title: 'Error occurred.',
            description: 'Unable to create your account.',
            status: 'error',
            duration: 5000,
            isClosable: true,
          })
        }
      >
        Show Error Toast
      </Button>

      {/* Warning toast */}
      <Button
        colorScheme="orange"
        onClick={() =>
          toast({
            title: 'Warning',
            description: 'Your session will expire soon.',
            status: 'warning',
            duration: 5000,
            isClosable: true,
          })
        }
      >
        Show Warning Toast
      </Button>

      {/* Info toast */}
      <Button
        colorScheme="blue"
        onClick={() =>
          toast({
            title: 'Info',
            description: 'Chakra UI is awesome!',
            status: 'info',
            duration: 5000,
            isClosable: true,
          })
        }
      >
        Show Info Toast
      </Button>

      {/* Position */}
      <Button
        onClick={() =>
          toast({
            title: 'Top right toast',
            status: 'info',
            position: 'top-right',
          })
        }
      >
        Top Right Toast
      </Button>

      {/* Promise toast */}
      <Button
        onClick={() => {
          const promise = new Promise((resolve) => {
            setTimeout(() => resolve('Data loaded'), 2000)
          })

          toast.promise(promise, {
            success: { title: 'Success', description: 'Data loaded' },
            error: { title: 'Error', description: 'Something went wrong' },
            loading: { title: 'Loading', description: 'Please wait...' },
          })
        }}
      >
        Promise Toast
      </Button>

      {/* Closeable all */}
      <Button onClick={() => toast.closeAll()}>
        Close All Toasts
      </Button>
    </Stack>
  )
}

Card

Code
TypeScript
import {
  Card,
  CardHeader,
  CardBody,
  CardFooter,
  Heading,
  Text,
  Button,
  Stack,
  Image,
  Divider,
  ButtonGroup,
} from '@chakra-ui/react'

function CardDemo() {
  return (
    <Stack spacing={6}>
      {/* Basic card */}
      <Card>
        <CardBody>
          <Text>View a summary of all your customers over the last month.</Text>
        </CardBody>
      </Card>

      {/* Card with header and footer */}
      <Card>
        <CardHeader>
          <Heading size="md">Customer Reports</Heading>
        </CardHeader>
        <CardBody>
          <Text>
            View a summary of all your customers over the last month.
          </Text>
        </CardBody>
        <CardFooter>
          <Button colorScheme="blue">View here</Button>
        </CardFooter>
      </Card>

      {/* Card with image */}
      <Card maxW="sm">
        <CardBody>
          <Image
            src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc"
            alt="Green double couch"
            borderRadius="lg"
          />
          <Stack mt={6} spacing={3}>
            <Heading size="md">Living room Sofa</Heading>
            <Text>
              This sofa is perfect for modern tropical spaces, baroque inspired
              spaces, and everything in between.
            </Text>
            <Text color="blue.600" fontSize="2xl">
              $450
            </Text>
          </Stack>
        </CardBody>
        <Divider />
        <CardFooter>
          <ButtonGroup spacing={2}>
            <Button variant="solid" colorScheme="blue">
              Buy now
            </Button>
            <Button variant="ghost" colorScheme="blue">
              Add to cart
            </Button>
          </ButtonGroup>
        </CardFooter>
      </Card>

      {/* Horizontal card */}
      <Card
        direction={{ base: 'column', sm: 'row' }}
        overflow="hidden"
        variant="outline"
      >
        <Image
          objectFit="cover"
          maxW={{ base: '100%', sm: '200px' }}
          src="https://images.unsplash.com/photo-1667489022797-ab608913feeb"
          alt="Caffe Latte"
        />

        <Stack>
          <CardBody>
            <Heading size="md">The perfect latte</Heading>
            <Text py={2}>
              Caffè latte is a coffee beverage of Italian origin made with
              espresso and steamed milk.
            </Text>
          </CardBody>
          <CardFooter>
            <Button variant="solid" colorScheme="blue">
              Buy Latte
            </Button>
          </CardFooter>
        </Stack>
      </Card>
    </Stack>
  )
}

Tabs

Code
TypeScript
import {
  Tabs,
  TabList,
  TabPanels,
  Tab,
  TabPanel,
  TabIndicator,
} from '@chakra-ui/react'

function TabsDemo() {
  return (
    <Stack spacing={8}>
      {/* Basic tabs */}
      <Tabs>
        <TabList>
          <Tab>One</Tab>
          <Tab>Two</Tab>
          <Tab>Three</Tab>
        </TabList>

        <TabPanels>
          <TabPanel>
            <p>Content for tab one</p>
          </TabPanel>
          <TabPanel>
            <p>Content for tab two</p>
          </TabPanel>
          <TabPanel>
            <p>Content for tab three</p>
          </TabPanel>
        </TabPanels>
      </Tabs>

      {/* Colored tabs */}
      <Tabs variant="soft-rounded" colorScheme="green">
        <TabList>
          <Tab>Tab 1</Tab>
          <Tab>Tab 2</Tab>
        </TabList>
        <TabPanels>
          <TabPanel>Content 1</TabPanel>
          <TabPanel>Content 2</TabPanel>
        </TabPanels>
      </Tabs>

      {/* Enclosed tabs */}
      <Tabs variant="enclosed">
        <TabList>
          <Tab>Tab 1</Tab>
          <Tab>Tab 2</Tab>
        </TabList>
        <TabPanels>
          <TabPanel>Content 1</TabPanel>
          <TabPanel>Content 2</TabPanel>
        </TabPanels>
      </Tabs>

      {/* With custom indicator */}
      <Tabs position="relative" variant="unstyled">
        <TabList>
          <Tab>One</Tab>
          <Tab>Two</Tab>
          <Tab>Three</Tab>
        </TabList>
        <TabIndicator mt="-1.5px" height="2px" bg="blue.500" borderRadius="1px" />
        <TabPanels>
          <TabPanel>Content 1</TabPanel>
          <TabPanel>Content 2</TabPanel>
          <TabPanel>Content 3</TabPanel>
        </TabPanels>
      </Tabs>
    </Stack>
  )
}

Dark mode

Setup

TStheme.ts
TypeScript
// theme.ts
import { extendTheme, type ThemeConfig } from '@chakra-ui/react'

const config: ThemeConfig = {
  initialColorMode: 'light',
  useSystemColorMode: false, // or true for automatic detection
}

const theme = extendTheme({ config })

export default theme

Colour mode toggle

Code
TypeScript
import { Button, useColorMode, useColorModeValue, Box, IconButton } from '@chakra-ui/react'
import { MoonIcon, SunIcon } from '@chakra-ui/icons'

function ColorModeToggle() {
  const { colorMode, toggleColorMode } = useColorMode()

  return (
    <Button onClick={toggleColorMode}>
      Toggle {colorMode === 'light' ? 'Dark' : 'Light'}
    </Button>
  )
}

// Icon button version
function ColorModeIconButton() {
  const { colorMode, toggleColorMode } = useColorMode()

  return (
    <IconButton
      aria-label="Toggle color mode"
      icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
      onClick={toggleColorMode}
    />
  )
}

// Using useColorModeValue for dynamic values
function DynamicColorBox() {
  const bgColor = useColorModeValue('white', 'gray.800')
  const textColor = useColorModeValue('gray.800', 'white')
  const borderColor = useColorModeValue('gray.200', 'gray.600')

  return (
    <Box
      bg={bgColor}
      color={textColor}
      borderWidth="1px"
      borderColor={borderColor}
      p={4}
      borderRadius="md"
    >
      This box adapts to color mode
    </Box>
  )
}

// Alternative: using _light and _dark props
function AlternativeSyntax() {
  return (
    <Box
      bg="white"
      _dark={{ bg: 'gray.800' }}
      color="gray.800"
      _dark={{ color: 'white' }}
      p={4}
    >
      Using _dark pseudo prop
    </Box>
  )
}

Theming

Custom theme

Code
TypeScript
import { extendTheme } from '@chakra-ui/react'

const theme = extendTheme({
  // Colors
  colors: {
    brand: {
      50: '#f5f0ff',
      100: '#ede5ff',
      200: '#d4c4ff',
      300: '#b69eff',
      400: '#9a7aff',
      500: '#805ad5', // main brand color
      600: '#6b46c1',
      700: '#553c9a',
      800: '#44337a',
      900: '#322659',
    },
  },

  // Fonts
  fonts: {
    heading: `'Inter', sans-serif`,
    body: `'Inter', sans-serif`,
  },

  // Font sizes
  fontSizes: {
    xs: '0.75rem',
    sm: '0.875rem',
    md: '1rem',
    lg: '1.125rem',
    xl: '1.25rem',
    '2xl': '1.5rem',
    '3xl': '1.875rem',
    '4xl': '2.25rem',
  },

  // Breakpoints
  breakpoints: {
    sm: '30em',
    md: '48em',
    lg: '62em',
    xl: '80em',
    '2xl': '96em',
  },

  // Spacing
  space: {
    px: '1px',
    0.5: '0.125rem',
    1: '0.25rem',
    // ... etc
  },

  // Border radius
  radii: {
    none: '0',
    sm: '0.125rem',
    base: '0.25rem',
    md: '0.375rem',
    lg: '0.5rem',
    xl: '0.75rem',
    '2xl': '1rem',
    full: '9999px',
  },

  // Component styles
  components: {
    Button: {
      // Base styles for all buttons
      baseStyle: {
        fontWeight: 'semibold',
        borderRadius: 'md',
      },
      // Size variants
      sizes: {
        sm: {
          fontSize: 'sm',
          px: 4,
          py: 2,
        },
        md: {
          fontSize: 'md',
          px: 6,
          py: 3,
        },
      },
      // Visual variants
      variants: {
        primary: {
          bg: 'brand.500',
          color: 'white',
          _hover: { bg: 'brand.600' },
        },
        secondary: {
          bg: 'gray.100',
          color: 'gray.800',
          _hover: { bg: 'gray.200' },
        },
      },
      // Default props
      defaultProps: {
        size: 'md',
        variant: 'primary',
      },
    },

    Input: {
      baseStyle: {
        field: {
          borderRadius: 'md',
        },
      },
      variants: {
        filled: {
          field: {
            bg: 'gray.100',
            _hover: { bg: 'gray.200' },
            _focus: { bg: 'white', borderColor: 'brand.500' },
          },
        },
      },
      defaultProps: {
        variant: 'filled',
      },
    },

    Card: {
      baseStyle: {
        container: {
          borderRadius: 'lg',
          boxShadow: 'md',
        },
      },
    },
  },

  // Global styles
  styles: {
    global: {
      body: {
        bg: 'gray.50',
        color: 'gray.800',
      },
      a: {
        color: 'brand.500',
        _hover: { textDecoration: 'underline' },
      },
    },
  },
})

export default theme

Using custom theme

Code
TypeScript
import { ChakraProvider } from '@chakra-ui/react'
import theme from './theme'

function App() {
  return (
    <ChakraProvider theme={theme}>
      <Button variant="primary">Primary Button</Button>
      <Button variant="secondary">Secondary Button</Button>
    </ChakraProvider>
  )
}

Hooks

Code
TypeScript
import {
  useDisclosure,
  useClipboard,
  useMediaQuery,
  useBreakpointValue,
  useBoolean,
  useCounter,
  useOutsideClick,
  useControllableState,
} from '@chakra-ui/react'

// useDisclosure - for modals, drawers, etc.
function DisclosureDemo() {
  const { isOpen, onOpen, onClose, onToggle } = useDisclosure()

  return (
    <>
      <Button onClick={onOpen}>Open</Button>
      <Modal isOpen={isOpen} onClose={onClose}>...</Modal>
    </>
  )
}

// useClipboard
function ClipboardDemo() {
  const { hasCopied, onCopy } = useClipboard('Hello, World!')

  return (
    <Button onClick={onCopy}>
      {hasCopied ? 'Copied!' : 'Copy'}
    </Button>
  )
}

// useMediaQuery
function MediaQueryDemo() {
  const [isLargerThan768] = useMediaQuery('(min-width: 768px)')

  return (
    <Text>{isLargerThan768 ? 'Desktop' : 'Mobile'}</Text>
  )
}

// useBreakpointValue
function BreakpointValueDemo() {
  const buttonSize = useBreakpointValue({ base: 'sm', md: 'md', lg: 'lg' })

  return <Button size={buttonSize}>Responsive Button</Button>
}

// useBoolean
function BooleanDemo() {
  const [flag, setFlag] = useBoolean()

  return (
    <>
      <Text>{flag ? 'True' : 'False'}</Text>
      <Button onClick={setFlag.toggle}>Toggle</Button>
      <Button onClick={setFlag.on}>Set True</Button>
      <Button onClick={setFlag.off}>Set False</Button>
    </>
  )
}

// useCounter
function CounterDemo() {
  const { value, increment, decrement, reset } = useCounter({
    defaultValue: 0,
    min: 0,
    max: 10,
  })

  return (
    <>
      <Text>{value}</Text>
      <Button onClick={() => increment()}>+</Button>
      <Button onClick={() => decrement()}>-</Button>
      <Button onClick={reset}>Reset</Button>
    </>
  )
}

// useOutsideClick
function OutsideClickDemo() {
  const ref = useRef<HTMLDivElement>(null)
  const [isOpen, setIsOpen] = useState(false)

  useOutsideClick({
    ref,
    handler: () => setIsOpen(false),
  })

  return (
    <>
      <Button onClick={() => setIsOpen(true)}>Open Menu</Button>
      {isOpen && (
        <Box ref={ref} bg="white" p={4} shadow="md">
          Click outside to close
        </Box>
      )}
    </>
  )
}

Form integration

React Hook Form with Chakra UI

Code
TypeScript
import { useForm } from 'react-hook-form'
import {
  FormControl,
  FormLabel,
  FormErrorMessage,
  Input,
  Button,
  VStack,
} from '@chakra-ui/react'

interface FormData {
  email: string
  password: string
}

function HookFormDemo() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>()

  const onSubmit = async (data: FormData) => {
    await new Promise((resolve) => setTimeout(resolve, 2000))
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <VStack spacing={4} align="stretch">
        <FormControl isInvalid={!!errors.email}>
          <FormLabel>Email</FormLabel>
          <Input
            {...register('email', {
              required: 'Email is required',
              pattern: {
                value: /^\S+@\S+$/i,
                message: 'Invalid email address',
              },
            })}
            placeholder="you@example.com"
          />
          <FormErrorMessage>{errors.email?.message}</FormErrorMessage>
        </FormControl>

        <FormControl isInvalid={!!errors.password}>
          <FormLabel>Password</FormLabel>
          <Input
            type="password"
            {...register('password', {
              required: 'Password is required',
              minLength: {
                value: 8,
                message: 'Password must be at least 8 characters',
              },
            })}
            placeholder="Enter password"
          />
          <FormErrorMessage>{errors.password?.message}</FormErrorMessage>
        </FormControl>

        <Button
          type="submit"
          colorScheme="blue"
          isLoading={isSubmitting}
          loadingText="Submitting"
        >
          Submit
        </Button>
      </VStack>
    </form>
  )
}

FAQ - frequently asked questions

How does Chakra UI compare to Tailwind CSS?

Chakra uses style props (props on components), while Tailwind uses utility classes in className. Chakra ships with built-in components that include accessibility out of the box, whereas Tailwind requires manual implementation. Chakra is a better fit for React-specific projects, while Tailwind works well for any kind of project.

Is Chakra UI slower than Tailwind?

Chakra generates styles at runtime through Emotion, which is theoretically slower than Tailwind's static stylesheet. In practice the difference is unmeasurable for most applications. It surfaces on lists of hundreds of elements, where every row recomputes its own styles.

How can I reduce bundle size?

Import only what you use, since unused components drop out at build time anyway. The biggest win, though, comes from removing dependencies after migrating to version 3, because framer-motion and @emotion/styled are no longer required.

Does Chakra work with the Next.js App Router?

Yes, but the provider has to be a client component. Server components can render Chakra elements, while anything responding to interaction requires crossing the client boundary.

How do I style third party components?

Wrap them in Box and put style props on the wrapper, or use the chakra() factory, which applies the styling system to any component. In version 3 the cleanest option is often the asChild prop, which passes styles down instead of adding another element to the tree.

Performance and size, worth knowing up front

This library has a real cost and it is fair to name it rather than settle for an assurance that everything is fast.

Styles are produced at runtime. Emotion creates classes on the fly and injects them into the document, so the first render does work that a static stylesheet never has to. On a typical screen with a few dozen elements that is imperceptible. On a table of a thousand rows, where every cell carries its own style props, it becomes noticeable and shows up as scroll latency.

The practical advice is simple: in places with many repeated elements, move styles out of props and into a single class or a variant defined in the theme. A variant is computed once and reused by every instance, whereas props supplied inline are computed for each one separately.

The second matter is server rendering. Runtime generated styles have to be collected during server rendering and sent with the document, otherwise the user briefly sees unstyled content. The mechanism is built in but needs the provider set up correctly, and this is the most common cause of layout flashing on a first visit.

The third is size. The library with its theming system weighs noticeably more than a set of utility classes. For an application a user opens once and keeps open, that does not matter. For a page reached by incidental search traffic it matters a great deal, and it is a reason to pick something lighter.

Common mistakes

The first is mixing examples from both versions. Version 2 code looks nearly identical yet does not work, because prop names changed. Before hunting for a bug in your own code, check which version the snippet you pasted came from.

The second is overriding styles through the plain style attribute. Inline styles beat classes, so mixing the two approaches yields code where some rules apply and others are silently ignored. If you need a value computed at runtime, pass it as a Chakra style prop rather than an HTML attribute.

The third concerns the theme. Overriding the spacing scale or colours to match an existing visual design is tempting. If you are overriding more than a dozen or so values, that is a signal you picked the wrong library, because you are fighting its assumptions instead of using them.

The fourth is forgetting the provider in tests. Components rendered without it fail on the missing theme, and the error message does not point at the cause directly. A small helper wrapping renders in the provider saves considerable time when writing tests.

The fifth is treating accessibility as finished. Composite components do handle the keyboard, but focus order in your layout, form field labelling, and colour contrast in your theme remain your responsibility. The library removes part of the work, not all of it.

The full list of changes between versions is in the Chakra UI migration guide, and current package versions live in the project repository.