0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js + Tailwind CSS — スタイリングの基本を覚えた

0
Posted at

はじめに

Next.jsのプロジェクトを作るときにTailwind CSSを選択していたが、ちゃんと使い方を整理したことがなかった。

PHPのプロジェクトではBootstrapかカスタムCSSを書いていた。TailwindはユーティリティファーストというBootstrapとも普通のCSSとも違う発想で、最初は「クラス名が長くて読みにくい」と思っていたが、慣れると手放せなくなった。


ユーティリティファーストとは

<!-- Bootstrapのアプローチ -->
<button class="btn btn-primary btn-lg">送信</button>

<!-- 普通のCSS -->
<button class="submit-button">送信</button>
<style>
.submit-button {
    background-color: #2563eb;
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 0.5rem;
    font-size: 1rem;
}
</style>

<!-- Tailwindのアプローチ -->
<button class="bg-blue-600 text-white px-6 py-3 rounded-lg text-base">
    送信
</button>

Tailwindは小さなユーティリティクラスを組み合わせてスタイルを作る。CSSファイルを別途書かなくていいのでコンポーネントを見るだけでスタイルがわかる。


インストール(Next.jsに追加する場合)

create-next-appでTailwindを選択していれば不要。後から追加する場合:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
    content: [
        './pages/**/*.{js,ts,jsx,tsx,mdx}',
        './components/**/*.{js,ts,jsx,tsx,mdx}',
        './app/**/*.{js,ts,jsx,tsx,mdx}',
    ],
    theme: {
        extend: {},
    },
    plugins: [],
};

export default config;
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

よく使うクラスの整理

スペーシング

<!-- margin(外側の余白) -->
<div class="m-4">    <!-- margin: 1rem(全方向) -->
<div class="mt-4">   <!-- margin-top: 1rem -->
<div class="mb-4">   <!-- margin-bottom: 1rem -->
<div class="ml-4">   <!-- margin-left: 1rem -->
<div class="mr-4">   <!-- margin-right: 1rem -->
<div class="mx-4">   <!-- margin-left + right: 1rem -->
<div class="my-4">   <!-- margin-top + bottom: 1rem -->

<!-- padding(内側の余白) -->
<div class="p-4">    <!-- padding: 1rem -->
<div class="px-6">   <!-- padding-left + right: 1.5rem -->
<div class="py-3">   <!-- padding-top + bottom: 0.75rem -->

数字は4で割るとremになる(4=1rem、8=2rem、2=0.5rem)。

<!-- テキストカラー -->
<p class="text-gray-900">濃いグレー(ほぼ黒)</p>
<p class="text-gray-600">中くらいグレー</p>
<p class="text-gray-400">薄いグレー</p>
<p class="text-blue-600"></p>
<p class="text-red-500"></p>

<!-- 背景色 -->
<div class="bg-white"></div>
<div class="bg-gray-50">超薄いグレー(背景によく使う)</div>
<div class="bg-blue-600"></div>

<!-- ボーダー色 -->
<div class="border border-gray-200">薄いボーダー</div>
<div class="border border-blue-500">青いボーダー</div>

色は{色}-{強さ}の形式。強さは50〜900(大きいほど濃い)。

タイポグラフィ

<!-- フォントサイズ -->
<p class="text-xs">12px</p>
<p class="text-sm">14px</p>
<p class="text-base">16px(デフォルト)</p>
<p class="text-lg">18px</p>
<p class="text-xl">20px</p>
<p class="text-2xl">24px</p>
<p class="text-3xl">30px</p>

<!-- フォントウェイト -->
<p class="font-normal">普通(400)</p>
<p class="font-medium">やや太め(500)</p>
<p class="font-semibold">セミボールド(600)</p>
<p class="font-bold">ボールド(700)</p>

<!-- テキスト配置 -->
<p class="text-left">左揃え</p>
<p class="text-center">中央揃え</p>
<p class="text-right">右揃え</p>

レイアウト

<!-- Flexbox -->
<div class="flex">                    <!-- display: flex -->
<div class="flex items-center">       <!-- align-items: center -->
<div class="flex items-start">        <!-- align-items: flex-start -->
<div class="flex justify-between">    <!-- justify-content: space-between -->
<div class="flex justify-center">     <!-- justify-content: center -->
<div class="flex gap-4">              <!-- gap: 1rem -->
<div class="flex flex-col">           <!-- flex-direction: column -->
<div class="flex flex-wrap">          <!-- flex-wrap: wrap -->

<!-- Grid -->
<div class="grid grid-cols-3 gap-4">  <!-- 3カラムグリッド -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">  <!-- レスポンシブグリッド -->

<!-- 幅・高さ -->
<div class="w-full">       <!-- width: 100% -->
<div class="w-1/2">        <!-- width: 50% -->
<div class="w-64">         <!-- width: 16rem -->
<div class="max-w-md">     <!-- max-width: 28rem -->
<div class="max-w-screen-lg">  <!-- max-width: 1024px -->
<div class="h-screen">    <!-- height: 100vh -->
<div class="min-h-screen"> <!-- min-height: 100vh -->

