Builds UI components using HeroUI v3 with React 19 and Tailwind CSS v4. Use when creating or modifying frontend components, forms, tables, or layouts in the UniManage frontend.
This skill guides you through building UI components using HeroUI v3 for the UniManage frontend application.
@heroui/react@beta)Always import from @heroui/react:
// ✅ Correct
import { Button, Card, CardBody, Input } from "@heroui/react";
// ❌ Wrong - Old NextUI
import { Button } from "@nextui-org/react";
HeroUI uses React Aria naming conventions:
// ✅ Use these props
<Button
isDisabled={false}
onPress={() => {}}
isLoading={false}
>
// ❌ Not these
<Button
disabled={false}
onClick={() => {}}
loading={false}
>
Common prop patterns:
isDisabled not disabledonPress not onClick (better touch support)isOpen / defaultOpen for controlled/uncontrolled stateisRequired not requiredisReadOnly not readonlyHeroUI components can be styled in 3 ways:
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">Custom Button</Button>
<Button
color="primary" // primary, secondary, success, warning, danger, default
variant="shadow" // solid, bordered, light, flat, faded, shadow, ghost
size="lg" // sm, md, lg
radius="full" // none, sm, md, lg, full
>
Styled Button
</Button>
<Table
classNames={{
wrapper: "min-h-[400px]",
th: "bg-primary text-white",
td: "text-sm",
}}
>
{/* Table content */}
</Table>
import { Card, CardHeader, CardBody, CardFooter, Divider, Spacer } from "@heroui/react";
<Card className="max-w-md">
<CardHeader>
<h4>Card Title</h4>
</CardHeader>
<Divider />
<CardBody>
<p>Content goes here</p>
</CardBody>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>;
import { Input, Textarea, Select, SelectItem, Checkbox } from '@heroui/react';
// Input with validation
<Input
label="Email"
type="email"
isRequired
errorMessage="Please enter a valid email"
isInvalid={!!errors.email}
/>
// Select
<Select
label="Department"
placeholder="Select department"
>
<SelectItem key="hr" value="hr">Human Resources</SelectItem>
<SelectItem key="it" value="it">Information Technology</SelectItem>
</Select>
// Checkbox
<Checkbox isSelected={isChecked} onValueChange={setIsChecked}>
I agree to the terms
</Checkbox>
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/react";
<Table aria-label="User list">
<TableHeader>
<TableColumn>NAME</TableColumn>
<TableColumn>EMAIL</TableColumn>
<TableColumn>STATUS</TableColumn>
</TableHeader>
<TableBody>
<TableRow key="1">
<TableCell>John Doe</TableCell>
<TableCell>[email protected]</TableCell>
<TableCell>Active</TableCell>
</TableRow>
</TableBody>
</Table>;
import { Button, Link, Tabs, Tab, Breadcrumbs, BreadcrumbItem } from '@heroui/react';
// Button
<Button onPress={handleClick} color="primary">
Save Changes
</Button>
// Link
<Link href="/dashboard" color="primary">
Go to Dashboard
</Link>
// Tabs
<Tabs>
<Tab key="overview" title="Overview">
Overview content
</Tab>
<Tab key="settings" title="Settings">
Settings content
</Tab>
</Tabs>
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner, Tooltip } from '@heroui/react';
// Modal
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader>Confirm Action</ModalHeader>
<ModalBody>
Are you sure you want to proceed?
</ModalBody>
<ModalFooter>
<Button onPress={onClose}>Cancel</Button>
<Button color="primary" onPress={handleConfirm}>Confirm</Button>
</ModalFooter>
</ModalContent>
</Modal>
// Spinner
<Spinner size="lg" color="primary" />
// Tooltip
<Tooltip content="This is helpful information">
<Button>Hover me</Button>
</Tooltip>
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Input, Button } from "@heroui/react";
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
{...register("email")}
label="Email"
type="email"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
/>
<Input
{...register("password")}
label="Password"
type="password"
isInvalid={!!errors.password}
errorMessage={errors.password?.message}
/>
<Button type="submit" color="primary" fullWidth>
Login
</Button>
</form>
);
}
import { useQuery } from "@tanstack/react-query";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
Spinner,
} from "@heroui/react";
function UserTable() {
const { data, isLoading } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
});
if (isLoading) {
return <Spinner size="lg" />;
}
return (
<Table aria-label="Users">
<TableHeader>
<TableColumn>NAME</TableColumn>
<TableColumn>EMAIL</TableColumn>
<TableColumn>ACTIONS</TableColumn>
</TableHeader>
<TableBody>
{data?.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Button size="sm" onPress={() => handleEdit(user.id)}>
Edit
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
HeroUI uses next-themes for dark mode:
import { useTheme } from "next-themes";
import { Button } from "@heroui/react";
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button onPress={() => setTheme(theme === "dark" ? "light" : "dark")} variant="light">
Toggle Theme
</Button>
);
}
✅ Every component should have:
aria-label, aria-labelledby)✅ For forms:
isRequirederrorMessage prop for validation feedback✅ For interactive elements:
<button>, <a>)onPress for better touch supportSolution: Check app/globals.css has correct import order:
@import "tailwindcss"; /* Must be first */
@import "@heroui/styles";
Solution: Verify you're importing from @heroui/react@beta:
npm list @heroui/react
# Should show: @heroui/[email protected]
Solution: Ensure React 19 types are installed:
npm install --save-dev @types/react@19 @types/react-dom@19
Solution: Check next-themes provider is configured in app/providers.tsx:
import { ThemeProvider } from "next-themes";
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>;
components/
├── common/ # Reusable UI components
│ ├── Button.tsx
│ ├── DataTable.tsx
│ └── Input.tsx
├── layout/ # Layout components
│ ├── Sidebar.tsx
│ ├── Header.tsx
│ └── Footer.tsx
└── features/ # Feature-specific components
├── users/
│ ├── UserForm.tsx
│ └── UserTable.tsx
└── auth/
└── LoginForm.tsx
When creating a new component:
Is it a standard UI element?
Does it need custom styling?
classNames slotsIs it a form?
Does it need data fetching?
Does it need accessibility beyond defaults?
// Button
<Button color="primary" variant="solid" size="md" radius="md" isDisabled={false} isLoading={false} />
// Input
<Input label="Name" type="text" isRequired isReadOnly={false} isInvalid={false} errorMessage="" />
// Card
<Card shadow="sm" radius="lg" isPressable isBlurred />
// Modal
<Modal isOpen={false} size="md" backdrop="opaque" scrollBehavior="inside" />
primary - Main brand colorsecondary - Secondary brand colorsuccess - Greenwarning - Yellow/Orangedanger - Reddefault - Graysm - Smallmd - Medium (default)lg - Largesolid - Filled backgroundbordered - Outline onlylight - Light backgroundflat - No shadowfaded - Faded backgroundshadow - With shadowghost - Minimal styling