Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide13 min read

HeroUI - Kompletny Przewodnik po Pięknych Komponentach React

HeroUI (formerly NextUI) is a modern React library with beautiful animations, smooth transitions and full accessibility. Built on Tailwind CSS and React Aria.

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?

  1. Beautiful Design - Thoughtful styles, gradients, and animations
  2. Framer Motion - All animations powered by Framer Motion
  3. React Aria - Full WAI-ARIA accessibility under the hood
  4. Tailwind CSS - Native support and easy customization
  5. Dark Mode - Built-in theme system
  6. TypeScript - Full type definitions for all components

HeroUI vs other libraries

FeatureHeroUIChakra UIshadcn/uiMaterial UI
StyleModern, fluidMinimalistCopy-pasteMaterial Design
AnimationsFramer MotionCSSTailwindCSS-in-JS
CustomizationTailwindPropsFull controlTheme override
Size~120KB~300KB~0KB*~500KB
A11yReact AriaBuilt-inRadixBuilt-in
Dark ModeBuilt-inBuilt-inManualBuilt-in
TypeScriptFullFullFullFull

*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

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

# yarn
yarn add @heroui/react framer-motion

# pnpm
pnpm add @heroui/react framer-motion

Tailwind CSS Configuration

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="en" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Individual Package Installation

For smaller bundle size, you can install only needed components:

Code
Bash
# 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
Code
TypeScript
// 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

Code
TypeScript
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

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

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

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">
      {/* 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

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

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",
    "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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

JStailwind.config.js
JavaScript
// 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

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">
      {/* 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:

Code
TypeScript
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

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

  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

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

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

Code
TypeScript
import dynamic from 'next/dynamic'

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

3. Customization through 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 - 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