Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik23 min czytania

Chakra UI, dostępne komponenty React w wersji 3

Chakra UI to dostępna biblioteka komponentów React. Wersja 3, migracja z dwójki, stylowanie przez props, motywy i porównanie z Mantine.

Chakra UI, dostępne komponenty React w wersji 3

Chakra UI to biblioteka komponentów React, w której style podaje się jako właściwości komponentu, a dostępność jest wbudowana zamiast dokładana później. Aktualna wersja to 3.36.1 na licencji MIT, zbudowana na bazie Ark UI. Trójka to przepisanie biblioteki od podstaw, więc większość materiałów krążących w sieci opisuje interfejs, który już nie istnieje.

Czym Chakra UI różni się od reszty

Podejście do stylowania jest tu główną cechą wyróżniającą i zarazem tym, co dzieli ludzi na dwa obozy. Zamiast pisać arkusz stylów albo listę klas narzędziowych, przekazujesz style jako właściwości: padding, bg, fontSize. Wartości pochodzą z motywu, więc padding="4" odwołuje się do skali odstępów, a nie do konkretnej liczby pikseli.

Zaleta jest oczywista przy pisaniu: nie przełączasz kontekstu między plikiem komponentu a plikiem stylów, a podpowiadanie w edytorze podsuwa dostępne wartości z motywu. Wada ujawnia się przy czytaniu cudzego kodu, bo komponent z piętnastoma właściwościami stylu robi się gęsty, a warunkowe style zamieniają się w wyrażenia wewnątrz JSX.

Druga cecha to dostępność. Komponenty złożone, czyli okna dialogowe, listy rozwijane, zakładki i menu, obsługują klawiaturę, zarządzają fokusem i mają poprawne atrybuty ról. To praca, której nikt nie lubi wykonywać ręcznie, a która decyduje o tym, czy interfejs da się obsłużyć bez myszy. W wersji trzeciej tę warstwę przejęło Ark UI, biblioteka zachowań bez własnych stylów, tworzona przez ten sam zespół.

Trzecia rzecz to skala biblioteki. To nie jest zestaw klocków do skopiowania, tylko zależność z własnym systemem motywów, generatorem stylów i konwencjami. Instalujesz ją raz i budujesz na niej całą aplikację, w odróżnieniu od shadcn/ui, gdzie kod komponentu trafia do Twojego repozytorium i przestaje być zależnością.

Wersja 3, czyli co się zmieniło i dlaczego to boli

To najważniejsza sekcja tego tekstu, bo dotyka najczęstszego problemu: przykład znaleziony w sieci nie działa, a komunikat błędu nie tłumaczy dlaczego. Trójka zmieniła nazwy pakietów, komponentów i właściwości naraz.

Zależności są krótsze. Pakiety @emotion/styled i framer-motion nie są już potrzebne, a @chakra-ui/icons został usunięty na rzecz zewnętrznych zestawów ikon. Zniknął też @chakra-ui/next-js, bo do stylowania komponentów Next.js służy teraz właściwość asChild.

Konfiguracja motywu działa inaczej. W miejsce extendTheme weszło createSystem z defaultConfig, a wartości tokenów wymagają opakowania w obiekt z polem value. Dostawca przyjmuje teraz value zamiast theme.

Tryb ciemny wyprowadzono na zewnątrz. useColorMode i ColorModeProvider zniknęły, a ich rolę przejęła biblioteka next-themes, co w praktyce oznacza, że przełącznik motywu piszesz raz i działa on tak samo w Chakrze i poza nią.

Komponenty złożone dostały nowe nazwy i strukturę. Modal to teraz Dialog.Root, Divider to Separator, Collapse to Collapsible.Root, Stepper to Steps.Root, a CircularProgress to ProgressCircle. Właściwości logiczne straciły przedrostek: isOpen stało się open, isDisabled to disabled, isInvalid to invalid. Zmieniły się też nazwy stylistyczne, na przykład colorScheme na colorPalette i noOfLines na lineClamp.

Dobra wiadomość jest taka, że większość tych zmian da się przeprowadzić automatycznie poleceniem npx @chakra-ui/codemod upgrade. Narzędzie zmienia nazwy komponentów, poprawia właściwości i importy, a także przebudowuje komponenty złożone, więc okno dialogowe i stepper również dostają nową strukturę, każdy przez własną regułę przekształcenia. Ręcznie zostaje reszta: usunięcie zbędnych pakietów, przepisanie motywu z extendTheme na createSystem oraz podmiana dostawcy na ten z gotowych fragmentów. Przy dużej aplikacji planuj to jako osobne zadanie, a nie jako punkt przy okazji innej pracy, bo wynik przekształcenia i tak trzeba przejrzeć. Wymagany jest przy tym Node w wersji 20 lub nowszej.

Przykłady kodu w dalszej części tego artykułu pochodzą jeszcze z wersji drugiej. Zostawiam je, bo pojęcia i wzorce pozostały te same, ale nazwy właściwości i komponentów sprawdzaj w bieżącej dokumentacji, zanim je przekleisz.

Chakra UI a alternatywy

