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

HeroUI - Kompletny Przewodnik po Pięknych Komponentach React

HeroUI (dawniej NextUI) to nowoczesna biblioteka React z pięknymi animacjami, smooth transitions i pełną dostępnością. Zbudowana na Tailwind CSS i React Aria.

HeroUI - Kompletny Przewodnik po Pięknych Komponentach React

Czym jest HeroUI?

HeroUI (dawniej znany jako NextUI) to nowoczesna biblioteka komponentów React stworzona z myślą o pięknym designie, płynnych animacjach i doskonałej dostępności. Zbudowana na solidnych fundamentach Tailwind CSS i React Aria, HeroUI oferuje zestaw gotowych do użycia komponentów, które wyglądają profesjonalnie "out of the box", ale są w pełni customizable.

Rebranding z NextUI na HeroUI nastąpił w 2024 roku, aby uniknąć konfuzji z Next.js i lepiej oddać misję biblioteki - tworzenie "heroicznych" interfejsów użytkownika. Pod spodem to ta sama świetna biblioteka z ciągłym rozwojem i wsparciem społeczności.

Dlaczego HeroUI?

  1. Piękny design - Przemyślane style, gradienty i animacje
  2. Framer Motion - Wszystkie animacje oparte na Framer Motion
  3. React Aria - Pełna dostępność WAI-ARIA pod spodem
  4. Tailwind CSS - Natywne wsparcie i łatwa customizacja
  5. Dark Mode - Wbudowany system motywów
  6. TypeScript - Pełne typy dla wszystkich komponentów

HeroUI vs Inne Biblioteki

CechaHeroUIChakra UIshadcn/uiMaterial UI
StylNowoczesny, fluidMinimalistycznyCopy-pasteMaterial Design
AnimacjeFramer MotionCSSTailwindCSS-in-JS
CustomizacjaTailwindPropsFull controlTheme override
Rozmiar~120KB~300KB~0KB*~500KB
A11yReact AriaBuilt-inRadixBuilt-in
Dark ModeWbudowanyWbudowanyManualWbudowany
TypeScriptPełnePełnePełnePełne

*shadcn/ui to copy-paste, nie instalujesz biblioteki

Kiedy wybrać HeroUI?

Wybierz HeroUI, gdy:

  • Chcesz piękny design bez dużo pracy
  • Potrzebujesz płynnych animacji
  • Używasz Tailwind CSS
  • Zależy Ci na dostępności
  • Budujesz aplikację Next.js lub React

Rozważ alternatywy, gdy:

  • Potrzebujesz Material Design (Material UI)
  • Chcesz pełnej kontroli bez gotowych stylów (shadcn/ui)
  • Budujesz dla korporacji (Ant Design)

Instalacja i Konfiguracja

Instalacja pakietów

Code
Bash
# npm
npm install @heroui/react framer-motion

# yarn
yarn add @heroui/react framer-motion

# pnpm
pnpm add @heroui/react framer-motion

Konfiguracja Tailwind CSS

JStailwind.config.js
JavaScript
// tailwind.config.js
const { heroui } = require("@heroui/react")

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
    './node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
  ],
  theme: {
    extend: {},
  },
  darkMode: "class",
  plugins: [heroui()]
}

Provider Setup

TSapp/providers.tsx
TypeScript
// app/providers.tsx
'use client'

import { HeroUIProvider } from '@heroui/react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <HeroUIProvider>
      <NextThemesProvider attribute="class" defaultTheme="dark">
        {children}
      </NextThemesProvider>
    </HeroUIProvider>
  )
}

// app/layout.tsx
import { Providers } from './providers'

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

Individual Package Installation

Dla mniejszego bundle size możesz instalować tylko potrzebne komponenty:

Code
Bash
# Instaluj tylko Button i Card
npm install @heroui/button @heroui/card

# Lub pojedynczo
npm install @heroui/modal
npm install @heroui/input
npm install @heroui/navbar
Code
TypeScript
// Import z pojedynczych pakietów
import { Button } from '@heroui/button'
import { Card, CardBody } from '@heroui/card'
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@heroui/modal'

Komponenty UI

Button

Code
TypeScript
import { Button } from '@heroui/react'

function ButtonExamples() {
  return (
    <div className="flex flex-wrap gap-4">
      {/* Kolory */}
      <Button color="default">Default</Button>
      <Button color="primary">Primary</Button>
      <Button color="secondary">Secondary</Button>
      <Button color="success">Success</Button>
      <Button color="warning">Warning</Button>
      <Button color="danger">Danger</Button>

      {/* Warianty */}
      <Button variant="solid">Solid</Button>
      <Button variant="bordered">Bordered</Button>
      <Button variant="light">Light</Button>
      <Button variant="flat">Flat</Button>
      <Button variant="faded">Faded</Button>
      <Button variant="shadow">Shadow</Button>
      <Button variant="ghost">Ghost</Button>

      {/* Rozmiary */}
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>

      {/* Loading */}
      <Button isLoading>Loading...</Button>
      <Button isLoading spinner={<CustomSpinner />}>
        Custom Spinner
      </Button>

      {/* Disabled */}
      <Button isDisabled>Disabled</Button>

      {/* Z ikonami */}
      <Button startContent={<PlusIcon />}>Add Item</Button>
      <Button endContent={<ArrowRightIcon />}>Continue</Button>

      {/* Icon Only */}
      <Button isIconOnly aria-label="Settings">
        <SettingsIcon />
      </Button>

      {/* Gradient */}
      <Button
        className="bg-gradient-to-tr from-pink-500 to-yellow-500 text-white shadow-lg"
      >
        Gradient Button
      </Button>
    </div>
  )
}

Button Group

Code
TypeScript
import { Button, ButtonGroup } from '@heroui/react'

function ButtonGroupExample() {
  return (
    <ButtonGroup>
      <Button>One</Button>
      <Button>Two</Button>
      <Button>Three</Button>
    </ButtonGroup>
  )
}

