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?
- Piękny design - Przemyślane style, gradienty i animacje
- Framer Motion - Wszystkie animacje oparte na Framer Motion
- React Aria - Pełna dostępność WAI-ARIA pod spodem
- Tailwind CSS - Natywne wsparcie i łatwa customizacja
- Dark Mode - Wbudowany system motywów
- TypeScript - Pełne typy dla wszystkich komponentów
HeroUI vs Inne Biblioteki
| Cecha | HeroUI | Chakra UI | shadcn/ui | Material UI |
|---|---|---|---|---|
| Styl | Nowoczesny, fluid | Minimalistyczny | Copy-paste | Material Design |
| Animacje | Framer Motion | CSS | Tailwind | CSS-in-JS |
| Customizacja | Tailwind | Props | Full control | Theme override |
| Rozmiar | ~120KB | ~300KB | ~0KB* | ~500KB |
| A11y | React Aria | Built-in | Radix | Built-in |
| Dark Mode | Wbudowany | Wbudowany | Manual | Wbudowany |
| TypeScript | Pełne | Pełne | Pełne | Peł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
# npm
npm install @heroui/react framer-motion
# yarn
yarn add @heroui/react framer-motion
# pnpm
pnpm add @heroui/react framer-motionKonfiguracja Tailwind CSS
// 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
// 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:
# 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// 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
// 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
'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ć:
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
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
// 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
import dynamic from 'next/dynamic'
const Modal = dynamic(() => import('@heroui/react').then((mod) => mod.Modal), {
ssr: false,
})3. Customizacja przez classNames
<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