CechaChakra UIMantineshadcn/uiTailwind CSS
Sposób stylowaniawłaściwości komponentuklasy i moduły CSSklasy narzędzioweklasy narzędziowe
Model dystrybucjizależność w projekciezależność w projekciekopiowany kodzależność
Dostępność złożonych komponentówwbudowanawbudowanawbudowanabrak, to tylko style
Kontrola nad kodem komponentuprzez motywprzez motywpełnanie dotyczy
Aktualizacjeautomatyczneautomatyczneręczneautomatyczne
Próg wejścianiskiniskiśredniśredni

Podział przebiega tu wzdłuż jednej osi: czy chcesz, żeby komponenty aktualizowały się same, czy wolisz mieć ich kod u siebie. Chakra i Mantine należą do pierwszej grupy, więc poprawka błędu w oknie dialogowym przychodzi z aktualizacją pakietu. shadcn/ui należy do drugiej, więc masz pełną swobodę i pełną odpowiedzialność.

Porównywanie Chakry z samym Tailwindem jest nieporozumieniem, które regularnie wraca w dyskusjach. Tailwind to warstwa stylów bez komponentów, więc okno dialogowe z obsługą klawiatury i pułapką fokusa musisz napisać sam albo dołożyć osobną bibliotekę zachowań. To nie są konkurenci, tylko różne poziomy stosu.

Kiedy Chakra jest złym wyborem

Uczciwie trzeba wymienić sytuacje, w których lepiej sięgnąć po coś innego. Pierwsza to strona wizerunkowa albo landing, gdzie liczy się rozmiar pobieranych plików. Biblioteka z systemem motywów i generatorem stylów dokłada tam znacznie więcej, niż daje.

Druga to projekt z gotowym systemem projektowym o nietypowych zasadach. Chakra zakłada określony sposób myślenia o skalach odstępów, kolorów i typografii, a wpychanie do niej systemu zbudowanego wokół innych założeń kończy się nadpisywaniem większości domyślnych wartości. W takim wypadku warstwa bez własnych stylów, czyli Ark UI albo Radix UI, daje ten sam poziom dostępności bez narzuconej estetyki.

Trzecia to zespół już pracujący na klasach narzędziowych. Mieszanie dwóch modeli stylowania w jednej aplikacji generuje spory o konwencje i kod, w którym połowa komponentów wygląda inaczej niż druga.

Jest jeszcze przypadek czwarty, rzadziej wymieniany, a w praktyce najbardziej dotkliwy. Chodzi o projekt, który musi przetrwać kilka lat bez większych przepisań. Trójka pokazała, że ta biblioteka potrafi zmienić nazwy komponentów i właściwości w jednym wydaniu, a narzędzie automatyzujące migrację nie pokrywa wszystkiego. Jeśli aplikacja ma żyć długo, a zespół nie ma budżetu na okresowe migracje, warstwa bez własnych stylów albo kopiowany kod komponentu starzeją się spokojniej, bo nikt Ci ich nie przemianuje zdalnie.

Instalacja i konfiguracja

Instalacja

Wersja trzecia wymaga dwóch pakietów zamiast czterech. Jeśli w Twoim projekcie wiszą jeszcze @emotion/styled i framer-motion wyłącznie na potrzeby Chakry, po migracji można je usunąć.

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

Gotowe fragmenty, w tym dostawcę i przełącznik motywu, dokłada narzędzie wiersza poleceń. Kopiuje ono kod do Twojego repozytorium, więc możesz go swobodnie zmieniać.

Code
Bash
npx @chakra-ui/cli snippet add

Provider setup

W wersji trzeciej dostawca składa ChakraProvider odpowiedzialny za style oraz ThemeProvider z next-themes odpowiedzialny za tryb kolorystyczny.

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

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

Poniższy fragment pokazuje starszy układ z wersji drugiej, oparty o extendTheme. Zostawiam go jako punkt odniesienia przy migracji, bo w wielu projektach nadal tak to wygląda.

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>
  )
}

Dla 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 - serce Chakra UI

Podstawy Style Props

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>
    </>
  )
}

Responsywne style

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>
    </>
  )
}

Komponenty

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

Konfiguracja

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

const config: ThemeConfig = {
  initialColorMode: 'light',
  useSystemColorMode: false, // lub true dla automatycznego
}

const theme = extendTheme({ config })

export default theme

Przełącznik trybu

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

Własny motyw

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

Użycie własnego motywu

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>
      )}
    </>
  )
}

Integracja z formularzami

React Hook Form z 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 - Najczęściej zadawane pytania

Jak Chakra UI wypada w porównaniu do Tailwind CSS?

Chakra używa style props (props na komponentach), Tailwind używa utility classes w className. Chakra ma wbudowane komponenty z dostępnością, Tailwind wymaga ręcznej implementacji. Chakra jest lepsze dla projektów React, Tailwind dla dowolnych projektów.

Czy Chakra UI jest wolniejszy od Tailwind?

Chakra generuje style w czasie działania aplikacji przez Emotion, co teoretycznie jest wolniejsze niż statyczny arkusz Tailwinda. W praktyce dla większości aplikacji różnica jest niemierzalna. Widać ją dopiero na listach o setkach elementów, gdzie każdy wiersz przelicza własne style.

