はじめに
正直なところ、React用のUIライブラリは選択肢が多すぎて疲れていたんですよ。Material UI、Chakra UI、shadcn/ui...どれも良いライブラリなんですけど、「もう少し美しくて、でも軽量なものはないかな」と思っていました。
そんな時に出会ったのがNextUI。GitHubスター27,500以上、月間ダウンロード数90万近くという数字を見て、「これだけ支持されているなら試してみるか」と。
30代になって思うのは、人気があるツールには理由があるということ。NextUIを使ってみて、その理由がよくわかりました。
ちなみに最近、NextUIはHeroUIにリブランドされました。この記事では両方の名前を使いますが、同じライブラリです。
NextUI(現HeroUI)とは
NextUI(現HeroUI)は、「Beautiful, fast and modern React UI library」を掲げるReact用のUIコンポーネントライブラリです。
Tailwind CSSをベースにしつつ、内部的にはReact Ariaを使ってアクセシビリティを担保している。この組み合わせが絶妙なんですよ。
主な数字:
- GitHubスター: 27,500以上
- 月間ダウンロード: 67万〜95万
- コントリビューター: 272人
- コンポーネント数: 50以上
- ライセンス: MIT
5年前から開発が始まって、今も活発に更新されています。最終コミットは数時間前とか。この開発の活発さは、ライブラリ選定で重要なポイントですね。
特徴・メリット
1. とにかく見た目が美しい
これ、意外と重要なんですよ。デザイナーがいない個人開発や小規模チームだと、UIの見た目で悩むことが多い。NextUIのコンポーネントは最初から洗練されたデザインで、そのまま使っても「それっぽく」見えます。
デフォルトのスタイルが良いので、カスタマイズの手間が減る。時短になりますね。
2. Tailwind CSSベースで軽量
ランタイムスタイルがないので、パフォーマンスに優れています。使っていないスタイルはバンドルに含まれない。
バンドルサイズは圧縮後354KB程度。必要なコンポーネントだけ使えば、さらに軽くなります。
3. React Ariaで堅牢なアクセシビリティ
内部的にAdobe製のReact Ariaを使っているので、アクセシビリティがしっかり確保されています。
- キーボードナビゲーション対応
- スクリーンリーダー対応
- フォーカス管理
個人的には、アクセシビリティ対応を後から入れるのは本当に大変なので、最初から入っているのはQOL上がりますね。
4. TypeScriptファースト
全コンポーネントがTypeScriptで書かれていて、型推論がしっかり効く。propsの補完が効くのは、開発効率に直結します。
5. ダークモード対応が簡単
HTMLのtheme属性を変更するだけで、自動的にダークモードに切り替わります。next-themesとの相性も良い。
6. 豊富なアニメーション
Framer Motionとの統合が前提になっていて、コンポーネントの切り替えやホバー時のアニメーションが滑らか。追加設定なしで「気持ちいい」UIになります。
インストール方法
前提条件
- React 18以上
- Tailwind CSS v4
- Framer Motion 11.9以上
CLIを使った自動インストール(推奨)
# CLIをグローバルインストール
npm install -g heroui-cli
# 新規プロジェクト作成
heroui init my-heroui-app
テンプレートを選択できます:
- App(Next.js App Router)
- Pages(Next.js Pages Router)
- Vite
既存プロジェクトへの導入
# 依存関係をインストール
npm install @heroui/react framer-motion
Tailwind CSSの設定
// tailwind.config.ts
import { heroui } from "@heroui/react"
export default {
content: [
// ...
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
],
darkMode: "class",
plugins: [heroui()],
}
プロバイダーの設定
// 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,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
個別コンポーネントの追加
# CLIでコンポーネントを追加
heroui add button
heroui add card modal input
基本的な使い方
Buttonコンポーネント
import { Button } from "@heroui/react"
export function ButtonDemo() {
return (
<div className="flex gap-4">
<Button>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>
</div>
)
}
バリアントも豊富:
<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>
Cardコンポーネント
import { Card, CardHeader, CardBody, CardFooter, Button } from "@heroui/react"
export function CardDemo() {
return (
<Card className="max-w-[400px]">
<CardHeader className="flex gap-3">
<div className="flex flex-col">
<p className="text-md">プロジェクト名</p>
<p className="text-small text-default-500">説明文がここに入ります</p>
</div>
</CardHeader>
<CardBody>
<p>
カードの本文コンテンツ。プロジェクトの詳細情報などを
記載できます。
</p>
</CardBody>
<CardFooter>
<Button color="primary">詳細を見る</Button>
</CardFooter>
</Card>
)
}
Inputコンポーネント
import { Input } from "@heroui/react"
export function InputDemo() {
return (
<div className="flex w-full flex-wrap gap-4">
<Input
type="email"
label="メールアドレス"
placeholder="you@example.com"
/>
<Input
type="password"
label="パスワード"
placeholder="パスワードを入力"
/>
</div>
)
}
バリデーション付き:
import { Input } from "@heroui/react"
import { useState } from "react"
export function ValidatedInput() {
const [value, setValue] = useState("")
const isInvalid = value.length > 0 && !value.includes("@")
return (
<Input
type="email"
label="メールアドレス"
value={value}
onValueChange={setValue}
isInvalid={isInvalid}
errorMessage={isInvalid ? "有効なメールアドレスを入力してください" : ""}
/>
)
}
Modalコンポーネント
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
useDisclosure,
} from "@heroui/react"
export function ModalDemo() {
const { isOpen, onOpen, onOpenChange } = useDisclosure()
return (
<>
<Button onPress={onOpen}>モーダルを開く</Button>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader>確認</ModalHeader>
<ModalBody>
<p>この操作を実行してもよろしいですか?</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
キャンセル
</Button>
<Button color="primary" onPress={onClose}>
確認
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
)
}
Framer Motionのおかげで、モーダルの開閉アニメーションが滑らかなんですよね。
実践的なユースケース
ダークモード切り替え
"use client"
import { Switch } from "@heroui/react"
import { useTheme } from "next-themes"
import { SunIcon, MoonIcon } from "@heroui/shared-icons"
export function ThemeSwitch() {
const { theme, setTheme } = useTheme()
return (
<Switch
defaultSelected={theme === "dark"}
size="lg"
color="secondary"
startContent={<SunIcon />}
endContent={<MoonIcon />}
onValueChange={(isSelected) => setTheme(isSelected ? "dark" : "light")}
/>
)
}
ナビゲーションバー
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Link,
Button,
} from "@heroui/react"
export function NavigationBar() {
return (
<Navbar>
<NavbarBrand>
<p className="font-bold text-inherit">MyApp</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
機能
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link href="#" aria-current="page">
料金
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
会社概要
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">ログイン</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
無料で始める
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
)
}
データテーブル
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/react"
const users = [
{ id: 1, name: "田中太郎", role: "管理者", status: "有効" },
{ id: 2, name: "山田花子", role: "編集者", status: "有効" },
{ id: 3, name: "佐藤次郎", role: "閲覧者", status: "無効" },
]
export function UserTable() {
return (
<Table aria-label="ユーザー一覧">
<TableHeader>
<TableColumn>名前</TableColumn>
<TableColumn>役割</TableColumn>
<TableColumn>ステータス</TableColumn>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>{user.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
ソート、ページネーション、選択機能も簡単に追加できます。
テーマのカスタマイズ
// tailwind.config.ts
import { heroui } from "@heroui/react"
export default {
// ...
plugins: [
heroui({
themes: {
light: {
colors: {
primary: {
DEFAULT: "#006FEE",
foreground: "#FFFFFF",
},
focus: "#006FEE",
},
},
dark: {
colors: {
primary: {
DEFAULT: "#006FEE",
foreground: "#FFFFFF",
},
focus: "#006FEE",
},
},
// カスタムテーマも作れる
"brand-theme": {
extend: "dark",
colors: {
primary: {
DEFAULT: "#7C3AED",
foreground: "#FFFFFF",
},
},
},
},
}),
],
}
shadcn/uiとの違い
よく比較されるshadcn/uiとの違いについて。
| 項目 | NextUI / HeroUI | shadcn/ui |
|---|---|---|
| 導入方法 | npmパッケージ | コードをコピー |
| アニメーション | Framer Motion統合 | 追加設定が必要 |
| コンポーネント所有権 | ライブラリが管理 | 自分のコード |
| アップデート | 自動 | 手動 |
| デザイン | モダン・洗練 | シンプル・ミニマル |
個人的には、「すぐに使いたい」ならNextUI、「細かくカスタマイズしたい」ならshadcn/uiという使い分けかなと。
まとめ
NextUI(現HeroUI)を使ってみて感じたこと:
- 見た目: デフォルトで美しい。デザイナーなしでも「それっぽい」UIが作れる
- 開発体験: TypeScript対応でprops補完が効く。ドキュメントも充実
- パフォーマンス: Tailwind CSSベースで軽量。ランタイムスタイルなし
- アクセシビリティ: React Ariaのおかげで最初から対応済み
- アニメーション: Framer Motion統合で滑らかな動き
30代になって思うのは、ツール選びで「見た目の良さ」も重要だということ。毎日使うものだし、モチベーションに影響する。その点で、NextUIは開発していて楽しいライブラリですね。
特にNext.jsやViteでReactプロジェクトを立ち上げる時、UIライブラリで迷ったら候補に入れてみてください。CLIで初期化すれば5分で動くものが作れます。
