Skip to content
Star on GitHub
Forms and inputsSocial sign-in
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Social sign-in

One-tap providers that remember the last one you used. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add social-sign-in
Sign in to DesignMe
or
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add social-sign-in

React

$npx designme add social-sign-in --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Social sign-in is a sign-in card with one button per provider, such as Google, GitHub, Figma and SSO, plus a way to continue with email. The provider used last time gets an outline and a Last used badge, so a returning person can find it quickly. After sign-in the card swaps to a welcome message with the provider's name and a Sign out button.

  • When people sign in to your agent product with an existing work account
  • When returning users tend to forget which provider they used last time
  • When sign-in calls an OAuth flow that takes a moment and the button should show it is working
  • When you need a small signed-in confirmation before the product loads
03

Usage

Import it and drive it from your own state.

Import

import { SocialSignIn } from '@/components/designme/social-sign-in/SocialSignIn';

Example

The demo above as a React component, 19 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { SocialSignIn, type SocialSignInProvider } from './SocialSignIn';

const PROVIDERS: SocialSignInProvider[] = [
  { key: 'google', label: 'Google', brand: 'google' }, { key: 'github', label: 'GitHub', brand: 'github' },
  { key: 'figma', label: 'Figma', brand: 'figma' }, { key: 'sso', label: 'SSO', icon: 'lock' }
];

/* ---------- Social sign-in: Figma was used last; any provider signs Maya in after a second ---------- */
export default function SocialSignInDemo() {
  return (
    <div style={{ width: '100%' }}>
      <SocialSignIn providers={PROVIDERS} defaultLastUsed="figma" user={{ name: 'Maya', initials: 'MK', color: 'var(--dm-av-l)' }}
        onSignIn={() => new Promise(res => setTimeout(res, 1100))} />
    </div>
  );
}
04

Props

Everything SocialSignIn takes.

PropTypeWhat it does
providersSocialSignInProvider[]Defaults to Google, GitHub, Figma and SSO
lastUsedstring | nullThe provider used last time, by key. It gets an outline and a "Last used" badge
defaultLastUsedstring | null
onLastUsedChange(key: string | null) => void
signedInstring | nullThe provider the person is signed in with, by key, or null. Signing in sets it, Sign out clears it
defaultSignedInstring | null
onSignedInChange(key: string | null) => void
onSignIn(provider: SocialSignInProvider) => unknownCalled when a provider is clicked. Return a promise and its button spins until it settles; resolve to false or reject to stay signed out
onSignOut() => void
onEmail() => voidAdds a link under the providers
userSocialSignInUser
titlestring
logoReactNodeYour logo, on the dark tile at the top. Defaults to the DesignMe mark
emailLabelstring
classNamestring
05

Code

The full source, as the CLI installs it.

SocialSignIn.tsxReact · 125 lines
'use client';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { Brand, type BrandName } from '../lib/brands';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Avatar, enter, Spin } from '../lib/motion';
import '../designme.css';
import './social-sign-in.css';

/** A way to sign in */
export interface SocialSignInProvider {
  key: string;
  /** Its name, after "Continue with", like "GitHub" */
  label: string;
  /** Its official mark: google, github, figma or slack */
  brand?: BrandName;
  /** An icon instead, like 'lock' for SSO */
  icon?: IconName;
  /** A letter tile instead, like "MS" */
  letters?: string;
}

/** The person who signed in */
export interface SocialSignInUser {
  /** Shown as "Welcome back, Maya" */
  name: string;
  initials: string;
  /** The avatar colour, like 'var(--dm-av-l)' */
  color?: string;
}

export interface SocialSignInProps {
  /** Defaults to Google, GitHub, Figma and SSO */
  providers?: SocialSignInProvider[];
  /** The provider used last time, by key. It gets an outline and a "Last used" badge */
  lastUsed?: string | null;
  defaultLastUsed?: string | null;
  onLastUsedChange?: (key: string | null) => void;
  /** The provider the person is signed in with, by key, or null. Signing in sets it, Sign out clears it */
  signedIn?: string | null;
  defaultSignedIn?: string | null;
  onSignedInChange?: (key: string | null) => void;
  /** Called when a provider is clicked. Return a promise and its button spins until it settles; resolve to false or reject to stay signed out */
  onSignIn?: (provider: SocialSignInProvider) => unknown;
  onSignOut?: () => void;
  /** Adds a link under the providers */
  onEmail?: () => void;
  user?: SocialSignInUser;
  title?: string;
  /** Your logo, on the dark tile at the top. Defaults to the DesignMe mark */
  logo?: ReactNode;
  emailLabel?: string;
  className?: string;
}

