【Next.js】ログイン画面のデザインと作り方を Tailwind のコード付きで解説

公開日: 2026年10月8日 · 読了目安 9 分

Next.js と Tailwind CSS でログイン画面を作る手順です。中央カードと左右分割のレイアウト、ラベルと autocomplete の付け方、パスワードの表示切り替え、エラーメッセージ、ソーシャルログインの並べ方、写真の上のガラス風カードとスマホ対応まで、コード付きで解説します。

Next.js のログイン画面を使いやすくデザインする

Next.js でアプリを作り、ログイン画面に入力欄2つとボタンを並べた。動きはするけれど、どこか手作り感が抜けない。

ログイン画面の印象は、飾りよりも細部で決まります。ラベル・入力の補助・エラーの出し方が揃うと、それだけで信頼できる画面に見えます。

この記事では、見た目と使いやすさを Tailwind CSS で整えます。認証の仕組みそのものは扱わず、フォームの送信先は仮の処理にしておきます。

この記事で分かることは次のとおりです。

  • 中央カードと左右分割、2つのレイアウトの選び方

  • ラベルと autocomplete を正しく付けた入力欄

  • パスワードの表示切り替えボタンとエラーメッセージ

  • ソーシャルログインボタンの並べ方

  • 写真の上にガラス風のカードを置くときのコントラストとスマホ対応

ログイン画面のデザインで押さえる3つのこと

ログイン画面は毎日のように開かれるのに、誰もじっくりとは見ない画面です。使う人がしたいのは、早く中に入ることだけです。

  • 迷わせない:入力欄は2つ、主なボタンは1つ。見出しで何の画面かがすぐ分かる

  • 手間を省く:パスワード管理ツールやブラウザの自動入力がきちんと効く

  • 失敗から戻しやすい:エラーの内容と直し方が、フォームのそばに出る

デザインの工夫は、この3つを崩さない範囲で足します。写真の背景やガラスの効果も、文字が読めることが前提です。

Step1:ログイン画面のレイアウトを決める(中央カードと左右分割)

ログイン画面の配置には、大きく2つの型があります。

  • 中央カード:画面の真ん中にフォームのカードを1枚置く。どんなサービスにも合い、スマホでも崩れにくい

  • 左右分割:片側にフォーム、反対側に写真や製品の説明を置く。広い画面でブランドの印象を出しやすい

迷ったら中央カードから始めます。下のコードはページの枠だけで、フォームは Step2 で作ります。

app/login/page.tsx
import type { Metadata } from "next";
import { LoginForm } from "./LoginForm";

export const metadata: Metadata = { title: "ログイン" };

export default function LoginPage() {
  return (
    <main className="flex min-h-dvh items-center justify-center bg-zinc-50 px-4 py-12">
      <div className="w-full max-w-sm rounded-2xl border border-zinc-200 bg-white p-8 shadow-sm">
        <h1 className="text-2xl font-semibold text-zinc-900">ログイン</h1>
        <p className="mt-2 text-sm text-zinc-600">アカウントにログインして続けます。</p>
        <LoginForm />
      </div>
    </main>
  );
}

左右分割のログイン画面にする場合

広い画面だけ2列にし、スマホでは写真を隠してフォームだけにします。lg: から grid を2列にするだけで作れます。

写真は飾りなので aria-hidden を付けます。背景画像は style で指定します。

app/login/page.tsx(左右分割の枠)
<main className="grid min-h-dvh lg:grid-cols-2">
  <div className="flex items-center justify-center px-4 py-12">
    <div className="w-full max-w-sm">{/* 見出しと LoginForm */}</div>
  </div>
  <div
    aria-hidden="true"
    className="hidden bg-zinc-900 bg-cover bg-center lg:block"
    style={{ backgroundImage: "url(/login-side.jpg)" }}
  />
</main>

Step2:ログイン画面の入力欄を作る(ラベルと autocomplete)

入力欄には、必ず label を付けます。プレースホルダー(欄の中の薄い文字)だけだと、入力を始めた途端に何の欄か分からなくなります。