// Z różnymi wariantami
function VariantButtonGroup() {
  return (
    <div className="flex flex-col gap-4">
      <ButtonGroup variant="bordered">
        <Button>Edit</Button>
        <Button>Delete</Button>
        <Button>Archive</Button>
      </ButtonGroup>

      <ButtonGroup variant="flat" color="secondary">
        <Button>Left</Button>
        <Button>Center</Button>
        <Button>Right</Button>
      </ButtonGroup>
    </div>
  )
}

Card

Code
TypeScript
import { Card, CardHeader, CardBody, CardFooter, Image, Button, Divider, Link } from '@heroui/react'

function BasicCard() {
  return (
    <Card className="max-w-[400px]">
      <CardHeader className="flex gap-3">
        <Image
          alt="heroui logo"
          height={40}
          radius="sm"
          src="/logo.png"
          width={40}
        />
        <div className="flex flex-col">
          <p className="text-md">HeroUI</p>
          <p className="text-small text-default-500">heroui.com</p>
        </div>
      </CardHeader>
      <Divider />
      <CardBody>
        <p>Make beautiful websites regardless of your design experience.</p>
      </CardBody>
      <Divider />
      <CardFooter>
        <Link
          isExternal
          showAnchorIcon
          href="https://github.com/heroui-inc/heroui"
        >
          Visit source code on GitHub.
        </Link>
      </CardFooter>
    </Card>
  )
}

// Card z obrazkiem
function CardWithImage() {
  return (
    <Card className="py-4">
      <CardHeader className="pb-0 pt-2 px-4 flex-col items-start">
        <p className="text-tiny uppercase font-bold">Daily Mix</p>
        <small className="text-default-500">12 Tracks</small>
        <h4 className="font-bold text-large">Frontend Radio</h4>
      </CardHeader>
      <CardBody className="overflow-visible py-2">
        <Image
          alt="Card background"
          className="object-cover rounded-xl"
          src="/images/hero-card-complete.jpeg"
          width={270}
        />
      </CardBody>
    </Card>
  )
}

// Pressable Card
function PressableCard() {
  return (
    <Card
      isPressable
      onPress={() => console.log('Card pressed')}
      className="border-none bg-gradient-to-br from-violet-500 to-fuchsia-500"
    >
      <CardBody className="text-center">
        <p className="text-white text-lg font-semibold">
          Click me!
        </p>
      </CardBody>
    </Card>
  )
}

// Card z blur
function BlurCard() {
  return (
    <Card isBlurred className="border-none bg-background/60 dark:bg-default-100/50 max-w-[610px]">
      <CardBody>
        <div className="flex gap-6 items-center">
          <Image
            alt="Album cover"
            className="object-cover"
            height={200}
            shadow="md"
            src="/album-cover.png"
            width={200}
          />
          <div className="flex flex-col">
            <h3 className="text-xl font-semibold">Daily Mix</h3>
            <p className="text-default-500">12 Tracks</p>
          </div>
        </div>
      </CardBody>
    </Card>
  )
}

Input

Code
TypeScript
import { Input } from '@heroui/react'
import { EyeIcon, EyeOffIcon, SearchIcon, MailIcon } from './icons'
import { useState } from 'react'

function InputExamples() {
  const [value, setValue] = useState('')
  const [isVisible, setIsVisible] = useState(false)

  return (
    <div className="flex flex-col gap-4 w-full max-w-md">
      {/* Podstawowy */}
      <Input
        type="email"
        label="Email"
        placeholder="Enter your email"
      />

      {/* Z opisem */}
      <Input
        type="email"
        label="Email"
        placeholder="you@example.com"
        description="We'll never share your email"
      />

      {/* Wymagany */}
      <Input
        isRequired
        type="email"
        label="Email"
        placeholder="Enter your email"
      />

      {/* Warianty */}
      <Input variant="flat" label="Flat" placeholder="Flat variant" />
      <Input variant="bordered" label="Bordered" placeholder="Bordered variant" />
      <Input variant="underlined" label="Underlined" placeholder="Underlined variant" />
      <Input variant="faded" label="Faded" placeholder="Faded variant" />

      {/* Rozmiary */}
      <Input size="sm" label="Small" />
      <Input size="md" label="Medium" />
      <Input size="lg" label="Large" />

      {/* Z ikonami */}
      <Input
        label="Email"
        placeholder="you@example.com"
        startContent={<MailIcon className="text-default-400" />}
      />

      <Input
        label="Search"
        placeholder="Search..."
        startContent={<SearchIcon className="text-default-400" />}
        endContent={
          <kbd className="hidden lg:inline-block px-2 py-0.5 text-xs bg-default-100 rounded">
            ⌘K
          </kbd>
        }
      />

      {/* Password z toggle */}
      <Input
        label="Password"
        placeholder="Enter password"
        type={isVisible ? "text" : "password"}
        endContent={
          <button
            type="button"
            onClick={() => setIsVisible(!isVisible)}
          >
            {isVisible ? <EyeOffIcon /> : <EyeIcon />}
          </button>
        }
      />

      {/* Controlled */}
      <Input
        label="Controlled"
        value={value}
        onValueChange={setValue}
      />
      <p className="text-sm text-default-500">Value: {value}</p>

      {/* Error state */}
      <Input
        isInvalid
        type="email"
        label="Email"
        defaultValue="invalid@"
        errorMessage="Please enter a valid email"
      />

      {/* Disabled */}
      <Input
        isDisabled
        label="Disabled"
        placeholder="Cannot edit"
        defaultValue="Fixed value"
      />

      {/* Read only */}
      <Input
        isReadOnly
        label="Read Only"
        defaultValue="Cannot change this"
      />
    </div>
  )
}

Textarea

Code
TypeScript
import { Textarea } from '@heroui/react'

