shadcn/ui
TailwindCSS
デザインシステム

shadcn/uiとTailwindCSSでモダンなUIを構築

shadcn/uiとTailwindCSSを使って、美しく再利用可能なUIコンポーネントを構築する方法を解説します。

著者: Tech Blog 編集部
2024-11-18
8分

shadcn/uiとは?

shadcn/uiは、コピー&ペーストで使える高品質なReactコンポーネント集です。通常のコンポーネントライブラリとは異なり、コンポーネントのソースコードをプロジェクトに直接追加します。

特徴

  • 所有権: コードが自分のプロジェクトに含まれるため、完全にカスタマイズ可能
  • 依存関係の軽減: 必要なコンポーネントだけを追加
  • TailwindCSS: ユーティリティファーストのスタイリング
  • アクセシビリティ: Radix UIを基盤として構築
  • TypeScript: 完全な型サポート

TailwindCSSのセットアップ

1. インストール

npm install -D tailwindcss @astrojs/tailwind
npx astro add tailwind

2. 設定ファイル

// tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
export default {
  darkMode: ["class"],
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}'],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        // ... 他の色定義
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

3. グローバルCSS

/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    /* ... 他のCSS変数 */
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    /* ... ダークモードの色 */
  }
}

shadcn/uiコンポーネントの追加

ユーティリティ関数

// src/lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Buttonコンポーネント

// src/components/ui/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 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: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        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",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

Cardコンポーネント

// src/components/ui/card.tsx
import * as React from "react"
import { cn } from "@/lib/utils"

const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn(
        "rounded-lg border bg-card text-card-foreground shadow-sm",
        className
      )}
      {...props}
    />
  )
)
Card.displayName = "Card"

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn("flex flex-col space-y-1.5 p-6", className)}
      {...props}
    />
  )
)
CardHeader.displayName = "CardHeader"

// ... 他のカードコンポーネント

export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }

実際の使用例

ブログカードの実装

// src/components/BlogCard.tsx
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Calendar, Clock } from 'lucide-react';

interface BlogCardProps {
  title: string;
  description: string;
  date: string;
  readTime: string;
  tags: string[];
  slug: string;
}

export function BlogCard({ title, description, date, readTime, tags, slug }: BlogCardProps) {
  return (
    <Card className="h-full flex flex-col hover:shadow-lg transition-shadow">
      <CardHeader>
        <div className="flex gap-2 mb-2 flex-wrap">
          {tags.map((tag) => (
            <Badge key={tag} variant="secondary">
              {tag}
            </Badge>
          ))}
        </div>
        <CardTitle className="line-clamp-2">{title}</CardTitle>
        <CardDescription className="line-clamp-2">{description}</CardDescription>
      </CardHeader>
      <CardContent className="flex-grow">
        <div className="flex items-center gap-4 text-sm text-muted-foreground">
          <div className="flex items-center gap-1">
            <Calendar className="h-4 w-4" />
            <span>{date}</span>
          </div>
          <div className="flex items-center gap-1">
            <Clock className="h-4 w-4" />
            <span>{readTime}</span>
          </div>
        </div>
      </CardContent>
      <CardFooter>
        <Button asChild variant="outline" className="w-full">
          <a href={`/blog/${slug}`}>続きを読む</a>
        </Button>
      </CardFooter>
    </Card>
  );
}

Astroページでの使用

---
import Layout from '../layouts/Layout.astro';
import { BlogCard } from '../components/BlogCard';

const posts = await Astro.glob('../content/blog/*.md');
---

<Layout title="ブログ">
  <div class="container mx-auto px-4 py-16">
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {posts.map((post) => (
        <BlogCard
          client:idle
          title={post.frontmatter.title}
          description={post.frontmatter.description}
          date={post.frontmatter.date}
          readTime={post.frontmatter.readTime}
          tags={post.frontmatter.tags}
          slug={post.frontmatter.slug}
        />
      ))}
    </div>
  </div>
</Layout>

カスタマイズのヒント

1. カラーテーマの変更

CSS変数を変更することで、簡単にテーマをカスタマイズできます:

:root {
  --primary: 200 100% 50%; /* 青系 */
  --primary: 340 75% 55%;  /* ピンク系 */
  --primary: 142 76% 36%;  /* 緑系 */
}

2. ボーダー半径の調整

:root {
  --radius: 0.5rem; /* デフォルト */
  --radius: 0rem;   /* シャープなデザイン */
  --radius: 1rem;   /* より丸みを帯びたデザイン */
}

3. コンポーネントのバリアント追加

const buttonVariants = cva(
  // ... base classes
  {
    variants: {
      variant: {
        // ... existing variants
        gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
      },
    },
  }
)

まとめ

shadcn/uiとTailwindCSSの組み合わせにより:

  • 美しく一貫性のあるUI
  • 完全にカスタマイズ可能
  • 型安全な開発体験
  • 優れたアクセシビリティ

コンポーネントのソースコードを所有できるため、プロジェクトの要件に合わせて自由にカスタマイズできるのが最大の魅力です。