ボーダーと角丸

<div class="border">              <!-- 1px solid border -->
<div class="border-2">            <!-- 2px solid border -->
<div class="border-t">            <!-- 上だけ -->
<div class="border-b">            <!-- 下だけ -->
<div class="rounded">             <!-- border-radius: 0.25rem -->
<div class="rounded-md">          <!-- border-radius: 0.375rem -->
<div class="rounded-lg">          <!-- border-radius: 0.5rem -->
<div class="rounded-xl">          <!-- border-radius: 0.75rem -->
<div class="rounded-full">        <!-- border-radius: 9999px(丸) -->

レスポンシブデザイン

<!-- モバイルファーストで書く(小さい画面がデフォルト) -->
<div class="
    grid
    grid-cols-1     <!-- デフォルト(モバイル): 1カラム -->
    md:grid-cols-2  <!-- md以上: 2カラム -->
    lg:grid-cols-3  <!-- lg以上: 3カラム -->
">

ブレークポイント:

sm:  640px以上
md:  768px以上
lg:  1024px以上
xl:  1280px以上
2xl: 1536px以上

Bootstrapのグリッドと似ているが、「プレフィックスをつけるだけで条件が変わる」という書き方はBootstrapより直感的だと感じた。


ホバー・フォーカス・アクティブ

<!-- ホバー -->
<button class="bg-blue-600 hover:bg-blue-700 text-white">
    ホバーで濃くなる
</button>

<!-- フォーカス -->
<input class="border focus:outline-none focus:ring-2 focus:ring-blue-500">

<!-- アクティブ(クリック中) -->
<button class="active:scale-95">クリックで少し縮む</button>

<!-- disabled -->
<button class="disabled:opacity-50 disabled:cursor-not-allowed" disabled>
    無効ボタン
</button>

擬似クラスもプレフィックスで書ける。hover:をつけるだけでホバー時のスタイルが定義できる。


よく使うコンポーネントパターン

カード

function Card({
    title,
    description,
    children,
}: {
    title:       string;
    description: string;
    children?:   React.ReactNode;
}) {
    return (
        <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
            <h3 className="text-lg font-semibold text-gray-900 mb-2">{title}</h3>
            <p className="text-gray-600 text-sm mb-4">{description}</p>
            {children}
        </div>
    );
}

ボタン

type ButtonVariant = 'primary' | 'secondary' | 'danger';

const variantClasses: Record<ButtonVariant, string> = {
    primary:   'bg-blue-600 hover:bg-blue-700 text-white',
    secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-900',
    danger:    'bg-red-600 hover:bg-red-700 text-white',
};

function Button({
    variant  = 'primary',
    children,
    disabled,
    ...props
}: {
    variant?:  ButtonVariant;
    children:  React.ReactNode;
    disabled?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
    return (
        <button
            className={`
                px-4 py-2 rounded-lg font-medium text-sm
                transition-colors duration-150
                disabled:opacity-50 disabled:cursor-not-allowed
                ${variantClasses[variant]}
            `}
            disabled={disabled}
            {...props}
        >
            {children}
        </button>
    );
}

フォーム要素

function FormField({
    label,
    name,
    type    = 'text',
    error,
    ...props
}: {
    label:  string;
    name:   string;
    type?:  string;
    error?: string;
} & React.InputHTMLAttributes<HTMLInputElement>) {
    return (
        <div className="space-y-1">
            <label
                htmlFor={name}
                className="block text-sm font-medium text-gray-700"
            >
                {label}
            </label>
            <input
                id={name}
                name={name}
                type={type}
                className={`
                    w-full px-3 py-2 border rounded-lg text-sm
                    focus:outline-none focus:ring-2 focus:ring-blue-500
                    ${error
                        ? 'border-red-500 focus:ring-red-500'
                        : 'border-gray-300'
                    }
                `}
                {...props}
            />
            {error && (
                <p className="text-red-500 text-xs">{error}</p>
            )}
        </div>
    );
}

バッジ・ステータス表示

type BadgeColor = 'green' | 'yellow' | 'red' | 'gray' | 'blue';

const colorClasses: Record<BadgeColor, string> = {
    green:  'bg-green-100 text-green-800',
    yellow: 'bg-yellow-100 text-yellow-800',
    red:    'bg-red-100 text-red-800',
    gray:   'bg-gray-100 text-gray-800',
    blue:   'bg-blue-100 text-blue-800',
};

function Badge({
    color    = 'gray',
    children,
}: {
    color?:   BadgeColor;
    children: React.ReactNode;
}) {
    return (
        <span className={`
            inline-flex items-center px-2.5 py-0.5
            rounded-full text-xs font-medium
            ${colorClasses[color]}
        `}>
            {children}
        </span>
    );
}

// 使う側
<Badge color="green">完了</Badge>
<Badge color="yellow">処理中</Badge>
<Badge color="red">エラー</Badge>

cn()関数で条件付きクラスを管理する

クラスを条件で切り替えるときはclsxtailwind-mergeを使う。

npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge }               from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}
import { cn } from '@/lib/utils';