autocomplete 属性も大切です。どの欄に何を入れるかを、ブラウザやパスワード管理ツールに伝える指定です。

送信中の表示とエラーは、React 19 の useActionState で受け取ります。パスワード欄の部品は Step3 で作ります。

app/login/LoginForm.tsx
"use client";

import { useActionState } from "react";
import { login, type LoginState } from "./actions";
import { PasswordField } from "./PasswordField";

const initialState: LoginState = {};

export function LoginForm() {
  const [state, formAction, pending] = useActionState(login, initialState);

  return (
    <form action={formAction} className="mt-8 space-y-5">
      {state.error ? (
        <p role="alert" className="rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700">
          {state.error}
        </p>
      ) : null}

      <div>
        <label htmlFor="email" className="block text-sm font-medium text-zinc-800">
          メールアドレス
        </label>
        <input
          id="email"
          name="email"
          type="email"
          autoComplete="username"
          required
          defaultValue={state.email}
          className="mt-2 block min-h-11 w-full rounded-lg border border-zinc-300 px-3 text-base focus:border-zinc-900 focus:outline-none focus:ring-2 focus:ring-zinc-900/20"
        />
      </div>

      <PasswordField />

      <button
        type="submit"
        disabled={pending}
        className="flex min-h-11 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-700 disabled:opacity-60"
      >
        {pending ? "ログインしています…" : "ログイン"}
      </button>
    </form>
  );
}

ログインフォームの送信先を用意する

フォームの送信先は、サーバーで動く関数(サーバーアクション)にします。ファイルの先頭に "use server" を書くと、フォームから直接呼べます。

ここでは入力の確認だけをする仮の処理です。実際のログインは、Auth.js(NextAuth.js)などの認証ライブラリの説明に沿って、この関数の中で呼びます。

app/login/actions.ts
"use server";

export type LoginState = { error?: string; email?: string };

export async function login(_prev: LoginState, formData: FormData): Promise<LoginState> {
  const email = String(formData.get("email") ?? "").trim();
  const password = String(formData.get("password") ?? "");

  if (!email || !password) {
    return { error: "メールアドレスとパスワードを入力してください。", email };
  }

  // ここで認証ライブラリのログイン処理を呼ぶ(仮の処理)。
  // 成功したら next/navigation の redirect("/dashboard") で移動する。
  return { error: "メールアドレスかパスワードが正しくありません。", email };
}

autocomplete は username と current-password にする

ログインの欄は、メールアドレスで入る場合も autocomplete="username" にします。パスワードの欄は current-password です。

新規登録やパスワード変更の画面では、new-password を使います。保存済みのパスワードを出すか、新しく提案するかをブラウザが判断できます。

メールの欄を type="email" にすると、スマホでは @ の入ったキーボードが出ます。

Step3:ログイン画面にパスワードの表示切り替えを付ける

スマホで長いパスワードを伏せ字のまま打つと、打ち間違いに気づけません。目のアイコンのボタンで、入力した文字を表示できるようにします。

アイコンは lucide-react の Eye と EyeOff を使っています。npm i lucide-react で入れるか、手持ちの SVG に替えてください。

app/login/PasswordField.tsx
"use client";

import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";

export function PasswordField() {
  const [visible, setVisible] = useState(false);

  return (
    <div>
      <label htmlFor="password" className="block text-sm font-medium text-zinc-800">
        パスワード
      </label>
      <div className="relative mt-2">
        <input
          id="password"
          name="password"
          type={visible ? "text" : "password"}
          autoComplete="current-password"
          required
          className="block min-h-11 w-full rounded-lg border border-zinc-300 pl-3 pr-12 text-base focus:border-zinc-900 focus:outline-none focus:ring-2 focus:ring-zinc-900/20"
        />
        <button
          type="button"
          onClick={() => setVisible((v) => !v)}
          aria-controls="password"
          aria-pressed={visible}
          className="absolute inset-y-0 right-0 flex w-11 items-center justify-center text-zinc-500 hover:text-zinc-900"
        >
          <span className="sr-only">パスワードを表示</span>
          {visible ? <EyeOff aria-hidden="true" className="size-5" /> : <Eye aria-hidden="true" className="size-5" />}
        </button>
      </div>
    </div>
  );
}