function TextareaExamples() {
  return (
    <div className="flex flex-col gap-4 w-full max-w-md">
      <Textarea
        label="Description"
        placeholder="Enter your description"
      />

      <Textarea
        label="With min rows"
        placeholder="This has minimum 3 rows"
        minRows={3}
      />

      <Textarea
        label="With max rows"
        placeholder="This has maximum 5 rows"
        maxRows={5}
      />

      <Textarea
        isDisabled
        label="Disabled"
        defaultValue="Cannot edit this"
      />

      <Textarea
        isInvalid
        label="Invalid"
        errorMessage="This field is required"
      />
    </div>
  )
}

Select

Code
TypeScript
import { Select, SelectItem } from '@heroui/react'

const animals = [
  { key: 'cat', label: 'Cat' },
  { key: 'dog', label: 'Dog' },
  { key: 'elephant', label: 'Elephant' },
  { key: 'lion', label: 'Lion' },
  { key: 'tiger', label: 'Tiger' },
]

function SelectExamples() {
  return (
    <div className="flex flex-col gap-4 w-full max-w-md">
      {/* Podstawowy */}
      <Select label="Select an animal" placeholder="Choose...">
        {animals.map((animal) => (
          <SelectItem key={animal.key}>{animal.label}</SelectItem>
        ))}
      </Select>

      {/* Z domyślną wartością */}
      <Select
        label="Favorite Animal"
        defaultSelectedKeys={["cat"]}
      >
        {animals.map((animal) => (
          <SelectItem key={animal.key}>{animal.label}</SelectItem>
        ))}
      </Select>

      {/* Multiple */}
      <Select
        label="Select animals"
        selectionMode="multiple"
        placeholder="Select multiple..."
      >
        {animals.map((animal) => (
          <SelectItem key={animal.key}>{animal.label}</SelectItem>
        ))}
      </Select>

      {/* Z opisem */}
      <Select
        label="Animal"
        description="Select your favorite animal"
      >
        {animals.map((animal) => (
          <SelectItem key={animal.key}>{animal.label}</SelectItem>
        ))}
      </Select>

      {/* Warianty */}
      <Select variant="bordered" label="Bordered">
        {animals.map((animal) => (
          <SelectItem key={animal.key}>{animal.label}</SelectItem>
        ))}
      </Select>

      <Select variant="underlined" label="Underlined">
        {animals.map((animal) => (
          <SelectItem key={animal.key}>{animal.label}</SelectItem>
        ))}
      </Select>
    </div>
  )
}

Modal

Code
TypeScript
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, Input } from '@heroui/react'

function BasicModal() {
  const { isOpen, onOpen, onOpenChange } = useDisclosure()

  return (
    <>
      <Button onPress={onOpen}>Open Modal</Button>

      <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
        <ModalContent>
          {(onClose) => (
            <>
              <ModalHeader className="flex flex-col gap-1">
                Modal Title
              </ModalHeader>
              <ModalBody>
                <p>
                  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                  Nullam pulvinar risus non risus hendrerit venenatis.
                </p>
                <p>
                  Pellentesque sit amet sapien fringilla, mattis ligula
                  consectetur, ultrices mauris.
                </p>
              </ModalBody>
              <ModalFooter>
                <Button color="danger" variant="light" onPress={onClose}>
                  Close
                </Button>
                <Button color="primary" onPress={onClose}>
                  Action
                </Button>
              </ModalFooter>
            </>
          )}
        </ModalContent>
      </Modal>
    </>
  )
}

// Modal z formularzem
function ModalWithForm() {
  const { isOpen, onOpen, onOpenChange } = useDisclosure()

  return (
    <>
      <Button onPress={onOpen} color="primary">
        Sign Up
      </Button>

      <Modal
        isOpen={isOpen}
        onOpenChange={onOpenChange}
        placement="top-center"
      >
        <ModalContent>
          {(onClose) => (
            <>
              <ModalHeader className="flex flex-col gap-1">
                Create Account
              </ModalHeader>
              <ModalBody>
                <Input
                  autoFocus
                  label="Email"
                  placeholder="Enter your email"
                  variant="bordered"
                />
                <Input
                  label="Password"
                  placeholder="Enter your password"
                  type="password"
                  variant="bordered"
                />
                <div className="flex py-2 px-1 justify-between">
                  <Checkbox
                    classNames={{
                      label: "text-small",
                    }}
                  >
                    Remember me
                  </Checkbox>
                  <Link color="primary" href="#" size="sm">
                    Forgot password?
                  </Link>
                </div>
              </ModalBody>
              <ModalFooter>
                <Button color="danger" variant="flat" onPress={onClose}>
                  Close
                </Button>
                <Button color="primary" onPress={onClose}>
                  Sign up
                </Button>
              </ModalFooter>
            </>
          )}
        </ModalContent>
      </Modal>
    </>
  )
}

// Modal z różnymi rozmiarami
function SizedModals() {
  const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', 'full']
  const { isOpen, onOpen, onOpenChange } = useDisclosure()
  const [size, setSize] = useState('md')

  return (
    <div className="flex flex-wrap gap-3">
      {sizes.map((s) => (
        <Button
          key={s}
          onPress={() => {
            setSize(s)
            onOpen()
          }}
        >
          Open {s}
        </Button>
      ))}

      <Modal size={size} isOpen={isOpen} onOpenChange={onOpenChange}>
        <ModalContent>
          <ModalHeader>Modal {size}</ModalHeader>
          <ModalBody>
            <p>This is a {size} modal.</p>
          </ModalBody>
        </ModalContent>
      </Modal>
    </div>
  )
}

// Scrollable Modal
function ScrollableModal() {
  const { isOpen, onOpen, onOpenChange } = useDisclosure()

  return (
    <>
      <Button onPress={onOpen}>Long Content</Button>

      <Modal
        isOpen={isOpen}
        onOpenChange={onOpenChange}
        scrollBehavior="inside"
      >
        <ModalContent>
          <ModalHeader>Terms and Conditions</ModalHeader>
          <ModalBody>
            {Array.from({ length: 20 }).map((_, i) => (
              <p key={i}>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
              </p>
            ))}
          </ModalBody>
          <ModalFooter>
            <Button color="primary">Accept</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </>
  )
}