const PROVIDERS: SocialSignInProvider[] = [
  { key: 'google', label: 'Google', brand: 'google' }, { key: 'github', label: 'GitHub', brand: 'github' },
  { key: 'figma', label: 'Figma', brand: 'figma' }, { key: 'sso', label: 'SSO', icon: 'lock' }
];

const Mark = ({ p }: { p: SocialSignInProvider }) =>
  p.brand ? <Brand name={p.brand} /> : p.letters ? <span className="dm-lt">{p.letters}</span> : <Icon name={p.icon ?? 'lock'} />;

const Logo = () => (
  <svg viewBox="-20 -83 799 799" aria-hidden="true" className="dm-logo"><path fill="currentColor" d="M149.497 283.784C149.477 284.047 149.474 284.311 149.487 284.575C156.385 340.034 183.548 391.116 225.9 428.196C267.989 465.044 322.188 485.532 378.414 485.855L379.743 485.859C436.453 485.868 491.188 465.353 533.619 428.212C575.912 391.194 603.062 340.216 610.018 284.856L610.018 281.512C610.008 277.337 608.324 273.315 605.303 270.335C602.277 267.351 598.155 265.656 593.837 265.656L165.647 265.656C161.338 265.656 157.226 267.349 154.21 270.331C151.198 273.309 149.524 277.326 149.523 281.491L149.523 283.437L149.497 283.784ZM114.378 247.395C118.396 247.391 122.254 245.919 125.214 243.29C128.063 240.759 129.887 237.33 130.396 233.629L130.396 233.35L130.437 232.917C136.355 171.634 165.162 114.754 211.202 73.3687C257.239 31.9859 317.206 9.06063 379.393 9.04448L379.396 9.04448C441.593 9.04583 501.574 31.964 547.624 73.3482C593.677 114.735 622.491 171.624 628.411 232.917L628.453 233.35L628.453 233.643C628.953 237.344 630.773 240.774 633.621 243.304C636.58 245.932 640.437 247.399 644.452 247.395L725.644 247.395C728.882 247.404 732.089 248.06 735.071 249.329C738.055 250.598 740.753 252.453 743.001 254.787C745.252 257.123 747.005 259.891 748.151 262.924C749.296 265.954 749.81 269.183 749.662 272.418L749.663 272.418C745.484 366.797 704.617 455.919 635.615 521.211C566.616 586.499 474.809 622.924 379.338 622.924C283.872 622.924 192.071 586.496 123.081 521.206C54.0935 455.917 13.2412 366.804 9.07133 272.438L9.07036 272.438L9.07036 272.436C9.07007 272.43 9.06967 272.423 9.06938 272.417L9.07036 272.417C8.92272 269.182 9.43655 265.953 10.5811 262.924C11.7276 259.89 13.481 257.123 15.7315 254.787C17.9805 252.452 20.6797 250.597 23.6622 249.329C26.6443 248.06 29.8504 247.404 33.0889 247.395L114.378 247.395Z" /></svg>
);