パスワード表示ボタンのコードのポイント

  • ボタンは type="button" にする。付け忘れると、押したときにフォームが送信される

  • 読み上げ用の名前は「パスワードを表示」で固定し、押されているかは aria-pressed で伝える

  • ボタンの幅を 44px にし、欄の右側に余白(pr-12)を取る。文字がアイコンに重ならない

ログイン画面のエラーメッセージの出し方

ログインに失敗したときは、メールアドレスとパスワードのどちらが違うかを書きません。登録済みのアドレスかどうかを、第三者に知られないためです。

「メールアドレスかパスワードが正しくありません」と、まとめて伝えます。Step2 のコードでは role="alert" を付け、表示されたら読み上げられるようにしています。

入力したメールアドレスは消さずに残します。赤い色だけでなく文章でも伝えると、色が見分けにくい人にも届きます。

Step4:ログイン画面のソーシャルログインボタンを並べる

Google や Apple でのログインを置くなら、並べ方を先に決めておきます。

  • メールとソーシャルの間に「または」の区切り線を入れ、2つの方法があると分かるようにする

  • 利用者が多い方法を上に置き、並び順はサービス内のどの画面でも揃える

  • ボタンの文言は「Google でログイン」のように、動作まで書く

ソーシャルログインが主なサービスなら、ボタンをフォームの上に置きます。メールが主なら、下のようにフォームの下に置きます。

Google と Apple は、ロゴやボタンの見た目のガイドラインをそれぞれ公開しています。ロゴを使うときは、各社のガイドラインに合わせてください。

app/login/SocialLogin.tsx
export function SocialLogin() {
  return (
    <div className="mt-8">
      <div className="flex items-center gap-3 text-xs text-zinc-500">
        <span aria-hidden="true" className="h-px flex-1 bg-zinc-200" />
        または
        <span aria-hidden="true" className="h-px flex-1 bg-zinc-200" />
      </div>
      <div className="mt-6 grid gap-3">
        {/* 押したときの処理は、使う認証ライブラリの方法に合わせて付ける */}
        <button
          type="button"
          className="flex min-h-11 items-center justify-center gap-3 rounded-lg border border-zinc-300 bg-white text-sm font-medium text-zinc-900 hover:bg-zinc-50"
        >
          {/* Google のロゴ(ガイドラインに沿った SVG) */}
          Google でログイン
        </button>
        <button
          type="button"
          className="flex min-h-11 items-center justify-center gap-3 rounded-lg bg-black text-sm font-medium text-white hover:bg-zinc-800"
        >
          {/* Apple のロゴ */}
          Apple でサインイン
        </button>
      </div>
    </div>
  );
}

Step5:写真の上にガラス風のログイン画面を作る

背景に写真を敷き、すりガラスのようなカードを重ねると、ログイン画面に印象が出ます。ガラス風の見た目は、背景をぼかす backdrop-blur と半透明の白で作ります。

気をつけるのは文字の読みやすさです。写真の明るい部分にカードが重なると、白い文字が読めなくなります。

app/login/page.tsx(ガラス風の版)
import { LoginForm } from "./LoginForm";

export default function LoginPage() {
  return (
    <main
      className="relative flex min-h-dvh items-center justify-center bg-zinc-900 bg-cover bg-center px-4 py-12"
      style={{ backgroundImage: "url(/login-bg.jpg)" }}
    >
      {/* 写真を暗くして、白い文字のコントラストを保つ */}
      <div aria-hidden="true" className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/30 to-black/60" />

      <div className="relative w-full max-w-sm rounded-3xl border border-white/20 bg-black/60 p-8 text-white shadow-2xl supports-[backdrop-filter]:bg-white/10 supports-[backdrop-filter]:backdrop-blur-xl">
        <h1 className="text-2xl font-semibold">ログイン</h1>
        <p className="mt-2 text-sm text-white/80">アカウントにログインして続けます。</p>
        <LoginForm />
      </div>
    </main>
  );
}

