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
| Feature | Chakra UI | Mantine | shadcn/ui | Tailwind CSS |
|---|---|---|---|---|
| Styling method | component props | classes and CSS modules | utility classes | utility classes |
| Distribution model | project dependency | project dependency | copied code | dependency |
| Composite component accessibility | built in | built in | built in | none, styles only |
| Control over component code | through the theme | through the theme | complete | not applicable |
| Updates | automatic | automatic | manual | automatic |
| Entry barrier | low | low | medium | medium |
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.
npm i @chakra-ui/react @emotion/reactReady 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.
npx @chakra-ui/cli snippet addProvider setup
In version 3 the provider composes ChakraProvider for styling and ThemeProvider from next-themes for colour mode.
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.
// 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>
}// 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)
// 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
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
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
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
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
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
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
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
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
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
// 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 themeColour mode toggle
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
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 themeUsing custom theme
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
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
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.