Navbar

Code
TypeScript
import {
  Navbar,
  NavbarBrand,
  NavbarContent,
  NavbarItem,
  NavbarMenuToggle,
  NavbarMenu,
  NavbarMenuItem,
  Link,
  Button
} from '@heroui/react'
import { useState } from 'react'

function BasicNavbar() {
  const [isMenuOpen, setIsMenuOpen] = useState(false)

  const menuItems = [
    "Profile",
    "Dashboard",
    "Activity",
    "Analytics",
    "System",
    "Settings",
    "Help & Feedback",
    "Log Out",
  ]

  return (
    <Navbar onMenuOpenChange={setIsMenuOpen}>
      <NavbarContent>
        <NavbarMenuToggle
          aria-label={isMenuOpen ? "Close menu" : "Open menu"}
          className="sm:hidden"
        />
        <NavbarBrand>
          <Logo />
          <p className="font-bold text-inherit">ACME</p>
        </NavbarBrand>
      </NavbarContent>

      <NavbarContent className="hidden sm:flex gap-4" justify="center">
        <NavbarItem>
          <Link color="foreground" href="#">
            Features
          </Link>
        </NavbarItem>
        <NavbarItem isActive>
          <Link href="#" aria-current="page">
            Customers
          </Link>
        </NavbarItem>
        <NavbarItem>
          <Link color="foreground" href="#">
            Integrations
          </Link>
        </NavbarItem>
      </NavbarContent>

      <NavbarContent justify="end">
        <NavbarItem className="hidden lg:flex">
          <Link href="#">Login</Link>
        </NavbarItem>
        <NavbarItem>
          <Button as={Link} color="primary" href="#" variant="flat">
            Sign Up
          </Button>
        </NavbarItem>
      </NavbarContent>

      <NavbarMenu>
        {menuItems.map((item, index) => (
          <NavbarMenuItem key={`${item}-${index}`}>
            <Link
              color={
                index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
              }
              className="w-full"
              href="#"
              size="lg"
            >
              {item}
            </Link>
          </NavbarMenuItem>
        ))}
      </NavbarMenu>
    </Navbar>
  )
}

// Navbar z wyszukiwarką
function NavbarWithSearch() {
  return (
    <Navbar>
      <NavbarContent justify="start">
        <NavbarBrand className="mr-4">
          <Logo />
          <p className="hidden sm:block font-bold text-inherit">ACME</p>
        </NavbarBrand>
        <NavbarContent className="hidden sm:flex gap-3">
          <NavbarItem>
            <Link color="foreground" href="#">Features</Link>
          </NavbarItem>
          <NavbarItem isActive>
            <Link href="#" aria-current="page">Customers</Link>
          </NavbarItem>
        </NavbarContent>
      </NavbarContent>

      <NavbarContent as="div" className="items-center" justify="end">
        <Input
          classNames={{
            base: "max-w-full sm:max-w-[10rem] h-10",
            mainWrapper: "h-full",
            input: "text-small",
            inputWrapper: "h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
          }}
          placeholder="Type to search..."
          size="sm"
          startContent={<SearchIcon size={18} />}
          type="search"
        />
        <Dropdown placement="bottom-end">
          <DropdownTrigger>
            <Avatar
              isBordered
              as="button"
              className="transition-transform"
              color="secondary"
              name="Jason Hughes"
              size="sm"
              src="/avatars/avatar-1.png"
            />
          </DropdownTrigger>
          <DropdownMenu aria-label="Profile Actions" variant="flat">
            <DropdownItem key="profile" className="h-14 gap-2">
              <p className="font-semibold">Signed in as</p>
              <p className="font-semibold">zoey@example.com</p>
            </DropdownItem>
            <DropdownItem key="settings">My Settings</DropdownItem>
            <DropdownItem key="logout" color="danger">Log Out</DropdownItem>
          </DropdownMenu>
        </Dropdown>
      </NavbarContent>
    </Navbar>
  )
}

Tabs

Code
TypeScript
import { Tabs, Tab, Card, CardBody, Input, Link, Button } from '@heroui/react'

function TabsExamples() {
  return (
    <div className="flex flex-col w-full">
      {/* Podstawowe */}
      <Tabs aria-label="Options">
        <Tab key="photos" title="Photos">
          <Card>
            <CardBody>
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </CardBody>
          </Card>
        </Tab>
        <Tab key="music" title="Music">
          <Card>
            <CardBody>
              Ut enim ad minim veniam, quis nostrud exercitation ullamco.
            </CardBody>
          </Card>
        </Tab>
        <Tab key="videos" title="Videos">
          <Card>
            <CardBody>
              Excepteur sint occaecat cupidatat non proident.
            </CardBody>
          </Card>
        </Tab>
      </Tabs>

      {/* Z ikonami */}
      <Tabs aria-label="Options" color="primary" variant="bordered">
        <Tab
          key="photos"
          title={
            <div className="flex items-center space-x-2">
              <GalleryIcon />
              <span>Photos</span>
            </div>
          }
        >
          <p>Photos content</p>
        </Tab>
        <Tab
          key="music"
          title={
            <div className="flex items-center space-x-2">
              <MusicIcon />
              <span>Music</span>
            </div>
          }
        >
          <p>Music content</p>
        </Tab>
      </Tabs>

      {/* Warianty */}
      <div className="flex flex-col gap-4">
        <Tabs variant="solid" aria-label="Solid">
          <Tab key="1" title="Tab 1" />
          <Tab key="2" title="Tab 2" />
        </Tabs>

        <Tabs variant="bordered" aria-label="Bordered">
          <Tab key="1" title="Tab 1" />
          <Tab key="2" title="Tab 2" />
        </Tabs>

        <Tabs variant="light" aria-label="Light">
          <Tab key="1" title="Tab 1" />
          <Tab key="2" title="Tab 2" />
        </Tabs>

        <Tabs variant="underlined" aria-label="Underlined">
          <Tab key="1" title="Tab 1" />
          <Tab key="2" title="Tab 2" />
        </Tabs>
      </div>

      {/* Disabled tabs */}
      <Tabs disabledKeys={["music"]} aria-label="Options">
        <Tab key="photos" title="Photos">Content 1</Tab>
        <Tab key="music" title="Music">Content 2</Tab>
        <Tab key="videos" title="Videos">Content 3</Tab>
      </Tabs>
    </div>
  )
}