ガラス風のログイン画面でもコントラストを保つ

本文の文字と背景の明るさの差(コントラスト比)は、4.5:1 以上が目安です。WCAG というアクセシビリティの基準が示している値です。

写真の上に黒の半透明を1枚重ねると、写真を替えても文字が読める状態を保てます。ぼかしに対応していないブラウザ向けに、濃い背景(bg-black/60)を先に指定しています。

LoginForm のラベルや枠線も、白系の色(text-white や border-white/30)に替えます。ラベルは必ず欄の外に出し、薄いプレースホルダーに頼らないようにします。

ガラス風 UI の作り方は https://design-layer.com/ja/articles/nextjs-glassmorphism-ui-guide で詳しく解説しています。

スマホでのログイン画面の調整

スマホでは、カードの左右に 16px ほどの余白を残します。上のコードの px-4 と w-full がその指定です。

入力欄の文字は 16px(text-base)以上にします。iPhone の Safari は、これより小さい文字の欄を押すと画面を拡大してしまいます。

高さは min-h-dvh にします。スマホのアドレスバーが出入りしても、カードが画面からはみ出しにくくなります。

ログイン画面でよくある失敗と公開前チェック

  • ラベルを付けず、プレースホルダーだけで欄の名前を伝える

  • autocomplete を付けない、または off にする。パスワード管理ツールが使いにくくなる

  • パスワード欄への貼り付けを禁止する。管理ツールからのコピーが使えなくなる

  • 表示切り替えボタンを type="button" にし忘れ、押すとフォームが送信される

  • エラーのたびに入力をすべて消す。メールアドレスは残す

  • 背景の写真が明るすぎて、ガラスのカードの文字が読めない

公開前に、ブラウザにパスワードを保存して自動入力を試します。

キーボードの Tab だけで、上から順にすべての欄とボタンへ移れるかも確かめます。

完成したログイン画面をコピーして使う方法

DesignLayer の「リキッドガラス・ログイン」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「リキッドガラス・ログイン」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

Step5 のガラス風のログイン画面は、写真選びや透明度の調整に時間がかかります。

そんなときは、完成したログイン画面をブラウザで確かめてから、コードを自分の Next.js に載せる方法もあります。

DesignLayer の「リキッドガラス・ログイン」は、自然風景の写真の上にリキッドガラスのカードを置いたログイン画面です。ユーザー名とパスワード・Remember me・Login ボタン・Google と Apple のログインが揃っています。

見出し・ボタンの文言・背景の写真を差し替えて、アプリや SaaS のログインに置けます。認証の処理は、Step2 と同じく自分のサービスに合わせてつなぎます。

詳細ページの「Cursor 用にコピー」を押すと、統合の指示・ライブプレビューの URL・本体の TSX・npm の依存がまとめてコピーされます。Cursor や Claude に貼って、組み込みを頼めます。

閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは日本時間で1日3回です。

カタログは https://design-layer.com/ja/components から開けます。

Next.js のログイン画面についてよくある質問

  • 迷ったら中央カードです。どんな画面幅でも崩れにくく、作るのも簡単です。

    製品の写真や説明を見せたい場合は、広い画面だけ左右分割にします。

まとめ:Next.js でログイン画面を作る手順

  • レイアウトは中央カードから始め、写真を見せたいときは広い画面だけ左右分割にする

  • 入力欄には label を付け、autocomplete は username と current-password にする

  • パスワードの表示切り替えは type="button" と aria-pressed で作る

  • エラーはどちらが違うかを書かずにまとめて伝え、メールアドレスは残す

  • ガラス風のカードは、写真に黒の半透明を重ねてコントラストを保つ

まずは Step1 と Step2 のコードで、中央カードのログイン画面を作ってみてください。

ログイン後の画面で使うナビの作り方は https://design-layer.com/ja/articles/nextjs-bottom-tab-bar-guide で解説しています。

【Next.js】ログイン画面のデザインと作り方を Tailwind のコード付きで解説 | DesignLayer