📌

shadcn/uiを使って簡単なWebページを作ってみた

2023/12/13に公開

はじめに

こんにちは、D2Cのフロントエンドエンジニアをやっている廣瀬です。
私が担当しているプロジェクトでは、ViteReactMUIを主に使用しています。

最近、フロントエンド系の技術を調査してみたところ、shadcn/uiというものを見つけました。
今回、このshadcn/uiViteで試してみましたので、その内容を記事にしたいと思います。
今回の記事では簡単なWebページを作ってみたいと思います。

shadcn/uiとは?

Radix UITailwind CSSをベースに開発された、比較的低レイヤーなUIコンポーネントの集まりです。
ここで注意なのが、shadcn/uiMUIChakraUIのようなUIライブラリではなく、npmパッケージとしては提供されていません。ですので、依存関係としてインストールすることはできません。

環境の準備

実際に、以下のような手順でshadcn/uiを利用できるよう環境を作ります

プロジェクトの立ち上げ

$ yarn create vite
? Project name: > practice-shadcnui
? Select a framework: > React
? Select a variant: > TypeScript + SWC

Tailwindをインストールし、設定ファイルを生成

$ cd practice-shadcnui
$ yarn
$ yarn add --dev tailwindcss postcss autoprefixer
$ yarn tailwindcss init -p

tsconfig.jsonを編集

{
    "compilerOptions": {
        ...
        ...
	↓追記
        "baseUrl": ".",
        "paths": {
            "@/*": ["./src/*"]
        },
        
        ...
        ...
    }
}

@types/nodeをインストールし、vite.config.tsを編集

$ yarn add --dev @types/node
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    resolve: {
        alias: {
            "@": path.resolve(__dirname, "./src"),
        },
    },
});

shadcn-uiのCLIを追加し、components.jsonを設定

以下の操作を行うことによって、shadcn/uiが用意するコンポーネントをyarnコマンドのCLIで追加することができます。

$ yarn add shadcn-ui

以下のコマンドを打つと、いくつか質問されるので、以下の通りに回答していく

$ yarn shadcn-ui init

? Would you like to use TypeScript (recommended)? > yes
? Which style would you like to use? > Default
? Which color would you like to use as base color? > Slate
? Where is your global CSS file? > src/index.css
? Would you like to use CSS variables for colors > yes
? Where is your tailwind.config.js located? > tailwind.config.js
? Configure the import alias for components: > @/components
? Configure the import alias for utils: > @/lib/utils
? Are you using React Server Components? > no
? Write cofiguration to components.json. Procced? > yキーを押下

これで、 shadcn/uiを利用できる環境の構築が完了しました!

プロジェクトディレクトリ配下を確認してみると、
components.jsoncomponents/lib/utils.tsが生成されていることを確認できるかと思います。

practice-shadcnui/
├── src/
│  ├── components/
│  └─── lib/
│        └── utils.ts
└─── components.json

開発

上の手順で、開発環境は構築できましたので
早速、shadcn/uiを使用して開発に入っていきましょう!

ヘッダーの実装

このセクションでは、shadcn/uiが用意しているbuttonを使用するので、以下のコマンドで追加していきます。

$ yarn shadcn-ui add button

src/components/配下を確認してみるとui/ディレクトリが生成され、さらにその配下にbutton.tsxが生成されていると思います。このように、インストールしたコンポーネントは自動的にui/配下に生成されていきます。

src/
└─ components/
    └─ ui/
        └─ button.tsx

それでは、src/components/配下にヘッダーコンポーネントを作成していきましょう。

$ mkdir src/components/Header && touch src/components/Header/index.tsx
// Header/index.tsx
import { Button } from "@/components/ui/button";