/** One-tap sign-in that remembers the provider you used last. */
export function SocialSignIn({ providers = PROVIDERS, lastUsed, defaultLastUsed = null, onLastUsedChange, signedIn, defaultSignedIn = null, onSignedInChange,
  onSignIn, onSignOut, onEmail, user, title = 'Sign in to DesignMe', logo, emailLabel = 'Continue with email', className }: SocialSignInProps) {
  const [last, setLast] = useControllable(lastUsed, defaultLastUsed, onLastUsedChange);
  const [session, setSession] = useControllable(signedIn, defaultSignedIn, onSignedInChange);
  const [busy, setBusy] = useState<string | null>(null);
  // signing in swaps the providers for a shorter welcome: it keeps the height it had and centres the welcome in it
  const [held, setHeld] = useState<number>();
  const root = useRef<HTMLDivElement>(null);
  const alive = useRef(true);
  useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);

  const signIn = (p: SocialSignInProvider) => {
    if (busy) return;
    const done = (ok: unknown) => { if (!alive.current) return; setBusy(null); if (ok === false) return; setHeld(root.current?.offsetHeight); setLast(p.key); setSession(p.key); };
    const out = onSignIn?.(p);
    if (out && typeof (out as Promise<unknown>).then === 'function') {
      setBusy(p.key);
      (out as Promise<unknown>).then(done, () => { if (alive.current) setBusy(null); });
    } else done(out);
  };
  const signOut = () => { setHeld(undefined); setSession(null); onSignOut?.(); };
  const who = providers.find(p => p.key === session);
  const step = (i: number) => (user ? i : i - 1) * 60;

  return (
    <div data-dm="" ref={root} className={cx('dm-ssi dm-panel', session && held && 'dm-held', className)} style={session && held ? { minHeight: held } : undefined}>
      {session ? (
        <div className="dm-ssi-ok">
          {/* the welcome comes in a line at a time */}
          {user && <Avatar initials={user.initials} color={user.color ?? 'var(--dm-av-l)'} className="dm-enter" />}
          <b {...enter(step(1))}>{user ? `Welcome back, ${user.name}` : 'You are signed in'}</b>
          <span className="dm-muted dm-enter" style={enter(step(2)).style}>Signed in with {who?.label ?? session}</span>
          <button type="button" className="dm-pill dm-ghost dm-sm dm-enter" style={enter(step(3)).style} onClick={signOut}>Sign out</button>
        </div>
      ) : <>
        <div className="dm-ssi-h"><span className="dm-ssi-lg">{logo ?? <Logo />}</span><b>{title}</b></div>
        <div className={cx('dm-ssi-list', busy && 'dm-wait')}>
          {providers.map((p, i) => {
            const e = enter(i * 50);
            return (
              <button key={p.key} type="button" className={cx('dm-ssi-b', p.key === last && 'dm-last-used', busy === p.key && 'dm-busy', e.className)} style={e.style}
                aria-busy={busy === p.key || undefined} onClick={() => signIn(p)}>
                <span className="dm-mk">{busy === p.key ? <Spin /> : <Mark p={p} />}</span>
                <span>Continue with {p.label}</span>
                {p.key === last && <span className="dm-last">Last used</span>}
              </button>
            );
          })}
        </div>
        <div className="dm-ssi-or">or</div>
        <button type="button" className="dm-ssi-mail" onClick={onEmail}>{emailLabel}</button>
      </>}
    </div>
  );
}
social-sign-in.cssReact · 26 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Social sign-in ---------- */
.dm-ssi{padding:28px 22px var(--dm-s-5);display:flex;flex-direction:column;align-items:center;gap:14px;max-width:320px;margin:0 auto;width:100%}
.dm-ssi-h{display:flex;flex-direction:column;align-items:center;gap:var(--dm-s-3);text-align:center}
.dm-ssi-lg{width:44px;height:44px;border-radius:14px;background:var(--dm-ink);color:var(--dm-on-ink);display:grid;place-items:center}
.dm-ssi-lg .dm-logo{width:26px;height:26px}
.dm-ssi-h b{font-weight:500;font-size:14px}
.dm-ssi-list{display:flex;flex-direction:column;gap:var(--dm-s-2);width:100%}
.dm-ssi-b{position:relative;display:flex;align-items:center;justify-content:center;gap:10px;height:42px;padding:0 18px;border-radius:var(--dm-r-pill);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent);font-size:13.5px;font-weight:500;transition:background .2s,box-shadow .2s,transform .15s}
@media (hover:hover) and (pointer:fine){.dm-ssi-b:hover{background:var(--dm-wash);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-faint) 50%,transparent)}}
.dm-ssi-b:active{transform:scale(.98)}
.dm-ssi-b .dm-mk{width:18px;height:18px;display:grid;place-items:center;flex:none}
.dm-ssi-b .dm-mk svg,.dm-ssi-b .dm-mk img{width:18px;height:18px;display:block}
.dm-ssi-b .dm-mk .dm-i{width:16px;height:16px}
.dm-ssi-b .dm-mk .dm-lt{min-width:18px;height:18px;padding:0 3px;border-radius:5px;background:var(--dm-ink);color:var(--dm-on-ink);display:grid;place-items:center;font-size:10px;font-weight:500}
.dm-ssi-b .dm-last{position:absolute;top:-8px;right:22px;font-size:10.5px;font-weight:500;color:var(--dm-fg2);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent);padding:1px 7px;border-radius:var(--dm-r-pill)}
.dm-ssi-b.dm-last-used{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-pick-on) 50%,transparent)}
.dm-ssi-b.dm-busy{pointer-events:none}
.dm-ssi-list.dm-wait .dm-ssi-b:not(.dm-busy){opacity:.45}
.dm-ssi-or{display:flex;align-items:center;gap:10px;width:100%;font-size:11.5px;color:var(--dm-muted)}
.dm-ssi-or::before,.dm-ssi-or::after{content:"";flex:1;height:1px;background:var(--dm-line)}
.dm-ssi-mail{font-size:12.5px;color:var(--dm-fg2);padding:var(--dm-s-1) 10px;border-radius:var(--dm-r-pill);transition:color .2s,background .2s}
@media (hover:hover) and (pointer:fine){.dm-ssi-mail:hover{color:var(--dm-fg);background:var(--dm-row)}}
.dm-ssi-ok{display:flex;flex-direction:column;align-items:center;gap:var(--dm-s-2);padding:var(--dm-s-2) 0 var(--dm-s-1);text-align:center;font-size:13px}
.dm-ssi-ok .dm-av{width:44px;height:44px;font-size:14px}
social-sign-in.htmlHTML · 11 lines
<div class="card" data-c="social-signin">
  <div class="stage-in">
    <div class="ssi panel">
      <div class="ssi-h"><span class="ssi-lg"><svg viewBox="-20 -83 799 799" aria-hidden="true" class="logo"><path d="M149.497 283.784C149.477 284.047 149.474 284.311 149.487 284.575C156.385 340.034 183.548 391.116 225.9 428.196C267.989 465.044 322.188 485.532 378.414 485.855L379.743 485.859C436.453 485.868 491.188 465.353 533.619 428.212C575.912 391.194 603.062 340.216 610.018 284.856L610.018 281.512C610.008 277.337 608.324 273.315 605.303 270.335C602.277 267.351 598.155 265.656 593.837 265.656L165.647 265.656C161.338 265.656 157.226 267.349 154.21 270.331C151.198 273.309 149.524 277.326 149.523 281.491L149.523 283.437L149.497 283.784ZM114.378 247.395C118.396 247.391 122.254 245.919 125.214 243.29C128.063 240.759 129.887 237.33 130.396 233.629L130.396 233.35L130.437 232.917C136.355 171.634 165.162 114.754 211.202 73.3687C257.239 31.9859 317.206 9.06063 379.393 9.04448L379.396 9.04448C441.593 9.04583 501.574 31.964 547.624 73.3482C593.677 114.735 622.491 171.624 628.411 232.917L628.453 233.35L628.453 233.643C628.953 237.344 630.773 240.774 633.621 243.304C636.58 245.932 640.437 247.399 644.452 247.395L725.644 247.395C728.882 247.404 732.089 248.06 735.071 249.329C738.055 250.598 740.753 252.453 743.001 254.787C745.252 257.123 747.005 259.891 748.151 262.924C749.296 265.954 749.81 269.183 749.662 272.418L749.663 272.418C745.484 366.797 704.617 455.919 635.615 521.211C566.616 586.499 474.809 622.924 379.338 622.924C283.872 622.924 192.071 586.496 123.081 521.206C54.0935 455.917 13.2412 366.804 9.07133 272.438L9.07036 272.438L9.07036 272.436C9.07007 272.43 9.06967 272.423 9.06938 272.417L9.07036 272.417C8.92272 269.182 9.43655 265.953 10.5811 262.924C11.7276 259.89 13.481 257.123 15.7315 254.787C17.9805 252.452 20.6797 250.597 23.6622 249.329C26.6443 248.06 29.8504 247.404 33.0889 247.395L114.378 247.395Z" fill="currentColor" /></svg></span><b>Sign in to DesignMe</b></div>
      <div class="ssi-list"></div>
      <div class="ssi-or">or</div>
      <button class="ssi-mail">Continue with email</button>
    </div>
  </div>
