Lunia DS

Base components

Social button

A sign-in button for a third-party account, with the provider logo and a label or the logo alone, in gray, color or brand style, which eases its colors on hover and gives a small press like Button.

Loading preview

A sign-in card with a SocialButtonGroup of Google, Facebook and Apple (gray, full width) above an email field, a row of the same three as logo-only buttons, and the color and brand themes for comparison. The demo draws simple placeholder marks in the logo slot. Replay presses the first button.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add social-button

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { Button } from '../button/button';
import { Input } from '../input/input';
import { SocialButton, SocialButtonGroup } from './social-button';
import type { SocialProvider } from './social-button-variants';

// Neutral placeholder marks: an app passes the provider's official logo instead.
const MARKS: Record<'google' | 'facebook' | 'apple', string> = {
  google: 'M8 2.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11Zm0 3a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z',
  facebook:
    'M4 2.5h8A1.5 1.5 0 0 1 13.5 4v8a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 12V4A1.5 1.5 0 0 1 4 2.5Z',
  apple: 'M8 2.25 14 13.25H2L8 2.25Z',
};

function PlaceholderMark({ provider }: { provider: keyof typeof MARKS }) {
  return (
    <svg aria-hidden="true" viewBox="0 0 16 16" fill="currentColor" fillRule="evenodd">
      <path d={MARKS[provider]} />
    </svg>
  );
}

const PROVIDERS: (keyof typeof MARKS & SocialProvider)[] = ['google', 'facebook', 'apple'];

export default function SocialButtonDemo({ replay = 0 }: { replay?: number }) {
  const [pressed, setPressed] = React.useState(false);

  React.useEffect(() => {
    if (replay === 0) return;
    setPressed(true);
    const timer = window.setTimeout(() => setPressed(false), 240);
    return () => window.clearTimeout(timer);
  }, [replay]);

  return (
    <div className="flex w-full max-w-sm flex-col gap-8">
      <div className="flex flex-col gap-6 rounded-xl border border-secondary bg-primary p-6 shadow-xs">
        <div className="flex flex-col gap-1">
          <p className="text-lg font-semibold text-primary">Sign in to your account</p>
          <p className="text-sm text-tertiary">Use an account you already have, or your email.</p>
        </div>
        <SocialButtonGroup>
          {PROVIDERS.map((provider, index) => (
            <SocialButton
              key={provider}
              provider={provider}
              logo={<PlaceholderMark provider={provider} />}
              data-pressed={index === 0 && pressed ? '' : undefined}
            />
          ))}
        </SocialButtonGroup>
        <div className="flex items-center gap-2 text-sm text-tertiary">
          <span className="flex-1 border-t border-secondary" />
          or
          <span className="flex-1 border-t border-secondary" />
        </div>
        <div className="flex flex-col gap-4">
          <Input label="Email" type="email" placeholder="you@example.com" autoComplete="email" />
          <Button>Continue with email</Button>
        </div>
      </div>
      <div className="flex flex-col gap-3">
        <p className="text-sm font-medium text-secondary">Logo only</p>
        <SocialButtonGroup layout="row">
          {PROVIDERS.map((provider) => (
            <SocialButton
              key={provider}
              provider={provider}
              iconOnly
              logo={<PlaceholderMark provider={provider} />}
            />
          ))}
        </SocialButtonGroup>
      </div>
      <div className="flex flex-col gap-3">
        <p className="text-sm font-medium text-secondary">Color and brand themes</p>
        <div className="flex flex-wrap gap-3">
          <SocialButton
            provider="facebook"
            theme="color"
            iconOnly
            logo={<PlaceholderMark provider="facebook" />}
          />
          <SocialButton
            provider="facebook"
            theme="brand"
            iconOnly
            logo={<PlaceholderMark provider="facebook" />}
          />
          <SocialButton provider="apple" theme="brand" iconOnly logo={<PlaceholderMark provider="apple" />} />
          <SocialButton
            provider="google"
            theme="brand"
            iconOnly
            logo={<PlaceholderMark provider="google" />}
          />
        </div>
      </div>
    </div>
  );
}

API

Parts: root (button or link), logo slot (provider logo, 16 or 20px), label ("Sign in with Google"), group (SocialButtonGroup: layout stack, full-width buttons 12px apart, or layout row, logo buttons 12px apart; label default "Sign in options").

PropTypeDefaultDescription
provider"google" | "facebook" | "apple" | "twitter" | "figma" | "dribbble"undefinedRequired. The account provider; sets the default label and the brand colors.
theme"gray" | "color" | "brand""gray"gray: white button with a one-color logo. color: white button with the full-color logo. brand: the provider's own fill with a white label.
size"md" | "lg""md"40 or 44px tall; Text sm or Text md.
iconOnlybooleanfalseShows only the logo, as a square button; the label becomes the aria-label (Figma Supporting text False).
labelstring"Sign in with <provider name>"Button text, for example "Sign up with Google" on a sign-up page.
hrefstringundefinedRenders a link (for example to an OAuth start URL). Without href the root is a button.
disabledbooleanfalseTurns the button off (50% opacity).
EventDescription
clickFires on activation when there is no href.
SlotDescription
logoRequired. The provider logo the app supplies (an SVG or img, aria-hidden). Lunia ships no brand logos.

Motion

Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.

IDTriggerElementPropertyFrom → toTimingReduced motion
SCB1hover (gray and color themes)root, labelbackground-color, colorbg-primary, text-secondary → bg-primary_hover, text-secondary_hoverduration-sm (200)ease-outSwaps at once
SCB2press (pointer down or Space held)rootscale1 → scale-pressduration-sm (200)ease-outStays still
SCB3releaserootscalescale-press → 1duration-sm (200)ease-outStays still
SCB4keyboard focusrootbox-shadowshadow-xs-skeuomorphic → focus ring (2px bg-primary gap, 4px focus-ring)duration-2xs (120)ease-outShows at once

Accessibility

  • Native button, or a link when href is set; the logo is aria-hidden and the label carries the name.
  • iconOnly buttons use label as aria-label ("Sign in with Apple"), so each logo button in a row has a different name.
  • Use theme gray by default: some brand fills do not reach 4.5:1 contrast with their white label (WCAG 1.4.3). Apple, X and Figma (black, 21:1) pass; Facebook (4.2:1) and Dribbble (3.6:1) do not, so use their brand theme only where the provider requires it.
  • Both sizes are 40px or taller, so the target meets the 24 x 24px minimum (WCAG 2.5.8).
  • SocialButtonGroup is a div with role group and an aria-label (default "Sign in options").
  • Reduced motion: colors swap at once and the button does not scale.