export const Header = () => {
    return (
        <div className="fixed flex justify-between px-8 w-screen h-16 bg-teal-400 items-center drop-shadow-2xl border-b border-gray-300 shadow-md">
            <h1 className="font-bold text-2xl">shadcn-ui TUTORIAL</h1>
            <div className="flex gap-3">
                <Button variant="outline">
                    <a href="https://ui.shadcn.com/docs">公式 Document</a>
                </Button>
                <Button>menu</Button>
            </div>
        </div>
    );
};
// App.tsx
import { Header } from "@/components/Header";

function App() {
    return (
        <div>
            <Header />
        </div>
    );
}

export default App;

結果↓

修正

しかし、このままでは「公式 Document」のボタンのフォントが少し細い感じがしますね。(個人的に)
なので、少しbutton.tsxを直接編集してみましょう。

// src/components/ui/button.tsx
...
const buttonVariants = cva(
    "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
    {
        variants: {
            variant: {
                default: "bg-primary text-primary-foreground hover:bg-primary/90",
                destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
                // ↓ outlineに'font-bold'を追記!
                outline:
                    "border border-input bg-background hover:bg-accent hover:text-accent-foreground font-bold",
                secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
                ghost: "hover:bg-accent hover:text-accent-foreground",
                link: "text-primary underline-offset-4 hover:underline",
            },
            size: {
                default: "h-10 px-4 py-2",
                sm: "h-9 rounded-md px-3",
                lg: "h-11 rounded-md px-8",
                icon: "h-10 w-10",
            },
        },
        defaultVariants: {
            variant: "default",
            size: "default",
        },
    }
);

...

これで、variantoutlineに指定したButtonコンポーネントすべてのフォントは太字になります。

結果(修正後)↓

カードの実装

このセクションでは、shadcn/uiが用意しているcardを使用するので、以下のコマンドで追加していきます。

$ yarn shadcn-ui add card

それでは、src/components/配下にカードコンポーネントを作成していきましょう。

$ mkdir src/components/CardDemo && touch src/components/CardDemo/index.tsx
// CardDemo/index.tsx
import {
    Card,
    CardHeader,
    CardTitle,
    CardDescription,
    CardContent,
    CardFooter,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { VFC } from "react";

type Props = {
    cardTitle: string;
    cardDescription: string;
    cardContent: string;
    cardFooter: string;
};

export const CardDemo: VFC<Props> = (props) => {
    return (
        <Card>
            <CardHeader className="h-32">
                <CardTitle>{props.cardTitle}</CardTitle>
                <CardDescription>{props.cardDescription}</CardDescription>
            </CardHeader>
            <CardContent>
                <Button variant="outline" className="border-solid border-2 border-gray-700">
                    <a href={props.cardContent} target="_blank">
                        Usage {props.cardTitle}
                    </a>
                </Button>
            </CardContent>
            <CardFooter>
                <p>{props.cardFooter}</p>
            </CardFooter>
        </Card>
    );
};

次に、複数のカードを用意するために、それっぽいデータを他で用意しましょう。

$ mkdir src/components/constants && touch src/components/constants/data.ts
// constants/data.ts
export const cardData = [
    {
        title: "Accordion",
        description:
            "A vertically stacked set of interactive headings that each reveal a section of content.",
        content: "https://ui.shadcn.com/docs/components/accordion",
        footer: "shadcn/ui",
    },
    {
        title: "Alert",
        description: "Displays a callout for user attention.",
        content: "https://ui.shadcn.com/docs/components/alert",
        footer: "shadcn/ui",
    },
    {
        title: "Avatar",
        description: "An image element with a fallback for representing the user.",
        content: "https://ui.shadcn.com/docs/components/avatar",
        footer: "shadcn/ui",
    },
    {
        title: "Badge",
        description: "Displays a badge or a component that looks like a badge.",
        content: "https://ui.shadcn.com/docs/components/badge",
        footer: "shadcn/ui",
    },
    {
        title: "Button",
        description: "Displays a button or a component that looks like a button.",
        content: "https://ui.shadcn.com/docs/components/button",
        footer: "shadcn/ui",
    },
    {
        title: "Card",
        description: "Displays a card with header, content, and footer.",
        content: "https://ui.shadcn.com/docs/components/card",
        footer: "shadcn/ui",
    },
    {
        title: "Checkbox",
        description: "A control that allows the user to toggle between checked and not checked.",
        content: "https://ui.shadcn.com/docs/components/checkbox",
        footer: "shadcn/ui",
    },
    {
        title: "Collapsible",
        description: "An interactive component which expands/collapses a panel.",
        content: "https://ui.shadcn.com/docs/components/collapsible",
        footer: "shadcn/ui",
    },
    {
        title: "Date Picker",
        description: "A date picker component with range and presets.",
        content: "https://ui.shadcn.com/docs/components/date-picker",
        footer: "shadcn/ui",
    },
];
// App.tsx
import { Header } from "@/components/Header";
import { CardDemo } from "@/components/CardDemo";

import { cardData } from "@/constants/data";

function App() {
    return (
        <div>
            <Header />
            <section className="container flex pt-32 grid grid-cols-2 gap-10 xl:grid-cols-3">
                {cardData.map((data) => (
                    <CardDemo
                        cardTitle={data.title}
                        cardDescription={data.description}
                        cardContent={data.content}
                        cardFooter={data.footer}
                    />
                ))}
            </section>
        </div>
    );
}

export default App;

結果↓

修正

しかし、これではカードの枠が少し寂しい感じがします。
これもcard.tsxを直接編集してみます。

...
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
    ({ className, ...props }, ref) => (
        <div
            ref={ref}
            className={cn(
                // shadow-smを'shadow-2xl'に変更!
                "rounded-lg border bg-card text-card-foreground shadow-2xl w-96",
                className
            )}
            {...props}
        />
    )
);
Card.displayName = "Card";
...