function Button({
    variant  = 'primary',
    disabled,
    className,
    children,
}: {
    variant?:   'primary' | 'secondary';
    disabled?:  boolean;
    className?: string;
    children:   React.ReactNode;
}) {
    return (
        <button
            className={cn(
                // ベーススタイル
                'px-4 py-2 rounded-lg font-medium text-sm transition-colors',

                // バリアントによる切り替え
                variant === 'primary'   && 'bg-blue-600 hover:bg-blue-700 text-white',
                variant === 'secondary' && 'bg-gray-100 hover:bg-gray-200 text-gray-900',

                // disabled時
                disabled && 'opacity-50 cursor-not-allowed',

                // 外から渡されたクラス
                className,
            )}
            disabled={disabled}
        >
            {children}
        </button>
    );
}

cn()clsxで条件付きクラスを組み立てて、tailwind-mergeで競合するクラスを自動で解決する。shadcn/uiなど多くのUIライブラリで採用されているパターン。


カスタムカラーとテーマの設定

// tailwind.config.ts
const config: Config = {
    theme: {
        extend: {
            colors: {
                brand: {
                    50:  '#eff6ff',
                    100: '#dbeafe',
                    500: '#3b82f6',
                    600: '#2563eb',
                    700: '#1d4ed8',
                    900: '#1e3a8a',
                },
            },
            fontFamily: {
                sans: ['Noto Sans JP', 'sans-serif'],
            },
            spacing: {
                '18': '4.5rem',
                '72': '18rem',
            },
        },
    },
};
<!-- カスタムカラーを使う -->
<div class="bg-brand-500 text-white">ブランドカラー</div>

ダークモード

// tailwind.config.ts
const config: Config = {
    darkMode: 'class',  // classベースのダークモード
    ...
};
<!-- ダークモード対応 -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
    ダークモード対応テキスト
</div>
// ダークモードの切り替え
'use client';

import { useEffect, useState } from 'react';

export function ThemeToggle() {
    const [dark, setDark] = useState(false);

    useEffect(() => {
        document.documentElement.classList.toggle('dark', dark);
    }, [dark]);

    return (
        <button onClick={() => setDark(!dark)}>
            {dark ? '☀️ ライト' : '🌙 ダーク'}
        </button>
    );
}

よくあるレイアウトパターン

サイドバーレイアウト

export default function Layout({ children }: { children: React.ReactNode }) {
    return (
        <div className="flex h-screen bg-gray-50">
            {/* サイドバー */}
            <aside className="w-64 bg-white border-r border-gray-200 flex flex-col">
                <div className="p-6 border-b border-gray-200">
                    <h1 className="text-lg font-bold">My App</h1>
                </div>
                <nav className="flex-1 p-4 space-y-1">
                    <a href="/dashboard"
                        className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100">
                        ダッシュボード
                    </a>
                    <a href="/users"
                        className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100">
                        ユーザー
                    </a>
                </nav>
            </aside>

            {/* メインコンテンツ */}
            <main className="flex-1 overflow-y-auto">
                <div className="p-8">
                    {children}
                </div>
            </main>
        </div>
    );
}

ヘッダーレイアウト

export default function Layout({ children }: { children: React.ReactNode }) {
    return (
        <div className="min-h-screen bg-gray-50">
            <header className="bg-white border-b border-gray-200 sticky top-0 z-10">
                <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
                    <h1 className="text-lg font-bold">My App</h1>
                    <nav className="flex items-center gap-6">
                        <a href="/users" className="text-sm text-gray-600 hover:text-gray-900">
                            ユーザー
                        </a>
                        <a href="/settings" className="text-sm text-gray-600 hover:text-gray-900">
                            設定
                        </a>
                    </nav>
                </div>
            </header>
            <main className="max-w-7xl mx-auto px-4 py-8">
                {children}
            </main>
        </div>
    );
}

まとめ

  • Tailwindはクラスを組み合わせてスタイルを作るユーティリティファースト
  • スペーシングは4=1rem。レスポンシブはmd: lg:のプレフィックス
  • 条件付きクラスはcn()ヘルパー関数で管理する
  • コンポーネント化してvariantで使い分けるパターンが多い
  • カスタムカラーや追加スペーシングはtailwind.config.tsで拡張できる

最初は「クラス名が長くて読みにくい」と感じていたが、CSSファイルを行き来しなくていいのは実際に書いてみると快適だった。BootstrapのようにCSSの知識を隠蔽するのではなく、CSSの知識があるほど使いこなせる設計になっている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?