Jak zmniejszyć rozmiar paczki?

Importuj wyłącznie to, czego używasz, bo nieużywane komponenty i tak wypadną przy budowaniu. Największy zysk daje jednak rezygnacja z niepotrzebnych zależności po migracji do wersji trzeciej, bo framer-motion i @emotion/styled przestały być wymagane.

Czy Chakra działa z App Routerem w Next.js?

Tak, ale dostawca musi być komponentem klienckim. Komponenty serwerowe mogą renderować elementy Chakry, natomiast wszystko, co reaguje na interakcję, wymaga przekroczenia granicy klienta.

Jak stylować komponenty zewnętrzne?

Owiń je w Box i podaj właściwości stylu na opakowaniu, albo skorzystaj z fabryki chakra(), która nakłada system stylów na dowolny komponent. W wersji trzeciej najczystszym rozwiązaniem bywa właściwość asChild, która przekazuje style w dół zamiast dokładać kolejny element do drzewa.

Wydajność i rozmiar, czyli o czym warto wiedzieć wcześniej

Ta biblioteka ma realny koszt i uczciwie jest go nazwać, zamiast poprzestać na zapewnieniu, że wszystko jest szybkie.

Style powstają w czasie działania aplikacji. Emotion tworzy klasy w locie i wstrzykuje je do dokumentu, więc pierwsze renderowanie wymaga pracy, której przy statycznym arkuszu nie ma. Na typowym ekranie z kilkudziesięcioma elementami jest to nieodczuwalne. Na tabeli z tysiącem wierszy, gdzie każda komórka niesie własne właściwości stylu, robi się zauważalne i objawia się opóźnieniem przy przewijaniu.

Praktyczna rada jest prosta: w miejscach o dużej liczbie powtarzalnych elementów przenieś style z właściwości do jednej klasy albo do stylu wariantowego zdefiniowanego w motywie. Wariant liczy się raz i jest wykorzystywany przez wszystkie instancje, podczas gdy właściwości podane bezpośrednio przeliczają się dla każdej z osobna.

Druga sprawa to renderowanie po stronie serwera. Style generowane w czasie działania trzeba przy renderowaniu serwerowym zebrać i wysłać razem z dokumentem, inaczej użytkownik zobaczy na moment nieostylowaną treść. Rozwiązanie jest wbudowane, ale wymaga poprawnego ustawienia dostawcy i jest to najczęstsza przyczyna migotania układu przy pierwszym wejściu na stronę.

Trzecia to rozmiar. Sama biblioteka z systemem motywów waży wyraźnie więcej niż zestaw klas narzędziowych. Przy aplikacji, którą użytkownik otwiera raz i trzyma otwartą, nie ma to znaczenia. Przy stronie odwiedzanej przez przypadkowy ruch z wyszukiwarki ma znaczenie zasadnicze i jest to argument, żeby wybrać coś lżejszego.

Typowe błędy

Pierwszy to mieszanie przykładów z obu wersji. Kod z wersji drugiej wygląda niemal tak samo, a jednak nie działa, bo zmieniły się nazwy właściwości. Zanim zaczniesz szukać błędu w swoim kodzie, sprawdź, z której wersji pochodzi wzór, który przekleiłeś.

Drugi to nadpisywanie stylów przez zwykły atrybut style. Style wbudowane wygrywają z klasami, więc mieszanie obu podejść daje kod, w którym część reguł działa, a część jest cicho ignorowana. Jeśli musisz podać wartość wyliczaną w czasie działania, podaj ją jako właściwość stylu Chakry, a nie jako atrybut HTML.

Trzeci dotyczy motywu. Kuszące jest nadpisanie skali odstępów albo kolorów tak, żeby pasowała do istniejącego projektu graficznego. Jeśli nadpisujesz więcej niż kilkanaście wartości, jest to sygnał, że wybrałeś niewłaściwą bibliotekę, bo walczysz z jej założeniami zamiast z nich korzystać.

Czwarty to zapominanie o dostawcy w testach. Komponenty renderowane bez niego wywalają się na braku motywu, a komunikat błędu nie wskazuje wprost przyczyny. Własna funkcja pomocnicza opakowująca renderowanie w dostawcę oszczędza sporo czasu przy pisaniu testów.

Piąty to traktowanie dostępności jako gotowej. Komponenty złożone rzeczywiście obsługują klawiaturę, ale kolejność fokusa w Twoim układzie, opisy pól formularza i kontrast kolorów w Twoim motywie to nadal Twoja odpowiedzialność. Biblioteka zdejmuje część pracy, nie całą. Najprostszy test, który to weryfikuje, zajmuje minutę: odłóż mysz i przejdź przez formularz samym klawiszem tabulacji, sprawdzając, czy widać, na którym elemencie stoi fokus, i czy da się zamknąć okno dialogowe klawiszem ucieczki.

Pełną listę zmian między wersjami opisuje przewodnik migracji Chakra UI, a bieżące wersje pakietów znajdziesz w repozytorium projektu.