こんにちは、D2Cのフロントエンドエンジニアをやっている廣瀬です。
私が担当しているプロジェクトでは、
Vite
・
React
・
MUI
を主に使用しています。
最近、フロントエンド系の技術を調査してみたところ、
shadcn/ui
というものを見つけました。
今回、この
shadcn/ui
を
Vite
で試してみましたので、その内容を記事にしたいと思います。
今回の記事では簡単なWebページを作ってみたいと思います。
Radix UI
と
Tailwind CSS
をベースに開発された、比較的低レイヤーなUIコンポーネントの集まりです。
ここで注意なのが、
shadcn/ui
は
MUI
や
ChakraUI
のような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.json
やcomponents/
、lib/utils.ts
が生成されていることを確認できるかと思います。practice-shadcnui/ ├── src/ │ ├── components/ │ └─── lib/ │ └── utils.ts └─── components.json
上の手順で、開発環境は構築できましたので
早速、shadcn/ui
を使用して開発に入っていきましょう!ヘッダーの実装
このセクションでは、
shadcn/ui
が用意しているbutton
を使用するので、以下のコマンドで追加していきます。$ yarn shadcn-ui add button
└─ components/ └─ ui/ └─ button.tsx
src/components/
配下を確認してみるとui/
ディレクトリが生成され、さらにその配下にbutton.tsx
が生成されていると思います。このように、インストールしたコンポーネントは自動的にui/
配下に生成されていきます。それでは、
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 ( <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",
これで、
variant
をoutline
に指定した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 ( <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} </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 ( <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;
しかし、これではカードの枠が少し寂しい感じがします。
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( ({ className, ...props }, ref) => ( 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";
これもcard.tsx
を直接編集してみます。結果(修正後)↓ タブの実装
このセクションでは、
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=""> <strong className="text-2xl"> Consectetur eveniet magnam debitis dolorum iste Quam sequiquisquam </strong> doloribus sed eos In quod sunt delectus voluptatibus a </div> </div> <div className=""> Consectetur exercitationem asperiores nihil vel autem Explicabo ipsa corrupti vitae accusantium nam modi, repellat. Aliquid temporibus <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.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は以下のようになりました。// 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 ( <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