| name | heroui-components |
| description | Add or customize HeroUI components in the web application. Use when implementing new UI features with HeroUI design system or updating component styling. |
| allowed-tools | Read, Edit, Write, mcp__heroui-react__list_components, mcp__heroui-react__get_component_info, mcp__heroui-react__get_component_examples |
HeroUI Components Skill
This skill helps you work with HeroUI (Hero UI / NextUI v3) components in apps/web/.
When to Use This Skill
- Adding new HeroUI components to pages
- Customizing HeroUI component variants
- Implementing forms with HeroUI inputs
- Creating modal dialogs and drawers
- Building navigation with HeroUI components
- Debugging HeroUI styling issues
HeroUI Overview
HeroUI is a modern React UI library built on Tailwind CSS and React Aria, providing accessible components.
Discovery and Research
List Available Components
// Use MCP tool to discover components
mcp__heroui-react__list_components()
Get Component Information
// Get details about a specific component
mcp__heroui-react__get_component_info({ component: "Button" })
mcp__heroui-react__get_component_info({ component: "Input" })
Get Usage Examples
// Get examples for a component
mcp__heroui-react__get_component_examples({ component: "Modal" })
Installation and Setup
HeroUI is already installed in the web app. Import from @heroui/react:
import { Button, Input, Card } from "@heroui/react";
Common Components
1. Button Component
import { Button } from "@heroui/react";
export function ActionButton() {
return (
<>
{/* Basic button */}
<Button>Click me</Button>
{/* Color variants */}
<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>
{/* Size variants */}
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</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>
{/* With icon */}
<Button startContent={<SearchIcon />}>Search</Button>
<Button endContent={<ArrowIcon />}>Next</Button>
{/* Loading state */}
<Button isLoading>Loading...</Button>
{/* Disabled */}
<Button isDisabled>Disabled</Button>
</>
);
}
2. Input Component
import { Input } from "@heroui/react";
export function SearchForm() {
return (
<>
{/* Basic input */}
<Input
type="text"
label="Search"
placeholder="Enter search term"
/>
{/* With validation */}
<Input
type="email"
label="Email"
placeholder="you@example.com"
isRequired
errorMessage="Please enter a valid email"
/>
{/* With start/end content */}
<Input
type="text"
label="Price"
placeholder="0.00"
startContent={<span>$</span>}
endContent={<span>SGD</span>}
/>
{/* Variants */}
<Input variant="flat" />
<Input variant="bordered" />
<Input variant="faded" />
<Input variant="underlined" />
{/* Colors */}
<Input color="primary" />
<Input color="success" />
<Input color="danger" />
</>
);
}
3. Card Component
import { Card, CardHeader, CardBody, CardFooter, Button } from "@heroui/react";
export function CarCard({ car }: { car: Car }) {
return (
<Card className="max-w-md">
<CardHeader className="flex gap-3">
<div className="flex flex-col">
<p className="text-md font-bold">{car.make}</p>
<p className="text-small text-default-500">{car.model}</p>
</div>
</CardHeader>
<CardBody>
<p>Year: {car.year}</p>
<p>Price: ${car.price?.toLocaleString()}</p>
</CardBody>
<CardFooter>
<Button color="primary" variant="flat">
View Details
</Button>
</CardFooter>
</Card>
);
}
4. Modal Component
"use client";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
useDisclosure,
} from "@heroui/react";
export function CarDetailsModal({ car }: { car: Car }) {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<Button onPress={onOpen}>View Details</Button>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
{car.make} {car.model}
</ModalHeader>
<ModalBody>
<p>Year: {car.year}</p>
<p>Registration: {car.registrationDate}</p>
<p>Fuel Type: {car.fuelType}</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Save
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
}
5. Table Component
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
} from "@heroui/react";
export function CarTable({ cars }: { cars: Car[] }) {
return (
<Table aria-label="Car registrations table">
<TableHeader>
<TableColumn>MAKE</TableColumn>
<TableColumn>MODEL</TableColumn>
<TableColumn>YEAR</TableColumn>
<TableColumn>PRICE</TableColumn>
</TableHeader>
<TableBody>
{cars.map((car) => (
<TableRow key={car.id}>
<TableCell>{car.make}</TableCell>
<TableCell>{car.model}</TableCell>
<TableCell>{car.year}</TableCell>
<TableCell>${car.price?.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
6. Tabs Component
import { Tabs, Tab } from "@heroui/react";
export function DataTabs() {
return (
<Tabs aria-label="Data options">
<Tab key="cars" title="Car Registrations">
<CarRegistrationData />
</Tab>
<Tab key="coe" title="COE Results">
<COEBiddingData />
</Tab>
<Tab key="pqp" title="PQP Data">
<PQPData />
</Tab>
</Tabs>
);
}
7. Select Component
import { Select, SelectItem } from "@heroui/react";
export function CarMakeSelector() {
const makes = ["Toyota", "Honda", "BMW", "Mercedes"];
return (
<Select
label="Select car make"
placeholder="Choose a make"
className="max-w-xs"
>
{makes.map((make) => (
<SelectItem key={make} value={make}>
{make}
</SelectItem>
))}
</Select>
);
}
8. Dropdown Component
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
Button,
} from "@heroui/react";
export function ActionsDropdown() {
return (
<Dropdown>
<DropdownTrigger>
<Button variant="bordered">Actions</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Actions">
<DropdownItem key="view">View</DropdownItem>
<DropdownItem key="edit">Edit</DropdownItem>
<DropdownItem key="delete" className="text-danger" color="danger">
Delete
</DropdownItem>
</DropdownMenu>
</Dropdown>
);
}
Form Handling
Complete Form Example
"use client";
import { useState } from "react";
import { Input, Button, Textarea } from "@heroui/react";
import { createBlogPost } from "@/actions/blog";
export function CreatePostForm() {
const [isLoading, setIsLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setIsLoading(true);
const formData = new FormData(e.currentTarget);
const result = await createBlogPost(formData);
setIsLoading(false);
if (result.success) {
// Handle success
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<Input
name="title"
label="Post Title"
placeholder="Enter post title"
isRequired
/>
<Textarea
name="content"
label="Content"
placeholder="Enter post content"
minRows={5}
isRequired
/>
<Input
name="tags"
label="Tags"
placeholder="Enter tags separated by commas"
/>
<Button
type="submit"
color="primary"
isLoading={isLoading}
className="w-full"
>
Create Post
</Button>
</form>
);
}
Theming and Customization
Theme Provider Setup
// app/providers.tsx
"use client";
import { HeroUIProvider } from "@heroui/react";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<HeroUIProvider>
{children}
</HeroUIProvider>
);
}
// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Custom Colors via Tailwind
// tailwind.config.ts
import { heroui } from "@heroui/react";
export default {
plugins: [
heroui({
themes: {
light: {
colors: {
primary: {
DEFAULT: "#0070F3",
foreground: "#FFFFFF",
},
focus: "#0070F3",
},
},
},
}),
],
};
Component-Level Customization
import { Button } from "@heroui/react";
export function CustomButton() {
return (
<Button
className="bg-gradient-to-r from-blue-500 to-purple-500"
variant="shadow"
>
Custom Styled Button
</Button>
);
}
Accessibility
HeroUI components are built on React Aria for accessibility:
import { Button } from "@heroui/react";
export function AccessibleButton() {
return (
<Button
aria-label="Close dialog"
aria-describedby="close-description"
>
Close
</Button>
);
}
Responsive Design
import { Button } from "@heroui/react";
export function ResponsiveButton() {
return (
<Button
size={{
initial: "sm", // Mobile
sm: "md", // Tablet
lg: "lg", // Desktop
}}
className="w-full sm:w-auto"
>
Responsive Button
</Button>
);
}
Common Patterns
Loading States
"use client";
import { useState } from "react";
import { Button } from "@heroui/react";
export function SubmitButton() {
const [isLoading, setIsLoading] = useState(false);
async function handleClick() {
setIsLoading(true);
await performAction();
setIsLoading(false);
}
return (
<Button
onPress={handleClick}
isLoading={isLoading}
color="primary"
>
{isLoading ? "Processing..." : "Submit"}
</Button>
);
}
Controlled Inputs
"use client";
import { useState } from "react";
import { Input } from "@heroui/react";
export function ControlledInput() {
const [value, setValue] = useState("");
return (
<Input
value={value}
onValueChange={setValue}
label="Search"
placeholder="Type to search..."
/>
);
}
Testing HeroUI Components
// __tests__/components/car-card.test.tsx
import { render, screen } from "@testing-library/react";
import { HeroUIProvider } from "@heroui/react";
import CarCard from "../car-card";
function renderWithProvider(component: React.ReactElement) {
return render(
<HeroUIProvider>{component}</HeroUIProvider>
);
}
describe("CarCard", () => {
it("renders car information", () => {
const car = { make: "Toyota", model: "Camry", year: 2024 };
renderWithProvider(<CarCard car={car} />);
expect(screen.getByText("Toyota")).toBeInTheDocument();
expect(screen.getByText("Camry")).toBeInTheDocument();
});
});
Debugging Tips
- Missing Provider: Ensure HeroUIProvider wraps your app
- Styling Issues: Check Tailwind CSS configuration includes HeroUI plugin
- Type Errors: Import types from
@heroui/react - SSR Issues: Mark client components with
"use client"
References
- HeroUI Documentation: Use MCP tools for component info
- Related files:
apps/web/src/components/- React componentsapps/web/src/app/- Pages using HeroUIapps/web/tailwind.config.ts- Tailwind configurationapps/web/CLAUDE.md- Web app documentation
Best Practices
- Use MCP Tools: Query component info before implementing
- Consistent Variants: Use same variant across similar components
- Accessibility: Always add aria labels for icon-only buttons
- Responsive: Test components on mobile, tablet, desktop
- Provider: Ensure HeroUIProvider at root level
- TypeScript: Leverage TypeScript types for props
- Testing: Wrap tests in HeroUIProvider
- Performance: Use client components only when needed
- Size Utility: Use
size-*instead ofh-* w-*for equal dimensions (Tailwind v3.4+)
Size Utility Convention
When styling HeroUI components with equal height and width, use the size-* utility:
// ✅ Good - Use size-* for equal dimensions
<Button isIconOnly className="size-10">
<Icon className="size-4" />
</Button>
// ❌ Avoid - Redundant h-* and w-*
<Button isIconOnly className="h-10 w-10">
<Icon className="h-4 w-4" />
</Button>