Web InnoventixFreeCode

Social Set Button

Original · free

A set of social sign-in buttons with inline brand-style glyphs.

byWeb InnoventixReact + Tailwind
btnsocialsetbuttons
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/btn-social-set.json
btn-social-set.tsx
"use client";

import { useState, type ReactNode } from "react";
import { useReducedMotion } from "motion/react";

type ProviderId =
  | "google"
  | "apple"
  | "github"
  | "microsoft"
  | "facebook"
  | "x";

type Provider = {
  id: ProviderId;
  label: string;
  glyph: ReactNode;
  /** brand-tinted background for the solid/pill variants (light + dark handled inline) */
  solid: string;
  ring: string;
};

const GoogleGlyph = (
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
    <path
      fill="#4285F4"
      d="M23.52 12.27c0-.86-.08-1.68-.22-2.47H12v4.68h6.46a5.52 5.52 0 0 1-2.4 3.62v3h3.88c2.27-2.09 3.58-5.17 3.58-8.83Z"
    />
    <path
      fill="#34A853"
      d="M12 24c3.24 0 5.96-1.08 7.94-2.9l-3.88-3c-1.08.72-2.45 1.15-4.06 1.15-3.12 0-5.77-2.11-6.71-4.95H1.29v3.1A12 12 0 0 0 12 24Z"
    />
    <path
      fill="#FBBC05"
      d="M5.29 14.3a7.2 7.2 0 0 1 0-4.6v-3.1H1.29a12 12 0 0 0 0 10.8l4-3.1Z"
    />
    <path
      fill="#EA4335"
      d="M12 4.75c1.76 0 3.34.61 4.59 1.8l3.43-3.43C17.95 1.19 15.24 0 12 0A12 12 0 0 0 1.29 6.6l4 3.1C6.23 6.86 8.88 4.75 12 4.75Z"
    />
  </svg>
);

const AppleGlyph = (
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor">
    <path d="M17.05 12.54c-.03-2.68 2.19-3.97 2.29-4.03-1.25-1.83-3.19-2.08-3.88-2.11-1.65-.17-3.22.97-4.06.97-.84 0-2.13-.95-3.5-.92-1.8.03-3.46 1.05-4.39 2.66-1.87 3.25-.48 8.06 1.34 10.7.89 1.29 1.95 2.74 3.34 2.69 1.34-.05 1.85-.87 3.47-.87 1.62 0 2.08.87 3.5.84 1.44-.03 2.36-1.32 3.24-2.62 1.02-1.5 1.44-2.95 1.46-3.03-.03-.01-2.8-1.08-2.83-4.25ZM14.5 4.68c.74-.9 1.24-2.15 1.1-3.4-1.07.04-2.36.71-3.12 1.61-.68.79-1.28 2.06-1.12 3.28 1.19.09 2.4-.61 3.14-1.49Z" />
  </svg>
);

const GitHubGlyph = (
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor">
    <path d="M12 .5A11.5 11.5 0 0 0 .5 12a11.5 11.5 0 0 0 7.86 10.92c.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.56-.29-5.25-1.28-5.25-5.7 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.47.11-3.06 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.79 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.24 2.77.12 3.06.74.81 1.19 1.84 1.19 3.1 0 4.43-2.7 5.41-5.27 5.69.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12 11.5 11.5 0 0 0 12 .5Z" />
  </svg>
);

const MicrosoftGlyph = (
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
    <path fill="#F25022" d="M1 1h10.2v10.2H1z" />
    <path fill="#7FBA00" d="M12.8 1H23v10.2H12.8z" />
    <path fill="#00A4EF" d="M1 12.8h10.2V23H1z" />
    <path fill="#FFB900" d="M12.8 12.8H23V23H12.8z" />
  </svg>
);

const FacebookGlyph = (
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor">
    <path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07Z" />
  </svg>
);

const XGlyph = (
  <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="currentColor">
    <path d="M18.9 1.5h3.68l-8.04 9.19L24 22.5h-7.4l-5.8-7.58-6.63 7.58H.48l8.6-9.83L0 1.5h7.59l5.24 6.93 6.07-6.93Zm-1.29 18.8h2.04L6.48 3.6H4.29l13.32 16.7Z" />
  </svg>
);