Dropdown

Code
TypeScript
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from '@heroui/react'

function DropdownExamples() {
  return (
    <div className="flex gap-4">
      {/* Podstawowy */}
      <Dropdown>
        <DropdownTrigger>
          <Button variant="bordered">Open Menu</Button>
        </DropdownTrigger>
        <DropdownMenu aria-label="Actions">
          <DropdownItem key="new">New file</DropdownItem>
          <DropdownItem key="copy">Copy link</DropdownItem>
          <DropdownItem key="edit">Edit file</DropdownItem>
          <DropdownItem key="delete" className="text-danger" color="danger">
            Delete file
          </DropdownItem>
        </DropdownMenu>
      </Dropdown>

      {/* Z ikonami */}
      <Dropdown>
        <DropdownTrigger>
          <Button variant="bordered">Actions</Button>
        </DropdownTrigger>
        <DropdownMenu aria-label="Actions">
          <DropdownItem
            key="new"
            startContent={<AddIcon className="text-xl" />}
          >
            New file
          </DropdownItem>
          <DropdownItem
            key="copy"
            startContent={<CopyIcon className="text-xl" />}
          >
            Copy link
          </DropdownItem>
          <DropdownItem
            key="edit"
            startContent={<EditIcon className="text-xl" />}
          >
            Edit file
          </DropdownItem>
          <DropdownItem
            key="delete"
            className="text-danger"
            color="danger"
            startContent={<DeleteIcon className="text-xl" />}
          >
            Delete file
          </DropdownItem>
        </DropdownMenu>
      </Dropdown>

      {/* Z sekcjami */}
      <Dropdown>
        <DropdownTrigger>
          <Button variant="bordered">Grouped</Button>
        </DropdownTrigger>
        <DropdownMenu aria-label="Actions">
          <DropdownSection title="Actions" showDivider>
            <DropdownItem key="new">New file</DropdownItem>
            <DropdownItem key="copy">Copy link</DropdownItem>
          </DropdownSection>
          <DropdownSection title="Danger Zone">
            <DropdownItem
              key="delete"
              className="text-danger"
              color="danger"
            >
              Delete file
            </DropdownItem>
          </DropdownSection>
        </DropdownMenu>
      </Dropdown>

      {/* Single selection */}
      <Dropdown>
        <DropdownTrigger>
          <Button variant="bordered">Select option</Button>
        </DropdownTrigger>
        <DropdownMenu
          aria-label="Single selection"
          selectionMode="single"
          selectedKeys={selectedKeys}
          onSelectionChange={setSelectedKeys}
        >
          <DropdownItem key="text">Text</DropdownItem>
          <DropdownItem key="number">Number</DropdownItem>
          <DropdownItem key="date">Date</DropdownItem>
        </DropdownMenu>
      </Dropdown>
    </div>
  )
}

Avatar

Code
TypeScript
import { Avatar, AvatarGroup } from '@heroui/react'

function AvatarExamples() {
  return (
    <div className="flex flex-col gap-4">
      {/* Rozmiary */}
      <div className="flex gap-4 items-center">
        <Avatar src="/avatar.png" size="sm" />
        <Avatar src="/avatar.png" size="md" />
        <Avatar src="/avatar.png" size="lg" />
      </div>

      {/* Z inicjałami */}
      <div className="flex gap-4">
        <Avatar name="Jane" />
        <Avatar name="John Doe" />
        <Avatar name="JR" />
      </div>

      {/* Z border */}
      <div className="flex gap-4">
        <Avatar isBordered src="/avatar.png" />
        <Avatar isBordered color="secondary" src="/avatar.png" />
        <Avatar isBordered color="success" src="/avatar.png" />
        <Avatar isBordered color="warning" src="/avatar.png" />
        <Avatar isBordered color="danger" src="/avatar.png" />
      </div>

      {/* Avatar Group */}
      <AvatarGroup isBordered max={3}>
        <Avatar src="/avatar1.png" />
        <Avatar src="/avatar2.png" />
        <Avatar src="/avatar3.png" />
        <Avatar src="/avatar4.png" />
        <Avatar src="/avatar5.png" />
      </AvatarGroup>

      {/* Z fallback */}
      <Avatar
        showFallback
        src="https://broken-link.jpg"
        fallback={<UserIcon className="w-6 h-6" />}
      />
    </div>
  )
}

Badge

Code
TypeScript
import { Badge, Avatar, Button } from '@heroui/react'

function BadgeExamples() {
  return (
    <div className="flex gap-4 items-center">
      {/* Na Avatar */}
      <Badge content="5" color="danger">
        <Avatar src="/avatar.png" />
      </Badge>

      {/* Różne kolory */}
      <Badge content="new" color="primary">
        <Avatar src="/avatar.png" />
      </Badge>

      <Badge content="99+" color="secondary">
        <Avatar src="/avatar.png" />
      </Badge>

      {/* Placement */}
      <Badge content="5" placement="top-left">
        <Avatar src="/avatar.png" />
      </Badge>

      <Badge content="5" placement="bottom-right">
        <Avatar src="/avatar.png" />
      </Badge>

      {/* Dot */}
      <Badge content="" color="success" placement="bottom-right">
        <Avatar src="/avatar.png" />
      </Badge>

      {/* Na Button */}
      <Badge content="3" color="danger">
        <Button>Notifications</Button>
      </Badge>
    </div>
  )
}

