ログインフォーム(Login Form)— ソーシャルログイン

フォーム入力 初級

このコンポーネントについて

業務アプリのログイン画面では、自社アカウントのメール/パスワード認証に加えて、Google・Microsoft・GitHubなど外部サービスのアカウントでログインできる「ソーシャルログイン」ボタンを併設するケースが増えています。このページでは、ソーシャルログインボタンとメール/パスワードフォームを組み合わせた、実務でよく見るログイン画面のUIパターンを実装します。

ボタンを押した際の「プロバイダーへ移動中」というローディング表示や、他のボタンを一時的に無効化する挙動など、実装時に見落としやすい細かな状態管理を再現しています。なお本ページはボタンのUI・クリック時の状態遷移のみを実装しており、実際にGoogle等と通信して認証するOAuthの処理そのものは含まれません。OAuth・ソーシャルログインの仕組み自体は「OAuth・ソーシャルログインとは?」で解説しています。

  • 3種類のソーシャルログインボタン — Google・Microsoft・GitHubの公式ブランドカラーに沿ったボタンを縦に並べて表示
  • ボタン内ローディング+他ボタンの無効化 — 押したボタンのテキストが「〇〇に移動中...」に変わりスピナーを表示。同時に他の2ボタンとメール/パスワードフォームをdisabled化し、誤って複数プロバイダーへ同時に遷移してしまう事故を防ぐ
  • 区切り線「または」 — ソーシャルログインとメール/パスワードログインの間に視覚的な区切りを設ける定番レイアウト
  • メール/パスワードフォーム — パスワード表示切替・送信時バリデーション・ローディング・成功/失敗状態を実装
  • OAuth処理は含まない — ボタンのUI・状態遷移のみを再現し、実際の認可フロー(外部サービスへのリダイレクト・許可画面・トークン取得等)は実装しない設計

実装のポイント・注意点

本ページはOAuthを実装していません。ボタンのクリック時に見た目上のローディング状態を再現しているだけで、実際に外部サービスへ通信・リダイレクトする処理は含みません。実装時は、各プロバイダーの公式ドキュメント(Google Identity Services、Microsoft identity platform、GitHub OAuth Apps 等)を別途参照して、認可エンドポイントへのリダイレクトやサーバー側でのトークン交換処理を組み込む必要があります。サンプルソースのJSにも、実際に組み込む際に手を加える箇所をコメントで明記しています。

ソーシャルログインボタンを押している間に他のボタンやメール/パスワードフォームを操作できてしまうと、二重に認証を試みているように見えて混乱を招きます。disabled 属性でまとめて操作を無効化するのが実務上の定石です。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

サンプルソース