</div>
social-sign-in.cssHTML · 25 lines
/* ---------- Social sign-in ---------- */
.ssi{padding:28px 22px var(--s-5);display:flex;flex-direction:column;align-items:center;gap:14px;max-width:320px;margin:0 auto;width:100%}
.ssi-h{display:flex;flex-direction:column;align-items:center;gap:var(--s-3);text-align:center}
.ssi-lg{width:44px;height:44px;border-radius:14px;background:var(--ink);color:var(--on-ink);display:grid;place-items:center}
.ssi-lg .logo{width:26px;height:26px}
.ssi-h b{font-weight:500;font-size:14px}
.ssi-list{display:flex;flex-direction:column;gap:var(--s-2);width:100%}
.ssi-b{position:relative;display:flex;align-items:center;justify-content:center;gap:10px;height:42px;padding:0 18px;border-radius:var(--r-pill);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--rail) 50%,transparent);font-size:13.5px;font-weight:500;transition:background .2s,box-shadow .2s,transform .15s}
@media (hover:hover) and (pointer:fine){.ssi-b:hover{background:var(--wash);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--faint) 50%,transparent)}}
.ssi-b:active{transform:scale(.98)}
.ssi-b .mk{width:18px;height:18px;display:grid;place-items:center;flex:none}
.ssi-b .mk svg,.ssi-b .mk img{width:18px;height:18px;display:block}
.ssi-b .mk .i{width:16px;height:16px}
.ssi-b .mk .lt{min-width:18px;height:18px;padding:0 3px;border-radius:5px;background:var(--ink);color:var(--on-ink);display:grid;place-items:center;font-size:10px;font-weight:500}
.ssi-b .last{position:absolute;top:-8px;right:22px;font-size:10.5px;font-weight:500;color:var(--fg2);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--rail) 50%,transparent);padding:1px 7px;border-radius:var(--r-pill)}
.ssi-b.last-used{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--pick-on) 50%,transparent)}
.ssi-b.busy{pointer-events:none}
.ssi-list.wait .ssi-b:not(.busy){opacity:.45}
.ssi-or{display:flex;align-items:center;gap:10px;width:100%;font-size:11.5px;color:var(--muted)}
.ssi-or::before,.ssi-or::after{content:"";flex:1;height:1px;background:var(--line)}
.ssi-mail{font-size:12.5px;color:var(--fg2);padding:var(--s-1) 10px;border-radius:var(--r-pill);transition:color .2s,background .2s}
@media (hover:hover) and (pointer:fine){.ssi-mail:hover{color:var(--fg);background:var(--row)}}
.ssi-ok{display:flex;flex-direction:column;align-items:center;gap:var(--s-2);padding:var(--s-2) 0 var(--s-1);text-align:center;font-size:13px}
.ssi-ok .av{width:44px;height:44px;font-size:14px}
social-sign-in.jsHTML · 20 lines
DM.def('social-signin', (r, c) => {
  const P = [['Google','google'],['GitHub','github'],['Figma','figma'],['SSO','']];
  let last = 'Figma';
  const list = $('.ssi-list', r), root = $('.ssi', r);
  const mark = p => p[1] ? BRAND[p[1]] : I('lock','s');
  function paint(){
    list.innerHTML = P.map(p => `<button class="ssi-b${p[0] === last ? ' last-used' : ''}" data-p="${p[0]}"><span class="mk">${mark(p)}</span><span class="lb">Continue with ${p[0]}</span>${p[0] === last ? '<span class="last">Last used</span>' : ''}</button>`).join('') + '';
    stagger($$('.ssi-b', list), 50);
    $$('.ssi-b', list).forEach(b => b.onclick = () => c.run(async () => {
      b.classList.add('busy'); list.classList.add('wait'); $('.mk', b).innerHTML = '<span class="spin"></span>';
      await c.wait(1100); last = b.dataset.p;
      hold(root); const kids = [...root.children]; kids.forEach(k => k.hidden = true);
      const ok = html(`<div class="ssi-ok">${AV('MK','var(--av-l)')}<b>Welcome back, Maya</b><span class="muted">Signed in with ${last}</span><button class="pill ghost sm">Sign out</button></div>`);
      root.appendChild(ok); stagger([...ok.children], 60);
      $('button', ok).onclick = () => { ok.remove(); release(root); kids.forEach(k => k.hidden = false); list.classList.remove('wait'); paint(); };
    }));
  }
  paint();
});
06

Questions

What people ask about it.

How do I connect it to my OAuth flow?
Handle onSignIn, which receives the clicked provider, and return a promise. The button spins until it resolves. Resolve to false or reject to keep the card signed out.
How does it remember the last provider between visits?
It doesn't store anything itself. Save the key from onLastUsedChange wherever you like, then pass it back as lastUsed or defaultLastUsed next time.
Can I add a provider that has no brand mark, like Microsoft?
Yes. Give it letters, such as "MS", for a letter tile, or an icon name. Built-in brand marks cover google, github, figma and slack.
07

More in Forms and inputs

Buttons, fields, pickers and sign-in flows for any product.

ButtonsEvery button style, with loading, success and hold-to-confirm states.
BadgeSmall labels for plans, environments and status, with a dot, an icon or a remove button.
TooltipQuiet labels on hover, with shortcuts and a richer preview card.
Search inputSearch with scoped filters, recent queries and an agent answer.
Country selectA searchable select with country codes, currency and keyboard support.
Multi-selectA combobox that filters as you type and keeps your picks as chips.
Sign inEmail first, then the one-time code from the email, copied or typed in.
OTP fieldA one-time code, one box per digit, that fills from a paste.
All 11 in Forms and inputs →