Checkbox

Code
TypeScript
import { Checkbox, CheckboxGroup } from '@heroui/react'

function CheckboxExamples() {
  const [selected, setSelected] = useState<string[]>([])

  return (
    <div className="flex flex-col gap-4">
      {/* Podstawowy */}
      <Checkbox defaultSelected>Subscribe to newsletter</Checkbox>

      {/* Rozmiary */}
      <div className="flex gap-4">
        <Checkbox size="sm">Small</Checkbox>
        <Checkbox size="md">Medium</Checkbox>
        <Checkbox size="lg">Large</Checkbox>
      </div>

      {/* Kolory */}
      <div className="flex gap-4">
        <Checkbox color="default" defaultSelected>Default</Checkbox>
        <Checkbox color="primary" defaultSelected>Primary</Checkbox>
        <Checkbox color="secondary" defaultSelected>Secondary</Checkbox>
        <Checkbox color="success" defaultSelected>Success</Checkbox>
        <Checkbox color="warning" defaultSelected>Warning</Checkbox>
        <Checkbox color="danger" defaultSelected>Danger</Checkbox>
      </div>

      {/* Disabled */}
      <Checkbox isDisabled>Disabled</Checkbox>
      <Checkbox isDisabled defaultSelected>Disabled Selected</Checkbox>

      {/* Checkbox Group */}
      <CheckboxGroup
        label="Select cities"
        value={selected}
        onValueChange={setSelected}
      >
        <Checkbox value="buenos-aires">Buenos Aires</Checkbox>
        <Checkbox value="sydney">Sydney</Checkbox>
        <Checkbox value="san-francisco">San Francisco</Checkbox>
        <Checkbox value="london">London</Checkbox>
      </CheckboxGroup>
      <p className="text-default-500 text-sm">
        Selected: {selected.join(", ")}
      </p>

      {/* Horizontal */}
      <CheckboxGroup
        label="Select cities"
        orientation="horizontal"
        defaultValue={["london"]}
      >
        <Checkbox value="buenos-aires">Buenos Aires</Checkbox>
        <Checkbox value="sydney">Sydney</Checkbox>
        <Checkbox value="london">London</Checkbox>
      </CheckboxGroup>
    </div>
  )
}

Switch

Code
TypeScript
import { Switch } from '@heroui/react'

function SwitchExamples() {
  return (
    <div className="flex flex-col gap-4">
      {/* Podstawowy */}
      <Switch defaultSelected>Enable notifications</Switch>

      {/* Rozmiary */}
      <div className="flex gap-4">
        <Switch size="sm">Small</Switch>
        <Switch size="md">Medium</Switch>
        <Switch size="lg">Large</Switch>
      </div>

      {/* Kolory */}
      <div className="flex gap-4">
        <Switch color="default" defaultSelected />
        <Switch color="primary" defaultSelected />
        <Switch color="secondary" defaultSelected />
        <Switch color="success" defaultSelected />
        <Switch color="warning" defaultSelected />
        <Switch color="danger" defaultSelected />
      </div>

      {/* Z ikonami */}
      <Switch
        defaultSelected
        thumbIcon={({ isSelected, className }) =>
          isSelected ? (
            <SunIcon className={className} />
          ) : (
            <MoonIcon className={className} />
          )
        }
      >
        Dark mode
      </Switch>

      {/* Disabled */}
      <Switch isDisabled>Disabled</Switch>

      {/* Read only */}
      <Switch isReadOnly defaultSelected>Read Only</Switch>
    </div>
  )
}

Tooltip

Code
TypeScript
import { Tooltip, Button } from '@heroui/react'

function TooltipExamples() {
  return (
    <div className="flex gap-4 flex-wrap">
      {/* Podstawowy */}
      <Tooltip content="I am a tooltip">
        <Button>Hover me</Button>
      </Tooltip>

      {/* Placement */}
      <Tooltip content="Top" placement="top">
        <Button variant="flat">Top</Button>
      </Tooltip>
      <Tooltip content="Bottom" placement="bottom">
        <Button variant="flat">Bottom</Button>
      </Tooltip>
      <Tooltip content="Left" placement="left">
        <Button variant="flat">Left</Button>
      </Tooltip>
      <Tooltip content="Right" placement="right">
        <Button variant="flat">Right</Button>
      </Tooltip>

      {/* Kolory */}
      <Tooltip content="Primary" color="primary">
        <Button color="primary" variant="flat">Primary</Button>
      </Tooltip>
      <Tooltip content="Danger" color="danger">
        <Button color="danger" variant="flat">Danger</Button>
      </Tooltip>

      {/* Z delay */}
      <Tooltip content="Delayed tooltip" delay={1000} closeDelay={500}>
        <Button>Delayed</Button>
      </Tooltip>

      {/* Custom content */}
      <Tooltip
        content={
          <div className="px-1 py-2">
            <div className="text-small font-bold">Custom Content</div>
            <div className="text-tiny">This is a custom tooltip.</div>
          </div>
        }
      >
        <Button variant="bordered">Custom</Button>
      </Tooltip>
    </div>
  )
}

Progress i CircularProgress

Code
TypeScript
import { Progress, CircularProgress } from '@heroui/react'

