HeroUI - Complete Guide to Beautiful React Components
What is HeroUI?
HeroUI (formerly known as NextUI) is a modern React component library created with beautiful design, smooth animations, and excellent accessibility in mind. Built on the solid foundations of Tailwind CSS and React Aria, HeroUI offers a set of ready-to-use components that look professional "out of the box" but are fully customizable.
The rebrand from NextUI to HeroUI occurred in 2024 to avoid confusion with Next.js and better convey the library's mission - creating "heroic" user interfaces. Under the hood, it's the same great library with continuous development and community support.
Why HeroUI?
- Beautiful Design - Thoughtful styles, gradients, and animations
- Framer Motion - All animations powered by Framer Motion
- React Aria - Full WAI-ARIA accessibility under the hood
- Tailwind CSS - Native support and easy customization
- Dark Mode - Built-in theme system
- TypeScript - Full type definitions for all components
HeroUI vs other libraries
| Feature | HeroUI | Chakra UI | shadcn/ui | Material UI |
|---|---|---|---|---|
| Style | Modern, fluid | Minimalist | Copy-paste | Material Design |
| Animations | Framer Motion | CSS | Tailwind | CSS-in-JS |
| Customization | Tailwind | Props | Full control | Theme override |
| Size | ~120KB | ~300KB | ~0KB* | ~500KB |
| A11y | React Aria | Built-in | Radix | Built-in |
| Dark Mode | Built-in | Built-in | Manual | Built-in |
| TypeScript | Full | Full | Full | Full |
*shadcn/ui is copy-paste, you don't install a library
When to choose HeroUI?
Choose HeroUI when:
- You want beautiful design without much effort
- You need smooth animations
- You use Tailwind CSS
- Accessibility is important to you
- You're building a Next.js or React application
Consider alternatives when:
- You need Material Design (Material UI)
- You want full control without pre-built styles (shadcn/ui)
- You're building for enterprise (Ant Design)
Installation and setup
Package installation
# npm
npm install @heroui/react framer-motion
# yarn
yarn add @heroui/react framer-motion
# pnpm
pnpm add @heroui/react framer-motionTailwind CSS Configuration
// 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="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}Individual Package Installation
For smaller bundle size, you can install only needed components:
# Install only Button and Card
npm install @heroui/button @heroui/card
# Or individually
npm install @heroui/modal
npm install @heroui/input
npm install @heroui/navbar// Import from individual packages
import { Button } from '@heroui/button'
import { Card, CardBody } from '@heroui/card'
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@heroui/modal'UI components
Button
import { Button } from '@heroui/react'
function ButtonExamples() {
return (
<div className="flex flex-wrap gap-4">
{/* Colors */}
<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>
{/* Variants */}
<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>
{/* Sizes */}
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
{/* Loading */}
<Button isLoading>Loading...</Button>
{/* Disabled */}
<Button isDisabled>Disabled</Button>
{/* With icons */}
<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>
)
}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>
)
}
// 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 with 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">
{/* Basic */}
<Input
type="email"
label="Email"
placeholder="Enter your email"
/>
{/* With description */}
<Input
type="email"
label="Email"
placeholder="you@example.com"
description="We'll never share your email"
/>
{/* Required */}
<Input
isRequired
type="email"
label="Email"
placeholder="Enter your email"
/>
{/* Variants */}
<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" />
{/* Sizes */}
<Input size="sm" label="Small" />
<Input size="md" label="Medium" />
<Input size="lg" label="Large" />
{/* With icons */}
<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 with toggle */}
<Input
label="Password"
placeholder="Enter password"
type={isVisible ? "text" : "password"}
endContent={
<button
type="button"
onClick={() => setIsVisible(!isVisible)}
>
{isVisible ? <EyeOffIcon /> : <EyeIcon />}
</button>
}
/>
{/* 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"
/>
</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>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Action
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
)
}
// Modal with form
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"
/>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Sign up
</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",
"Settings",
"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 === menuItems.length - 1 ? "danger" : "foreground"
}
className="w-full"
href="#"
size="lg"
>
{item}
</Link>
</NavbarMenuItem>
))}
</NavbarMenu>
</Navbar>
)
}Tabs
import { Tabs, Tab, Card, CardBody } from '@heroui/react'
function TabsExamples() {
return (
<div className="flex flex-col w-full">
{/* Basic */}
<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>
{/* Variants */}
<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="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">
{/* Basic */}
<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>
{/* With icons */}
<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="delete"
className="text-danger"
color="danger"
startContent={<DeleteIcon className="text-xl" />}
>
Delete file
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
)
}Table
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, User, Chip } 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" },
]
function TableExample() {
return (
<Table aria-label="Example table with custom cells">
<TableHeader>
<TableColumn>NAME</TableColumn>
<TableColumn>ROLE</TableColumn>
<TableColumn>STATUS</TableColumn>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>
<User
avatarProps={{ radius: "lg", src: user.avatar }}
name={user.name}
/>
</TableCell>
<TableCell>
<p className="text-bold text-sm">{user.role}</p>
</TableCell>
<TableCell>
<Chip
className="capitalize"
color={user.status === 'active' ? 'success' : 'danger'}
size="sm"
variant="flat"
>
{user.status}
</Chip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}Theming
Theme Configuration
// tailwind.config.js
const { heroui } = require("@heroui/react")
module.exports = {
plugins: [
heroui({
themes: {
light: {
colors: {
background: "#FFFFFF",
foreground: "#11181C",
primary: {
DEFAULT: "#6172F3",
foreground: "#FFFFFF",
},
},
},
dark: {
colors: {
background: "#0D0D0D",
foreground: "#ECEDEE",
primary: {
DEFAULT: "#9A9ADA",
foreground: "#000000",
},
},
},
// Custom theme
"purple-dark": {
extend: "dark",
colors: {
background: "#0D001A",
foreground: "#ffffff",
primary: {
DEFAULT: "#DD62ED",
foreground: "#ffffff",
},
},
},
},
}),
],
}Theme Switching
'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">
{/* With buttons */}
<Button
variant={theme === 'light' ? 'solid' : 'bordered'}
onPress={() => setTheme('light')}
>
Light
</Button>
<Button
variant={theme === 'dark' ? 'solid' : 'bordered'}
onPress={() => setTheme('dark')}
>
Dark
</Button>
{/* With Switch */}
<Switch
defaultSelected={theme === 'dark'}
onChange={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
thumbIcon={({ isSelected }) =>
isSelected ? <MoonIcon /> : <SunIcon />
}
/>
</div>
)
}Animations with Framer Motion
HeroUI uses Framer Motion for animations. You can customize them:
import { Modal, ModalContent } from '@heroui/react'
function CustomAnimationModal() {
const { isOpen, onOpen, onOpenChange } = useDisclosure()
return (
<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>
)
}Integration with 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),
})
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}
/>
)}
/>
<Button type="submit" color="primary">
Submit
</Button>
</form>
)
}Best practices
1. Use server components where possible
// 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>
)
}
// Interactive components in Client Components
'use client'
import { Button } from '@heroui/react'
export function InteractiveButton() {
return <Button onPress={() => console.log('clicked')}>Click</Button>
}2. Lazy loading for large components
import dynamic from 'next/dynamic'
const Modal = dynamic(() => import('@heroui/react').then((mod) => mod.Modal), {
ssr: false,
})3. Customization through 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 - frequently asked questions
What's the difference between NextUI and HeroUI?
HeroUI is the new name for NextUI. The rebrand occurred in 2024 to avoid confusion with Next.js. It's the same library with the same team.
Does HeroUI work with Next.js App Router?
Yes! HeroUI fully supports Next.js App Router. Remember to use 'use client' for interactive components.
Can I use HeroUI without Tailwind CSS?
No. HeroUI is built on Tailwind CSS and requires it to work.
How do I change default animations?
Use the motionProps prop to pass your own Framer Motion variants.
Is HeroUI free?
Yes, HeroUI is fully open-source and free under the MIT license.
Summary
HeroUI is an excellent choice for React developers looking for a beautiful, well-designed component library. With its integration with Tailwind CSS, Framer Motion animations, and React Aria accessibility, HeroUI offers a complete solution for building modern user interfaces.
Key advantages:
- Beautiful, modern design
- Smooth animations out of the box
- Full WAI-ARIA accessibility
- Easy customization through Tailwind CSS
- Dark mode support
- Active community and development