3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ログインフォーム(ソーシャルログイン付き) サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="login-card">

  <div class="social-login-list">
    <button class="social-login-btn" type="button" data-provider="google">
      <span class="social-login-icon">
        <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
          <path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62z"/>
          <path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.81.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.95v2.33A9 9 0 0 0 9 18z"/>
          <path fill="#FBBC05" d="M3.97 10.72A5.4 5.4 0 0 1 3.68 9c0-.6.1-1.18.29-1.72V4.95H.95A9 9 0 0 0 0 9c0 1.45.35 2.83.95 4.05l3.02-2.33z"/>
          <path fill="#EA4335" d="M9 3.58c1.32 0 2.51.46 3.44 1.35l2.58-2.58C13.46.89 11.43 0 9 0A9 9 0 0 0 .95 4.95l3.02 2.33C4.68 5.16 6.66 3.58 9 3.58z"/>
        </svg>
      </span>
      <span class="social-login-text">Googleでログイン</span>
      <svg class="social-login-spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" hidden aria-hidden="true">
        <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
      </svg>
    </button>

    <button class="social-login-btn" type="button" data-provider="microsoft">
      <span class="social-login-icon">
        <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
          <rect x="0" y="0" width="8.5" height="8.5" fill="#F25022"/>
          <rect x="9.5" y="0" width="8.5" height="8.5" fill="#7FBA00"/>
          <rect x="0" y="9.5" width="8.5" height="8.5" fill="#00A4EF"/>
          <rect x="9.5" y="9.5" width="8.5" height="8.5" fill="#FFB900"/>
        </svg>
      </span>
      <span class="social-login-text">Microsoftでログイン</span>
      <svg class="social-login-spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" hidden aria-hidden="true">
        <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
      </svg>
    </button>

    <button class="social-login-btn" type="button" data-provider="github">
      <span class="social-login-icon">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="#181717" aria-hidden="true">
          <path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.11.82-.26.82-.58v-2.17c-3.34.73-4.04-1.42-4.04-1.42-.55-1.38-1.33-1.75-1.33-1.75-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.8 1.3 3.49 1 .11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.24 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.49 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.83.58A12 12 0 0 0 12 .3z"/>
        </svg>
      </span>
      <span class="social-login-text">GitHubでログイン</span>
      <svg class="social-login-spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" hidden aria-hidden="true">
        <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
      </svg>
    </button>
  </div>

  <div class="login-divider"><span>または</span></div>

  <!-- 認証成功バナー(送信成功時に表示) -->
  <div class="login-success-banner" id="loginSuccessBanner" hidden>
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
      <polyline points="20 6 9 17 4 12"/>
    </svg>
    <span>ログインに成功しました。</span>
  </div>

  <!-- 認証エラーバナー(送信失敗時に表示) -->
  <div class="login-error-banner" id="loginErrorBanner" hidden>
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
      <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
    </svg>
    <span>メールアドレスまたはパスワードが違います。</span>
  </div>

  <form class="login-form" id="loginForm" novalidate>
    <!-- メールアドレス -->
    <div class="login-field">
      <label class="login-label" for="email">メールアドレス</label>
      <input class="login-input" type="email" id="email" name="email"
             placeholder="[email protected]" autocomplete="email">
      <span class="login-field-error" id="emailError"></span>
    </div>

    <!-- パスワード -->
    <div class="login-field">
      <label class="login-label" for="password">パスワード</label>
      <div class="login-password-wrap">
        <input class="login-input" type="password" id="password" name="password"
               autocomplete="current-password">
        <button class="login-toggle-pw" type="button" id="togglePw"
                aria-label="パスワードを表示">
          <!-- 目を開くアイコン(パスワード非表示中に表示) -->
          <svg class="icon-eye" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
            <circle cx="12" cy="12" r="3"/>
          </svg>
          <!-- 目を閉じるアイコン(パスワード表示中に表示) -->
          <svg class="icon-eye-off" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" hidden aria-hidden="true">
            <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
            <path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
            <line x1="1" y1="1" x2="23" y2="23"/>
          </svg>
        </button>
      </div>
      <span class="login-field-error" id="passwordError"></span>
    </div>

    <!-- 送信ボタン -->
    <button class="login-btn" type="submit" id="loginBtn">
      <svg class="login-btn-spinner" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" hidden aria-hidden="true">
        <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
      </svg>
      <span class="login-btn-text">ログイン</span>
    </button>
  </form>
</div>

<script src="./script.js"></script>
</body>
</html>
:root {
  --color-primary: #2B7FE8;
  --color-primary-dark: #1E6ECF;
  --color-error: #E53E3E;
  --color-error-bg: #FFF5F5;
  --color-border: #D0D7E0;
  --color-text: #1A2B3C;
  --color-muted: #5A6A7A;
}

* { box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #F4F6F9;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  margin: 0;
  padding: 16px;
}

/* ===カード=== */
.login-card {
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
  padding: 32px;
  width: 100%;
  max-width: 400px;
}

/* ===ソーシャルログインボタン=== */
.social-login-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}

.social-login-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 10px 14px;
  background: #fff;
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s, border-color 0.15s;
}

.social-login-btn:hover:not(:disabled) {
  background: #F4F6F9;
  border-color: #9AA5B4;
}

.social-login-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.social-login-icon {
  display: flex;
  flex-shrink: 0;
}

.social-login-spinner {
  animation: spin 0.8s linear infinite;
}

/* hidden属性がdisplay:flexに上書きされないよう打ち消す */
.social-login-spinner[hidden] { display: none; }

/* ===区切り線「または」=== */
.login-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 20px 0;
  color: var(--color-muted);
  font-size: 13px;
}