function ProgressExamples() {
  return (
    <div className="flex flex-col gap-6 w-full max-w-md">
      {/* Progress Bar */}
      <Progress aria-label="Loading..." value={60} />

      {/* Z etykietą */}
      <Progress
        label="Loading..."
        value={60}
        showValueLabel={true}
      />

      {/* Rozmiary */}
      <Progress size="sm" aria-label="Loading..." value={30} />
      <Progress size="md" aria-label="Loading..." value={50} />
      <Progress size="lg" aria-label="Loading..." value={70} />

      {/* Kolory */}
      <Progress color="default" aria-label="Loading..." value={60} />
      <Progress color="primary" aria-label="Loading..." value={60} />
      <Progress color="secondary" aria-label="Loading..." value={60} />
      <Progress color="success" aria-label="Loading..." value={60} />
      <Progress color="warning" aria-label="Loading..." value={60} />
      <Progress color="danger" aria-label="Loading..." value={60} />

      {/* Indeterminate */}
      <Progress
        isIndeterminate
        aria-label="Loading..."
        className="max-w-md"
      />

      {/* Striped */}
      <Progress
        isStriped
        aria-label="Loading..."
        value={60}
      />

      {/* Circular Progress */}
      <div className="flex gap-4">
        <CircularProgress aria-label="Loading..." />
        <CircularProgress color="primary" aria-label="Loading..." />
        <CircularProgress size="lg" value={70} showValueLabel={true} />
      </div>
    </div>
  )
}

Table

Code
TypeScript
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, User, Chip, Tooltip, Button } from '@heroui/react'

const users = [
  { id: 1, name: "Tony Reichert", role: "CEO", status: "active", avatar: "/avatar1.png" },
  { id: 2, name: "Zoey Lang", role: "Tech Lead", status: "paused", avatar: "/avatar2.png" },
  { id: 3, name: "Jane Fisher", role: "Sr. Dev", status: "active", avatar: "/avatar3.png" },
]

const statusColorMap = {
  active: "success",
  paused: "danger",
  vacation: "warning",
}

