shadcn/ui入門(Next.js)
はじめに
これまで私は勉強のために自身でUIを構築することが多かったですが、最近名前を聞くことが多いshadcn/uiをはじめて触ってみました。忘備録、他の方のためになればと勉強した内容をまとめたものが本記事となります。
shadcn/uiとは
shadcn/uiはReact向けのコンポーネントコレクションであり、UIをシンプルかつ柔軟に構築することを目的にしています。
一番大きな特徴としてはコンポーネントライブラリではないということです。
世の中にはMaterial-UI、Chakra UIなど有名なコンポーネントライブラリが多くあります。
これらを使用する際は、たくさんのコンポーネントを含んだパッケージを開発環境にインストールし、その中から使用するコンポーネントを使用します。
一方でshadcn/uiはコンポーネントごとにインストールを行うことができ、面倒な手間を省くことができます。
また、カスタマイズがしやすいという特徴もあります。
CSSにはTaiwindCSSを採用しており、コンポーネントごとに高い自由度でデザインの変更を行うことができます。
環境構築
プロジェクトを作成します。
npx create-next-app@latest
セットアップを行います。
npx shadcn@latest init
ベースカラーについての質問が現れます。今回はNeutralを選択します。
React19を使用している場合、依存関係についての質問が現れます。今回はUse --legacy-peer-depsを選択します。
src>lib>utils.tsとcomponents.jsonが自動生成されます。
コンポーネントを触ってみる
インストール
ボタンコンポーネントをインストールします。
npx shadcn@latest add button
src>components>ui>button.tsxが自動生成されます。
呼び出し
page.tsxでコンポーネントを呼び出してみます。
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center">
<Button>Button</Button>
</div>
);
}
正常に呼び出せていることがわかります。
コンポーネントの中身
自動生成されたbutton.tsxの中身を見ていきます。
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
variants
variantsの中にはvariant、sizeのオブジェクトが入っています。
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
variant
varianはあらかじめshadcn/uiが用意してくれているスタイルになります。すべて見てみます。
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center flex-col">
<Button variant={"default"}>Button</Button>
<Button variant={"destructive"}>Button</Button>
<Button variant={"outline"}>Button</Button>
<Button variant={"secondary"}>Button</Button>
<Button variant={"ghost"}>Button</Button>
<Button variant={"link"}>Button</Button>
</div>
);
}
size
sizeはあらかじめshadcn/uiが用意してくれているサイズに関するスタイルになります。すべて見てみます。
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center flex-col">
<Button variant={"default"} size={"sm"}>Button</Button>
<Button variant={"default"} size={"default"}>Button</Button>
<Button variant={"default"} size={"lg"}>Button</Button>
</div>
);
}
defaultVariants
varinatとsizeのどの設定をデフォルトとするか設定することができます。
defaultVariants: {
variant: "default",
size: "default",
},
className
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
classNameを受け取ることができるように設定されています。
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center flex-col">
<Button className="bg-red-950 text-red-300 my-1">Button</Button>
<Button className="bg-blue-950 text-blue-300 my-1">Button</Button>
<Button className="bg-green-950 text-green-300 my-1">Button</Button>
</div>
);
}
Slot
button.tsxにはSlotがインポートされています。
import { Slot } from "@radix-ui/react-slot"
ドキュメントを確認すると、直近の子要素にpropsをマージするという説明があります。
また、buttonコンポーネントを見てみるとasChildがtrueの場合のみSlotが利用されていることがわかります。
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
機能を確認するためにpage.tsxを変更してデベロッパーツールで要素を確認します。
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center flex-col">
<Button>
<Link href="/">Button</Link>
</Button>
</div>
);
}
次にButtonコンポーネントにasChildを追加してみます。
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center flex-col">
<Button asChild>
<Link href="/">Button</Link>
</Button>
</div>
);
}
asChildがない場合はbuttonタグにclassが適用されており、その中にaタグがあります。
asChildがある場合はbuttonタグがなくなっており、aタグにclassが適用されています。
asCbildを設定することで、見た目には影響を与えずに親要素のbuttonでなく子要素のaとして表示することができるとわかりました。
cva
ドキュメントを見てみると利用方法が書いてあります。
第一引数に共通のcss、第二引数にoptionsを設定することができます。
cvaのおかげで、Buttonコンポーネントにvariant、sizeプロップスを渡すことでスタイルの変更が可能になっています。
カスタマイズ
...
red: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 text-red-100 bg-red-950",
blue: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 text-blue-100 bg-blue-950",
green:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 text-green-100 bg-green-950",
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="h-screen flex items-center justify-center flex-col">
<Button variant={"red"}>Button</Button>
<Button variant={"blue"}>Button</Button>
<Button variant={"green"}>Button</Button>
</div>
);
}
テーマ
shadcn/uiにはダークモードなどに変更するための機能があります。
ダークモードとライトモードの実装をしていきます。
next-themeをインストールします。
npm install next-themes
プロバイダーを作成します。
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
layout.tsxでルートレイアウトをラップします。
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme/ThemeProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
モード切替ボタンを作成します。
"use client";
import { Button } from "../ui/button";
import { useTheme } from "next-themes";
const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
const handleClick = () => {
if (theme === "light") setTheme("dark");
if (theme === "dark") setTheme("light");
};
return (
<div>
<Button onClick={handleClick}>DarkTheme</Button>
</div>
);
};
export default ThemeToggle;
ライトモードとダークモードの切り替えができるようになりました。
参考文献
まとめ
今回ははじめてshadcn/uiを触りましたが、簡単にUIを構築できるので今後も間違いなく使っていくことになりそうです。用意してくれているコンポーネントの種類も豊富なので今後使用していく中で慣れていきたいと思います。
Discussion