const PROVIDERS: Provider[] = [
  {
    id: "google",
    label: "Google",
    glyph: GoogleGlyph,
    solid:
      "bg-white text-slate-700 border border-slate-300 hover:bg-slate-50 dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800",
    ring: "focus-visible:ring-sky-500",
  },
  {
    id: "apple",
    label: "Apple",
    glyph: AppleGlyph,
    solid:
      "bg-neutral-900 text-white border border-neutral-900 hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:border-white dark:hover:bg-neutral-100",
    ring: "focus-visible:ring-neutral-500",
  },
  {
    id: "github",
    label: "GitHub",
    glyph: GitHubGlyph,
    solid:
      "bg-[#24292f] text-white border border-[#24292f] hover:bg-[#1c2126] dark:bg-zinc-100 dark:text-zinc-900 dark:border-zinc-100 dark:hover:bg-white",
    ring: "focus-visible:ring-violet-500",
  },
  {
    id: "microsoft",
    label: "Microsoft",
    glyph: MicrosoftGlyph,
    solid:
      "bg-white text-slate-700 border border-slate-300 hover:bg-slate-50 dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800",
    ring: "focus-visible:ring-sky-500",
  },
  {
    id: "facebook",
    label: "Facebook",
    glyph: FacebookGlyph,
    solid:
      "bg-[#1877F2] text-white border border-[#1877F2] hover:bg-[#0f66d8] dark:hover:bg-[#3b8bf5]",
    ring: "focus-visible:ring-sky-400",
  },
  {
    id: "x",
    label: "X",
    glyph: XGlyph,
    solid:
      "bg-black text-white border border-black hover:bg-neutral-800 dark:bg-white dark:text-black dark:border-white dark:hover:bg-neutral-100",
    ring: "focus-visible:ring-neutral-500",
  },
];

const Spinner = ({ className = "" }: { className?: string }) => (
  <svg
    viewBox="0 0 24 24"
    width="18"
    height="18"
    aria-hidden="true"
    className={`bss-spin ${className}`}
  >
    <circle
      cx="12"
      cy="12"
      r="9"
      fill="none"
      stroke="currentColor"
      strokeOpacity="0.25"
      strokeWidth="3"
    />
    <path
      d="M12 3a9 9 0 0 1 9 9"
      fill="none"
      stroke="currentColor"
      strokeWidth="3"
      strokeLinecap="round"
    />
  </svg>
);