function TableExample() {
  return (
    <Table aria-label="Example table with custom cells">
      <TableHeader>
        <TableColumn>NAME</TableColumn>
        <TableColumn>ROLE</TableColumn>
        <TableColumn>STATUS</TableColumn>
        <TableColumn>ACTIONS</TableColumn>
      </TableHeader>
      <TableBody>
        {users.map((user) => (
          <TableRow key={user.id}>
            <TableCell>
              <User
                avatarProps={{ radius: "lg", src: user.avatar }}
                name={user.name}
                description={user.email}
              />
            </TableCell>
            <TableCell>
              <div className="flex flex-col">
                <p className="text-bold text-sm">{user.role}</p>
                <p className="text-bold text-sm text-default-400">Developer</p>
              </div>
            </TableCell>
            <TableCell>
              <Chip
                className="capitalize"
                color={statusColorMap[user.status]}
                size="sm"
                variant="flat"
              >
                {user.status}
              </Chip>
            </TableCell>
            <TableCell>
              <div className="flex items-center gap-2">
                <Tooltip content="View">
                  <Button isIconOnly size="sm" variant="light">
                    <EyeIcon />
                  </Button>
                </Tooltip>
                <Tooltip content="Edit">
                  <Button isIconOnly size="sm" variant="light">
                    <EditIcon />
                  </Button>
                </Tooltip>
                <Tooltip color="danger" content="Delete">
                  <Button isIconOnly size="sm" variant="light" color="danger">
                    <DeleteIcon />
                  </Button>
                </Tooltip>
              </div>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Theming

Konfiguracja motywów

JStailwind.config.js
JavaScript
// tailwind.config.js
const { heroui } = require("@heroui/react")

module.exports = {
  // ...
  plugins: [
    heroui({
      themes: {
        light: {
          colors: {
            background: "#FFFFFF",
            foreground: "#11181C",
            primary: {
              50: "#F0F5FF",
              100: "#E0EAFF",
              200: "#C7D7FE",
              300: "#A4BCFD",
              400: "#8098F9",
              500: "#6172F3",
              600: "#444CE7",
              700: "#3538CD",
              800: "#2D31A6",
              900: "#2D3282",
              DEFAULT: "#6172F3",
              foreground: "#FFFFFF",
            },
            focus: "#6172F3",
          },
        },
        dark: {
          colors: {
            background: "#0D0D0D",
            foreground: "#ECEDEE",
            primary: {
              50: "#1A1A2E",
              100: "#2A2A4A",
              200: "#3A3A6A",
              300: "#5A5A9A",
              400: "#7A7ABA",
              500: "#9A9ADA",
              600: "#BABAFA",
              700: "#DADAFF",
              800: "#EAEAFF",
              900: "#FAFAFF",
              DEFAULT: "#9A9ADA",
              foreground: "#000000",
            },
          },
        },
        // Custom theme
        "purple-dark": {
          extend: "dark",
          colors: {
            background: "#0D001A",
            foreground: "#ffffff",
            primary: {
              50: "#3B096C",
              100: "#520F83",
              200: "#7318A2",
              300: "#9823C2",
              400: "#c031e2",
              500: "#DD62ED",
              600: "#F182F6",
              700: "#FCADF9",
              800: "#FDD5F9",
              900: "#FEECFE",
              DEFAULT: "#DD62ED",
              foreground: "#ffffff",
            },
            focus: "#F182F6",
          },
          layout: {
            disabledOpacity: "0.3",
            radius: {
              small: "4px",
              medium: "6px",
              large: "8px",
            },
            borderWidth: {
              small: "1px",
              medium: "2px",
              large: "3px",
            },
          },
        },
      },
    }),
  ],
}

Przełączanie motywów

Code
TypeScript
'use client'

import { useTheme } from 'next-themes'
import { Button, Switch } from '@heroui/react'
import { SunIcon, MoonIcon } from './icons'

function ThemeSwitcher() {
  const { theme, setTheme } = useTheme()

  return (
    <div className="flex gap-4">
      {/* Przyciskami */}
      <Button
        variant={theme === 'light' ? 'solid' : 'bordered'}
        onPress={() => setTheme('light')}
      >
        Light
      </Button>
      <Button
        variant={theme === 'dark' ? 'solid' : 'bordered'}
        onPress={() => setTheme('dark')}
      >
        Dark
      </Button>
      <Button
        variant={theme === 'purple-dark' ? 'solid' : 'bordered'}
        onPress={() => setTheme('purple-dark')}
      >
        Purple
      </Button>

      {/* Switch */}
      <Switch
        defaultSelected={theme === 'dark'}
        onChange={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
        thumbIcon={({ isSelected }) =>
          isSelected ? <MoonIcon /> : <SunIcon />
        }
      />
    </div>
  )
}

Animacje z Framer Motion

HeroUI używa Framer Motion do animacji. Możesz je customizować:

Code
TypeScript
import { Button, Modal, ModalContent } from '@heroui/react'

function CustomAnimationModal() {
  const { isOpen, onOpen, onOpenChange } = useDisclosure()

  return (
    <>
      <Button onPress={onOpen}>Open</Button>
      <Modal
        isOpen={isOpen}
        onOpenChange={onOpenChange}
        motionProps={{
          variants: {
            enter: {
              y: 0,
              opacity: 1,
              transition: {
                duration: 0.3,
                ease: "easeOut",
              },
            },
            exit: {
              y: -20,
              opacity: 0,
              transition: {
                duration: 0.2,
                ease: "easeIn",
              },
            },
          },
        }}
      >
        <ModalContent>
          <p>Custom animation!</p>
        </ModalContent>
      </Modal>
    </>
  )
}

Integracja z React Hook Form

Code
TypeScript
import { useForm, Controller } from 'react-hook-form'
import { Input, Select, SelectItem, Checkbox, Button } from '@heroui/react'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'

const schema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  role: z.string().min(1, 'Please select a role'),
  terms: z.boolean().refine((val) => val === true, 'You must accept terms'),
})

type FormData = z.infer<typeof schema>

function HeroUIForm() {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: {
      name: '',
      email: '',
      role: '',
      terms: false,
    },
  })

  const onSubmit = (data: FormData) => {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4 max-w-md">
      <Controller
        name="name"
        control={control}
        render={({ field }) => (
          <Input
            {...field}
            label="Name"
            placeholder="Enter your name"
            isInvalid={!!errors.name}
            errorMessage={errors.name?.message}
          />
        )}
      />

      <Controller
        name="email"
        control={control}
        render={({ field }) => (
          <Input
            {...field}
            type="email"
            label="Email"
            placeholder="you@example.com"
            isInvalid={!!errors.email}
            errorMessage={errors.email?.message}
          />
        )}
      />

      <Controller
        name="role"
        control={control}
        render={({ field }) => (
          <Select
            {...field}
            label="Role"
            placeholder="Select a role"
            isInvalid={!!errors.role}
            errorMessage={errors.role?.message}
          >
            <SelectItem key="developer">Developer</SelectItem>
            <SelectItem key="designer">Designer</SelectItem>
            <SelectItem key="manager">Manager</SelectItem>
          </Select>
        )}
      />

      <Controller
        name="terms"
        control={control}
        render={({ field: { value, onChange, ...field } }) => (
          <Checkbox
            {...field}
            isSelected={value}
            onValueChange={onChange}
            isInvalid={!!errors.terms}
          >
            I accept the terms and conditions
          </Checkbox>
        )}
      />
      {errors.terms && (
        <p className="text-danger text-sm">{errors.terms.message}</p>
      )}

      <Button type="submit" color="primary">
        Submit
      </Button>
    </form>
  )
}

Best Practices

1. Używaj Server Components gdzie możliwe

TSapp/page.tsx
TypeScript
// app/page.tsx - Server Component
import { Card, CardBody } from '@heroui/react'

export default async function Page() {
  const data = await fetchData()

  return (
    <Card>
      <CardBody>{data.title}</CardBody>
    </Card>
  )
}

// Interaktywne komponenty w Client Components
'use client'
import { Button } from '@heroui/react'

export function InteractiveButton() {
  return <Button onPress={() => console.log('clicked')}>Click</Button>
}

2. Lazy loading dla dużych komponentów

Code
TypeScript
import dynamic from 'next/dynamic'

const Modal = dynamic(() => import('@heroui/react').then((mod) => mod.Modal), {
  ssr: false,
})

3. Customizacja przez classNames

Code
TypeScript
<Input
  classNames={{
    base: "max-w-xs",
    mainWrapper: "h-full",
    input: "text-small",
    inputWrapper: "h-full font-normal text-default-500 bg-default-400/20",
  }}
/>

FAQ - Najczęściej Zadawane Pytania

Jaka jest różnica między NextUI a HeroUI?

HeroUI to nowa nazwa dla NextUI. Rebranding nastąpił w 2024 roku, aby uniknąć konfuzji z Next.js. To ta sama biblioteka z tym samym zespołem.

Czy HeroUI działa z Next.js App Router?

Tak! HeroUI w pełni wspiera Next.js App Router. Pamiętaj o 'use client' dla interaktywnych komponentów.

Czy mogę używać HeroUI bez Tailwind CSS?

Nie. HeroUI jest zbudowany na Tailwind CSS i wymaga go do działania.

Jak zmienić domyślne animacje?

Użyj prop motionProps do przekazania własnych wariantów Framer Motion.

Czy HeroUI jest darmowy?

Tak, HeroUI jest w pełni open-source i darmowy na licencji MIT.

Podsumowanie

HeroUI to doskonały wybór dla deweloperów React szukających pięknej, dobrze zaprojektowanej biblioteki komponentów. Dzięki integracji z Tailwind CSS, animacjom Framer Motion i dostępności React Aria, HeroUI oferuje kompletne rozwiązanie do budowania nowoczesnych interfejsów użytkownika.

Kluczowe zalety:

  • Piękny, nowoczesny design
  • Płynne animacje out of the box
  • Pełna dostępność WAI-ARIA
  • Łatwa customizacja przez Tailwind CSS
  • Wsparcie dla dark mode
  • Aktywna społeczność i rozwój