結果(修正後)↓

タブの実装

このセクションでは、shadcn/uiが用意しているtabs,input,textareaを使用するので、追加していきます。

$ yarn shadcn-ui add tabs input textarea

それでは、src/components/配下にタブコンポーネントを実装していきましょう。

$ mkdir src/components/TabsDemo && touch src/components/TabsDemo/index.tsx
// TabsDemo/index.tsx
import { Button } from "@/components/ui/button";
import {
    Card,
    CardContent,
    CardDescription,
    CardFooter,
    CardHeader,
    CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";

export const TabsDemo = () => {
    return (
        <Tabs defaultValue="about" className="w-[800px] h-[400px]">
            <TabsList className="w-full">
                <TabsTrigger value="about" className="w-1/2">
                    About
                </TabsTrigger>
                <TabsTrigger value="contact" className="w-1/2">
                    Contact
                </TabsTrigger>
            </TabsList>
            <TabsContent value="about" className="w-full h-full">
                <Card className="w-full h-full flex flex-col justify-between">
                    <CardHeader className="text-center">
                        <CardTitle>About</CardTitle>
                        <CardDescription>
                            Dolor voluptatibus eum dolores blanditiis cumque eaque! Laboriosam neque
                            illum ab tempore quae sapiente? Culpa repellat facilis accusamus maiores
                            quibusdam consectetur quidem expedita Deleniti tempore voluptates
                            aliquid perferendis incidunt! Rem.
                        </CardDescription>
                    </CardHeader>
                    <CardContent className="w-full flex items-center justify-center">
                        <div className="w-2/3">
                            <div className="">
                                <div>
                                    <strong className="text-2xl">
                                        Consectetur eveniet magnam debitis dolorum iste Quam
                                        sequiquisquam
                                    </strong>
                                    <br />
                                    <br />
                                    doloribus sed eos In quod sunt delectus voluptatibus a
                                </div>
                            </div>
                            <div className="">
                                <div>
                                    Consectetur exercitationem asperiores nihil vel autem Explicabo
                                    ipsa corrupti vitae accusantium nam modi, repellat. Aliquid
                                    temporibus
                                    <br />
                                    <strong className="text-md">
                                        consectetur neque fugit quasi Cupiditate aliquam hic
                                    </strong>
                                </div>
                            </div>
                        </div>
                    </CardContent>
                    <CardFooter className="flex justify-end">
                        <Button>Detail</Button>
                    </CardFooter>
                </Card>
            </TabsContent>
            <TabsContent value="contact" className="w-full h-full">
                <Card className="w-full h-full flex flex-col justify-between">
                    <CardHeader className="text-center">
                        <CardTitle>Contact</CardTitle>
                        <CardDescription>
                            Sit omnis libero facere rem reprehenderit Quis quasi dolor itaque
                            blanditiis repellendus? Explicabo beatae numquam eum unde deserunt
                            voluptates perferendis totam modi sint libero. Laborum sint nihil
                            corporis aliquid delectus.
                        </CardDescription>
                    </CardHeader>
                    <CardContent className="w-full flex items-center justify-center">
                        <div className="w-2/3">
                            <div className="">
                                <div className="w-full flex flex-col items-center">
                                    <Input
                                        type="text"
                                        placeholder="Title"
                                        className="transition duration-500 w-[400px] mb-4"
                                    />
                                    <Textarea
                                        className="transition duration-500 w-[600px] h-[150px]"
                                        placeholder="detail"
                                    />
                                </div>
                            </div>
                        </div>
                    </CardContent>
                    <CardFooter className="flex justify-end">
                        <Button>Send</Button>
                    </CardFooter>
                </Card>
            </TabsContent>
        </Tabs>
    );
};

結果↓

最終調整

最終的に軽くTop部分も作り、調整しました。
ディレクトリ構成とApp.tsxは以下のようになりました。

src
├── App.css
├── App.tsx
├── assets
│   └── react.svg
├── components
│   ├── CardDemo
│   │   └── index.tsx
│   ├── Header
│   │   └── index.tsx
│   ├── TabsDemo
│   │   └── index.tsx
│   ├── Top
│   │   └── index.tsx
│   └── ui
│       ├── button.tsx
│       ├── card.tsx
│       ├── input.tsx
│       ├── tabs.tsx
│       └── textarea.tsx
├── constants
│   └── data.ts
├── index.css
├── lib
│   └── utils.ts
├── main.tsx
└── vite-env.d.ts
// App.tsx
import { Header } from "@/components/Header";
import { CardDemo } from "@/components/CardDemo";
import { TabsDemo } from "@/components/TabsDemo";
import { Top } from "@/components/Top";

import { cardData } from "@/constants/data";

function App() {
    return (
        <div>
            <Header />
            <section className="pt-16 w-full container">
                <Top />
            </section>
            <section className="container flex pt-32 pb-32 grid grid-cols-2 gap-10 xl:grid-cols-3">
                {cardData.map((data) => (
                    <CardDemo
                        cardTitle={data.title}
                        cardDescription={data.description}
                        cardContent={data.content}
                        cardFooter={data.footer}
                    />
                ))}
            </section>
            <section className="pt-[120px] pb-[200px] flex justify-center bg-gray-900">
                <TabsDemo />
            </section>
        </div>
    );
}

まとめ&感想

shadcn/uiは比較的低レイヤーなUIコンポーネントを揃えています。
また、スタイリングのベースがTailwindであるため、Tailwindにあまり抵抗感がなく、操作に慣れている方にとっては、カスタマイズしやすい且つ柔軟性が非常に高く感じられるのではと思いました。

終わりに

今回は簡単なWebページを作ってみましたが、これからしっかりしたものも作ってみたりして、学習を続けていきたいと思います。
また、shadcn/uiの公式ドキュメントは比較的シンプルで読みやすいものになっていると思いますので、ご興味のある方はぜひ読んでみると面白いかもしれません。

最後までお読みいただきありがとうございました。

参考

https://ui.shadcn.com/docs
https://zenn.dev/mottox2/articles/react-shadcn-ui
https://reffect.co.jp/react/shadcn-react

D2C m-tech

Discussion