Social buttons
Stable“Continue with GitHub” and “Continue with Google” at equal weight, with a waiting state while the provider answers.
Both buttons use the pearl finish (graphite in dark mode), which sits close to Google’s own Neutral and Dark button themes, so neither provider looks more important. SocialButtons lays out a set and handles the pending state; SocialButton is one on its own. Google’s G keeps its four colours, the one exception to the kit’s black and white.
Stacked
import { useState } from 'react'import { SocialButtons, type Provider } from '@rchursin/product/forms'
const names: Record<Provider, string> = { github: 'GitHub', google: 'Google' }
/** Press one: it waits for the provider with a spinner (still focusable) while the other steps back. */export default function SocialButtonsStack() { const [pending, setPending] = useState<Provider | null>(null) const [result, setResult] = useState('')
function select(p: Provider) { setPending(p) setResult('') // In the app: redirect to the provider, or call api.signInWithProvider(p) window.setTimeout(() => { setPending(null) setResult(`Sample: ${names[p]} would open its sign-in page here.`) }, 1400) }
return ( <div className="flex w-full max-w-xs flex-col gap-3"> <SocialButtons onSelect={select} pending={pending} /> <p role="status" className="min-h-5 text-center text-xs text-pretty text-fg-3"> {pending ? `Contacting ${names[pending]}…` : result} </p> </div> )}Usage
import { SocialButtons, SocialButton, type Provider } from '@rchursin/product/forms'<SocialButtons onSelect={(provider) => signInWith(provider)} pending={waitingFor} />Examples
Side by side
layout="row" with verb="Sign up". Each button needs 11rem, so on a phone they go back to one per row.
import { SocialButtons } from '@rchursin/product/forms'import { useToast } from '@rchursin/product/ui'
/** Side by side on a sign-up form. Each needs 11rem, so on a phone they fall back to one per row. */export default function SocialButtonsRow() { const toast = useToast() return ( <div className="flex w-full max-w-md flex-col gap-4"> <p className="text-[13px] text-fg-2">Create your account with</p> <SocialButtons layout="row" verb="Sign up" onSelect={(p) => toast({ title: `Sign up with ${p === 'github' ? 'GitHub' : 'Google'}`, description: 'Sample: in the app this opens the provider’s sign-in.' })} /> </div> )}One at a time
SocialButton for your own layout: ready, waiting, disabled, and the default verb.
import { SocialButton } from '@rchursin/product/forms'import { useToast } from '@rchursin/product/ui'
const label = 'font-mono text-[10px] font-medium tracking-[0.14em] text-fg-3 uppercase'
/** One SocialButton at a time, for your own layout. The three verbs Google allows, and each state. */export default function SocialButtonStates() { const toast = useToast() const press = (what: string) => () => toast({ title: what, description: 'Sample: in the app this opens the provider’s sign-in.' }) return ( <div className="grid w-full max-w-lg grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2"> <div className="flex flex-col gap-2"> <p className={label}>Ready</p> <SocialButton provider="github" verb="Sign in" onClick={press('Sign in with GitHub')} /> </div> <div className="flex flex-col gap-2"> <p className={label}>Waiting for Google</p> <SocialButton provider="google" verb="Sign in" pending /> </div> <div className="flex flex-col gap-2"> <p className={label}>Disabled while another waits</p> <SocialButton provider="google" verb="Sign up" disabled /> </div> <div className="flex flex-col gap-2"> <p className={label}>Default verb</p> <SocialButton provider="google" onClick={press('Continue with Google')} /> </div> </div> )}API
SocialButtons props
| Prop | Type | Default | Description |
|---|---|---|---|
onSelectrequired | (provider: Provider) => void | – | Called with "github" or "google". Set pending to that provider while you wait. |
pending | Provider | null | null | The provider being contacted: its button shows a spinner and ignores presses; the others are disabled. |
disabled | boolean | – | Disables every button, e.g. while another sign-in method is busy. |
verb | 'Continue' | 'Sign in' | 'Sign up' | 'Continue' | The three verbs Google’s guidelines allow: “Continue with”, “Sign in with”, “Sign up with”. |
layout | 'stack' | 'row' | 'stack' | One per row, or side by side (they wrap when narrower than 11rem each). |
providers | Provider[] | ['github', 'google'] | Which buttons, in order. |
className | string | – | Classes on the grid. |
SocialButton props
| Prop | Type | Default | Description |
|---|---|---|---|
providerrequired | Provider | – | "github" or "google". |
onClick | () => void | – | Ignored while pending. |
pending | boolean | – | Spinner in place of the logo and aria-busy="true"; the button stays focusable. |
disabled | boolean | – | A real disabled button at 45% opacity. |
verb | 'Continue' | 'Sign in' | 'Sign up' | 'Continue' | The first words of the label. |
className | string | – | Classes on the button. It is 40px tall and fills its column. |
GoogleG props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | – | Google’s four-colour G as an inline 18px SVG, hidden from screen readers. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves between the buttons. A disabled one is skipped. |
| EnterSpace | Chooses the provider. |
- Real <button type="button"> elements, named by their words: “Continue with GitHub”.
- While pending, the button sets aria-busy="true", ignores presses and keeps focus, so a second press can’t start a second sign-in.
- The GitHub icon and the Google G are aria-hidden; the label says which provider it is.
- Each button is 40px tall, above the 24px minimum target.
Guidelines
Do
- Give every provider the same finish and size; Google requires its button to be at least as prominent as any other provider’s.
- Set pending while you wait for the provider, and clear it if the person comes back without signing in.
- Keep the order the same on every page: GitHub, then Google.
Don’t
- Don’t recolour, outline or resize the Google G; its guidelines forbid a one-colour G.
- Don’t make one provider the primary button.
- Don’t write your own verbs (“Log in via Google”); use the three Google allows.
In use
Related
- Sign-in flowThe whole front door in one component: email first, password or passkey, GitHub or Google, sign-up with a code, and recovery.
- Auth layoutThe frame for every sign-in page: a dark brand panel when there is room, one calm form column, the logo on top and legal links below.
- ButtonThe lit-metal button in five finishes and five sizes, with icons, a loading state and shortcut hints.
- SpinnerThe loader icon turning once every 900 ms, only when motion is allowed. Decorative next to words; give it a label when it stands alone.
Source
The component’s own files, exactly as they run on this site.
/** * "Continue with GitHub" / "Continue with Google". * * Both use the pearl button (graphite in dark mode), so neither is more prominent than the * other — Google requires its button to be at least as prominent as any other provider's. * The pearl and graphite faces sit close to Google's own Neutral (#F2F2F2) and Dark (#131314) * button themes. * * <SocialButtons onSelect={(provider) => …} pending={provider} /> */import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"import { Spinner } from "../ui/button"
export type Provider = "github" | "google"
const names: Record<Provider, string> = { github: "GitHub", google: "Google" }
/** * Google's "G", inlined from Google's sign-in branding guidelines (four paths, official colours).