.login-divider::before,
.login-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: #E0E4E9;
}

/* ===認証エラーバナー=== */
.login-error-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--color-error-bg);
  border: 1px solid #FEB2B2;
  border-radius: 8px;
  padding: 10px 14px;
  margin-bottom: 20px;
  color: var(--color-error);
  font-size: 14px;
}
.login-error-banner[hidden] { display: none; }

/* ===認証成功バナー=== */
.login-success-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #F0FFF4;
  border: 1px solid #9AE6B4;
  border-radius: 8px;
  padding: 10px 14px;
  margin-bottom: 20px;
  color: #276749;
  font-size: 14px;
}
.login-success-banner[hidden] { display: none; }

/* ===フィールド=== */
.login-field { margin-bottom: 20px; }

.login-label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  margin-bottom: 6px;
}

.login-input {
  width: 100%;
  padding: 10px 12px;
  font-size: 15px;
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  outline: none;
  transition: border-color 0.15s;
  font-family: inherit;
}

.login-input:focus { border-color: var(--color-primary); }
.login-input.is-error { border-color: var(--color-error); }
.login-input:disabled { background: #F4F6F9; cursor: not-allowed; }

.login-field-error {
  display: block;
  font-size: 12px;
  color: var(--color-error);
  margin-top: 4px;
}

/* ===パスワードトグル=== */
.login-password-wrap { position: relative; }
.login-password-wrap .login-input { padding-right: 44px; }

.login-toggle-pw {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px;
  color: var(--color-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
}

.login-toggle-pw:hover { color: var(--color-primary); }
.login-toggle-pw:disabled { cursor: not-allowed; opacity: 0.6; }

/* ===送信ボタン=== */
.login-btn {
  width: 100%;
  padding: 12px;
  background: var(--color-primary);
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transition: background 0.15s;
  font-family: inherit;
}

.login-btn:hover:not(:disabled) { background: var(--color-primary-dark); }
.login-btn:disabled { opacity: 0.7; cursor: not-allowed; }

/* スピナーアニメーション */
@keyframes spin { to { transform: rotate(360deg); } }
.login-btn-spinner { animation: spin 0.8s linear infinite; }

/* SVGのhidden属性はブラウザによっては効かないことがある(初期非表示の保険) */
.login-btn-spinner[hidden],
.icon-eye[hidden],
.icon-eye-off[hidden] { display: none; }

@media (max-width: 480px) {
  .login-card { padding: 24px; }
}
// ===== 設定 =====
var SOCIAL_PROVIDERS = [
  { id: 'google', label: 'Google' },
  { id: 'microsoft', label: 'Microsoft' },
  { id: 'github', label: 'GitHub' }
];
var SOCIAL_LOADING_MS = 1500;
var FAKE_LOADING_MS = 1200;

// ===== 要素取得 =====
var socialButtons = document.querySelectorAll('.social-login-btn');
var loginForm     = document.getElementById('loginForm');
var emailInput    = document.getElementById('email');
var passwordInput = document.getElementById('password');
var emailError    = document.getElementById('emailError');
var passwordError = document.getElementById('passwordError');
var errorBanner   = document.getElementById('loginErrorBanner');
var successBanner = document.getElementById('loginSuccessBanner');
var loginBtn      = document.getElementById('loginBtn');
var btnText       = loginBtn.querySelector('.login-btn-text');
var btnSpinner    = loginBtn.querySelector('.login-btn-spinner');
var togglePw      = document.getElementById('togglePw');
var eyeIcon       = togglePw.querySelector('.icon-eye');
var eyeOffIcon    = togglePw.querySelector('.icon-eye-off');

// デモ切替ラジオの現在値を取得
function getDemoMode() {
  var radio = document.querySelector('input[name="demoMode"]:checked');
  return radio ? radio.value : 'success';
}

// ===== SVG 表示ヘルパー(hidden属性はSVGで効かないためstyle.displayで制御) =====
function showSvg(el) { el.style.display = 'inline'; }
function hideSvg(el) { el.style.display = 'none'; }

function getProviderLabel(providerId) {
  for (var i = 0; i < SOCIAL_PROVIDERS.length; i++) {
    if (SOCIAL_PROVIDERS[i].id === providerId) return SOCIAL_PROVIDERS[i].label;
  }
  return '';
}

// ===== ソーシャルログイン =====
// メール/パスワードフォーム一式をまとめて disabled にする
function setFormDisabled(disabled) {
  emailInput.disabled = disabled;
  passwordInput.disabled = disabled;
  togglePw.disabled = disabled;
  loginBtn.disabled = disabled;
}

function setSocialLoading(providerId) {
  socialButtons.forEach(function (btn) {
    btn.disabled = true;
    if (btn.getAttribute('data-provider') === providerId) {
      btn.classList.add('is-loading');
      btn.querySelector('.social-login-text').textContent = getProviderLabel(providerId) + 'に移動中...';
      showSvg(btn.querySelector('.social-login-spinner'));
    }
  });
  setFormDisabled(true);

  // ▼▼▼ 実際にOAuthを組み込む場合はここを書き換える ▼▼▼
  // 本来はここで setTimeout ではなく、各プロバイダーの認可エンドポイントへ
  // window.location.href でリダイレクトする(例)
  //   Google:     https://accounts.google.com/o/oauth2/v2/auth?...
  //   Microsoft:  https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?...
  //   GitHub:     https://github.com/login/oauth/authorize?...
  // client_id・redirect_uri・scope・state(CSRF対策用のランダム値)は
  // 各プロバイダーの公式ドキュメントに従って付与すること。
  // リダイレクト後、自社サーバーの /auth/callback 等で認可コードとstateを検証し、
  // サーバー側でトークン交換を行う(client_secretを使うためブラウザJSでは行わない)。
  // このデモはUIの状態遷移のみを見せるため、実通信の代わりに
  // setTimeout でローディング状態を再現している。
  setTimeout(resetSocialButtons, SOCIAL_LOADING_MS);
  // ▲▲▲ ここまで ▲▲▲
}

function resetSocialButtons() {
  socialButtons.forEach(function (btn) {
    btn.disabled = false;
    btn.classList.remove('is-loading');
    var providerId = btn.getAttribute('data-provider');
    btn.querySelector('.social-login-text').textContent = getProviderLabel(providerId) + 'でログイン';
    hideSvg(btn.querySelector('.social-login-spinner'));
  });
  setFormDisabled(false);
}

socialButtons.forEach(function (btn) {
  btn.addEventListener('click', function () {
    setSocialLoading(btn.getAttribute('data-provider'));
  });
});

// ===== メール形式チェック =====
function validateEmail(val) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
}

// ===== フィールドエラー表示・解除 =====
function setFieldError(input, errorEl, message) {
  input.classList.add('is-error');
  errorEl.textContent = message;
}

function clearFieldError(input, errorEl) {
  input.classList.remove('is-error');
  errorEl.textContent = '';
}

// ===== バリデーション(送信ボタン押下時のみ) =====
function validate() {
  var ok = true;

  if (!emailInput.value.trim()) {
    setFieldError(emailInput, emailError, 'メールアドレスを入力してください');
    ok = false;
  } else if (!validateEmail(emailInput.value.trim())) {
    setFieldError(emailInput, emailError, '正しいメールアドレス形式で入力してください');
    ok = false;
  } else {
    clearFieldError(emailInput, emailError);
  }

  if (!passwordInput.value) {
    setFieldError(passwordInput, passwordError, 'パスワードを入力してください');
    ok = false;
  } else {
    clearFieldError(passwordInput, passwordError);
  }

  return ok;
}

// ===== ローディング状態 =====
function setLoading() {
  loginBtn.disabled = true;
  btnText.hidden = true;
  showSvg(btnSpinner);
}

// ===== 成功状態 =====
function setSuccess() {
  hideSvg(btnSpinner);
  btnText.textContent = 'ログイン';
  btnText.hidden = false;
  loginBtn.disabled = false;
  successBanner.hidden = false;
}

// ===== 失敗状態 =====
function setFailure() {
  hideSvg(btnSpinner);
  btnText.hidden = false;
  loginBtn.disabled = false;
  errorBanner.hidden = false;
}

// ===== リセット =====
function resetDemo() {
  resetSocialButtons();
  loginForm.reset();
  clearFieldError(emailInput, emailError);
  clearFieldError(passwordInput, passwordError);
  errorBanner.hidden = true;
  successBanner.hidden = true;
  loginBtn.disabled = false;
  btnText.textContent = 'ログイン';
  btnText.hidden = false;
  hideSvg(btnSpinner);
  // パスワードトグルを初期状態(非表示モード)に戻す
  passwordInput.type = 'password';
  showSvg(eyeIcon);
  hideSvg(eyeOffIcon);
  togglePw.setAttribute('aria-label', 'パスワードを表示');
}

// ===== フォーム送信 =====
loginForm.addEventListener('submit', function (e) {
  e.preventDefault();
  // 前回の表示をリセットしてから再バリデーション
  clearFieldError(emailInput, emailError);
  clearFieldError(passwordInput, passwordError);
  errorBanner.hidden = true;
  successBanner.hidden = true;

  if (!validate()) return;

  setLoading();

  setTimeout(function () {
    if (getDemoMode() === 'success') {
      setSuccess();
    } else {
      setFailure();
    }
  }, FAKE_LOADING_MS);
});

// ===== パスワード全角入力防止 =====
// type="text" 切替時に IME が有効になるため、IME 確定時に非 ASCII 文字を除去する
passwordInput.addEventListener('compositionend', function () {
  this.value = this.value.replace(/[^\x20-\x7E]/g, '');
});

// ===== パスワードトグル =====
togglePw.addEventListener('click', function () {
  var isVisible = passwordInput.type === 'text';
  if (isVisible) {
    // 非表示に切り替え
    passwordInput.type = 'password';
    showSvg(eyeIcon);
    hideSvg(eyeOffIcon);
    togglePw.setAttribute('aria-label', 'パスワードを表示');
  } else {
    // 表示に切り替え
    passwordInput.type = 'text';
    hideSvg(eyeIcon);
    showSvg(eyeOffIcon);
    togglePw.setAttribute('aria-label', 'パスワードを非表示');
  }
});

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や要件変更など、条件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# ログインフォーム(ソーシャルログイン付き)作成依頼

## 概要
Google・Microsoft・GitHubのソーシャルログインボタンと、メールアドレス/パスワードのログインフォームを組み合わせたログイン画面を作成してください。
認証ロジック・OAuthの実装は不要で、UIの状態遷移(ボタンのローディング・フォームのバリデーション・成功/失敗)の実装が目的です。

## 要件
- 上から「Googleでログイン」「Microsoftでログイン」「GitHubでログイン」の3つのボタン(各社ブランドカラーのアイコン付き)
- ソーシャルログインボタンをクリックすると、押したボタンのテキストが「〇〇に移動中...」に変わりスピナーを表示し、他の2ボタン+メール/パスワードフォームを一時的にdisabledにする
- 約1.5秒後、自動的にすべてのボタン・フォームを初期状態に戻す(実際の画面遷移・外部通信は発生させない)
- ソーシャルログインボタン群の下に区切り線「または」を配置
- その下にメールアドレス(必須・形式チェック)とパスワード(必須・表示/非表示トグル付き)の入力フォーム
- 送信ボタン押下時のバリデーション(フィールド下にエラーメッセージを表示)
- 送信中はボタンをスピナー表示+disabledにして二重送信を防止
- 成功時:フォーム上部に緑色の「ログインに成功しました。」バナーを表示。ボタンを通常状態に戻して再送信可能にする
- 失敗時:フォーム上部に「メールアドレスまたはパスワードが違います」エラーバナーを表示。ボタンを通常状態に戻して再入力・再送信可能にする
- 白背景・角丸・影のカード型デザイン

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(最小幅 320px)

## 動作詳細
ソーシャルログインボタンはクリック時にUI状態(ローディング表示・他ボタンの無効化)のみを変化させ、実際のOAuth認可フローや外部サービスへの通信・リダイレクトは一切行わない。約1.5秒後に自動で初期状態へ戻す。
メール/パスワードフォームは、送信ボタン押下 → バリデーション → 通過したらローディング状態(1〜2秒)→ 成功/失敗を表示という流れにする。失敗後はフォームを再入力・再送信できる状態に戻す。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。