const Check = ({ className = "" }: { className?: string }) => (
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" className={className}>
    <path
      d="M20 6 9 17l-5-5"
      fill="none"
      stroke="currentColor"
      strokeWidth="2.5"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

type Status = "idle" | "loading" | "done";

export default function BtnSocialSet() {
  const reduce = useReducedMotion();
  const [status, setStatus] = useState<Record<string, Status>>({});

  const activate = (key: string) => {
    setStatus((prev) => {
      if (prev[key] === "loading") return prev;
      return { ...prev, [key]: "loading" };
    });
    window.setTimeout(
      () => setStatus((prev) => ({ ...prev, [key]: "done" })),
      reduce ? 350 : 1300
    );
    window.setTimeout(
      () => setStatus((prev) => ({ ...prev, [key]: "idle" })),
      reduce ? 1600 : 3000
    );
  };

  const stateOf = (key: string): Status => status[key] ?? "idle";

  return (
    <section className="relative w-full bg-slate-50 px-5 py-20 text-slate-900 dark:bg-zinc-950 dark:text-zinc-50 sm:px-8">
      <style>{`
        @keyframes bss-spin-kf { to { transform: rotate(360deg); } }
        @keyframes bss-pop-kf {
          0% { transform: scale(0.4); opacity: 0; }
          60% { transform: scale(1.15); opacity: 1; }
          100% { transform: scale(1); opacity: 1; }
        }
        .bss-spin { animation: bss-spin-kf 0.8s linear infinite; transform-origin: center; }
        .bss-pop { animation: bss-pop-kf 0.35s cubic-bezier(0.34,1.56,0.64,1) both; }
        .bss-press { transition: transform 0.12s ease, box-shadow 0.2s ease, background-color 0.18s ease, border-color 0.18s ease; }
        .bss-press:active:not(:disabled) { transform: translateY(1px) scale(0.99); }
        @media (prefers-reduced-motion: reduce) {
          .bss-spin { animation-duration: 1.4s; }
          .bss-pop { animation: none; }
          .bss-press { transition: none; }
          .bss-press:active:not(:disabled) { transform: none; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <header className="mb-10 text-center">
          <span className="inline-flex items-center gap-1.5 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium tracking-wide text-indigo-700 dark:border-indigo-900/60 dark:bg-indigo-950/50 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Continue with
          </span>
          <h2 className="mt-4 text-2xl font-semibold tracking-tight sm:text-3xl">
            Social sign-in buttons
          </h2>
          <p className="mx-auto mt-2 max-w-md text-sm text-slate-500 dark:text-zinc-400">
            Tactile, accessible OAuth buttons with real loading and success
            states. Click one to see it work.
          </p>
        </header>

        {/* Variant 1 — full-width solid stack */}
        <div className="mb-14">
          <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-zinc-500">
            Full width
          </p>
          <div className="space-y-3">
            {PROVIDERS.slice(0, 4).map((p) => {
              const key = `full-${p.id}`;
              const s = stateOf(key);
              return (
                <button
                  key={key}
                  type="button"
                  onClick={() => activate(key)}
                  disabled={s === "loading"}
                  aria-label={`Continue with ${p.label}`}
                  className={`bss-press flex w-full items-center justify-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:cursor-not-allowed disabled:opacity-70 dark:focus-visible:ring-offset-zinc-950 ${p.solid} ${p.ring}`}
                >
                  <span className="flex h-[18px] w-[18px] items-center justify-center">
                    {s === "loading" ? (
                      <Spinner />
                    ) : s === "done" ? (
                      <Check className="bss-pop" />
                    ) : (
                      p.glyph
                    )}
                  </span>
                  <span>
                    {s === "loading"
                      ? "Connecting…"
                      : s === "done"
                      ? "Signed in"
                      : `Continue with ${p.label}`}
                  </span>
                </button>
              );
            })}
          </div>
        </div>

        {/* Variant 2 — outline / secondary style */}
        <div className="mb-14">
          <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-zinc-500">
            Outline
          </p>
          <div className="grid gap-3 sm:grid-cols-2">
            {PROVIDERS.map((p) => {
              const key = `outline-${p.id}`;
              const s = stateOf(key);
              return (
                <button
                  key={key}
                  type="button"
                  onClick={() => activate(key)}
                  disabled={s === "loading"}
                  aria-label={`Sign in with ${p.label}`}
                  className={`bss-press flex items-center justify-center gap-2.5 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm outline-none hover:border-slate-400 hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:cursor-not-allowed disabled:opacity-70 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:border-zinc-600 dark:hover:bg-zinc-800 dark:focus-visible:ring-offset-zinc-950 ${p.ring}`}
                >
                  <span className="flex h-[18px] w-[18px] items-center justify-center">
                    {s === "loading" ? (
                      <Spinner />
                    ) : s === "done" ? (
                      <Check className="bss-pop text-emerald-500" />
                    ) : (
                      p.glyph
                    )}
                  </span>
                  <span>
                    {s === "done" ? "Connected" : p.label}
                  </span>
                </button>
              );
            })}
          </div>
        </div>

        {/* Variant 3 — icon-only round */}
        <div className="mb-14">
          <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-zinc-500">
            Icon only
          </p>
          <div className="flex flex-wrap items-center justify-center gap-3">
            {PROVIDERS.map((p) => {
              const key = `icon-${p.id}`;
              const s = stateOf(key);
              return (
                <button
                  key={key}
                  type="button"
                  onClick={() => activate(key)}
                  disabled={s === "loading"}
                  aria-label={`Sign in with ${p.label}`}
                  className={`bss-press flex h-12 w-12 items-center justify-center rounded-full border border-slate-300 bg-white text-slate-700 shadow-sm outline-none hover:border-slate-400 hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:cursor-not-allowed disabled:opacity-70 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:border-zinc-600 dark:hover:bg-zinc-800 dark:focus-visible:ring-offset-zinc-950 ${p.ring}`}
                >
                  {s === "loading" ? (
                    <Spinner />
                  ) : s === "done" ? (
                    <Check className="bss-pop text-emerald-500" />
                  ) : (
                    p.glyph
                  )}
                </button>
              );
            })}
          </div>
        </div>

        {/* Variant 4 — compact pill row with divider */}
        <div>
          <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-zinc-500">
            Compact
          </p>
          <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
            <div className="flex flex-wrap justify-center gap-2.5">
              {PROVIDERS.slice(0, 3).map((p) => {
                const key = `pill-${p.id}`;
                const s = stateOf(key);
                return (
                  <button
                    key={key}
                    type="button"
                    onClick={() => activate(key)}
                    disabled={s === "loading"}
                    aria-label={`Continue with ${p.label}`}
                    className={`bss-press inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-70 dark:focus-visible:ring-offset-zinc-900 ${p.solid} ${p.ring}`}
                  >
                    <span className="flex h-[18px] w-[18px] items-center justify-center">
                      {s === "loading" ? (
                        <Spinner />
                      ) : s === "done" ? (
                        <Check className="bss-pop" />
                      ) : (
                        p.glyph
                      )}
                    </span>
                    {p.label}
                  </button>
                );
              })}
            </div>

            <div className="my-5 flex items-center gap-3">
              <span className="h-px flex-1 bg-slate-200 dark:bg-zinc-800" />
              <span className="text-xs font-medium uppercase tracking-wider text-slate-400 dark:text-zinc-500">
                or
              </span>
              <span className="h-px flex-1 bg-slate-200 dark:bg-zinc-800" />
            </div>

            <button
              type="button"
              onClick={() => activate("email")}
              disabled={stateOf("email") === "loading"}
              className="bss-press flex w-full items-center justify-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm outline-none hover:bg-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-70 dark:focus-visible:ring-offset-zinc-900"
            >
              {stateOf("email") === "loading" ? (
                <Spinner />
              ) : stateOf("email") === "done" ? (
                <Check className="bss-pop" />
              ) : (
                <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="2">
                  <rect x="3" y="5" width="18" height="14" rx="2" />
                  <path d="m3 7 9 6 9-6" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              )}
              {stateOf("email") === "loading"
                ? "Sending link…"
                : stateOf("email") === "done"
                ? "Check your inbox"
                : "Continue with email"}
            </button>
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →