# OAuthButtons

> Provider sign-in buttons for Google, GitHub, Apple and Microsoft with their official logos, stacked, in a grid or icon-only, with per-provider loading.

Source: https://docs.nasaqui.com/components/oauth-buttons

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/oauth-buttons.json
```

A group of "Continue with X" buttons. Each provider's logo is the official artwork in its official colours; the
button text uses the approved verbs (Continue / Sign in / Sign up with). Clicking one shows a spinner on that
button and disables the others until your handler settles.

## When to use

- Under or above a sign-in or sign-up form.
- `intent="signup"` on registration screens, `"signin"` on login.

## When not to use

- Enterprise SAML or a provider list you fully own: pass `OAuthCustomProvider` items, or build a menu.
- Connecting accounts inside settings: use a connected-accounts list.

## Import

```tsx
import { OAuthButtons } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { OAuthButtons } from "@fadymondy/nasaq/web";

export function Providers() {
  return <OAuthButtons providers={["google", "github", "apple"]} onSelect={(id) => startOAuth(id)} />;
}

declare function startOAuth(id: string): Promise<void>;
```

## Anatomy

```
OAuthButtons                     data-slot="oauth-buttons", role="group"
└─ Button (one per provider)     data-slot="oauth-button", data-provider
   ├─ logo                        official SVG, aria-hidden
   ├─ label                       "Continue with Google"
   └─ LastUsed (optional)         data-slot="last-used": the "Last used" pill at the inline end
OAuthDivider                     data-slot="oauth-divider" (the "or" rule)
```

## API

**OAuthButtons**: every `div` prop except `onSelect`, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `providers` | `OAuthProvider[]` | `["google", "github"]` | Ids (`google`, `github`, `apple`, `microsoft`) or custom `{ id, label, icon }`. |
| `onSelect` | `(id: string) => void \| Promise<unknown>` | required | Called with the provider id. A returned promise drives the loading state. |
| `layout` | `"stack" \| "grid" \| "icon-only"` | `"stack"` | Full-width rows, two columns, or square icon buttons. |
| `intent` | `"signin" \| "signup" \| "continue"` | `"continue"` | Which verb the labels use. |
| `pendingProvider` | `string \| null` | | Hold a provider in the loading state from outside (redirect flows). |
| `lastUsed` | `string \| null` | | Provider id to mark "Last used" (not shown in `icon-only`). Read it from your own cookie; the component stores nothing. |
| `disabled` | `boolean` | `false` | Disable all buttons. |
| `labels` | `Partial<OAuthButtonsLabels>` | English or Arabic | `signin`, `signup`, `continue` (use `{provider}`), `group`, `divider`, `lastUsed`. |

**LastUsed**: the small pill on its own, for other sign-in buttons (the passkey button in `SignInFlow` uses it).
Give its parent `relative`.

**OAuthDivider**: a horizontal rule with centred text. `children` defaults to "or" / "أو".

Logos: Google (multicolour "G"), GitHub (Octicon mark, follows text colour), Apple (black on light, white on dark)
and Microsoft (four squares) are the official marks, never recoloured, mirrored or redrawn. Source URLs are in
`oauth-logos.tsx`.

## Examples

**Sign-up, two columns**

```tsx
import { OAuthButtons, OAuthDivider } from "@fadymondy/nasaq/web";

export function SignUpProviders() {
  return (
    <>
      <OAuthButtons layout="grid" intent="signup" providers={["google", "microsoft"]} onSelect={begin} />
      <OAuthDivider />
    </>
  );
}

declare function begin(id: string): Promise<void>;
```

**A custom provider**

```tsx
import { OAuthButtons } from "@fadymondy/nasaq/web";

export function Sso() {
  return (
    <OAuthButtons
      providers={["google", { id: "okta", label: "Continue with Okta", icon: <span aria-hidden="true">O</span> }]}
      onSelect={() => {}}
    />
  );
}
```

## Accessibility

- The group has an accessible name (`labels.group`). Each button's name is the full text ("Continue with Google"), the
  logo is `aria-hidden`. In `icon-only` layout the text is kept as the accessible name.
- Loading buttons are `aria-busy`; the other buttons are disabled while one is pending.
- The "Last used" text is inside the button, so it is read as part of its name.
- All buttons are keyboard operable and show the standard focus ring.

## RTL & i18n

- Logos are never mirrored, in either direction. Only the position of the icon relative to the text flips.
- Provider names stay in Latin script inside Arabic labels (`المتابعة عبر Google`), as the brands require.
- The divider text follows the locale ("أو").

## Styling & tokens

- Buttons are the secondary `Button`; Apple is a black button in light theme and white in dark, per Apple's guidelines.
- Brand colours live only inside the logo SVGs. Everything else uses `--nq-*` tokens.

## Do / Don't

- Do keep the provider order the same on sign-in and sign-up.
- Do use the official verbs through `intent`.
- Don't restyle, recolour or redraw a provider logo.
- Don't show a provider you have not configured.

## Related

- [`login-form`](https://docs.nasaqui.com/components/login-form)
- [`register-form`](https://docs.nasaqui.com/components/register-form)
- [`button`](https://docs.nasaqui.com/components/button)
- [`auth-layout`](https://docs.nasaqui.com/components/auth-layout)

## Lab

https://docs.nasaqui.com/?path=/docs/components-auth-oauth-buttons--docs

## Code

### React

```tsx
import { OAuthButtons } from "@fadymondy/nasaq/web";

export function Providers() {
  return <OAuthButtons providers={["google", "github", "apple"]} onSelect={(id) => startOAuth(id)} />;
}

declare function startOAuth(id: string): Promise<void>;
```

### shadcn

```tsx
import { OAuthButtons } from "@/components/ui/oauth-buttons";

export function Providers() {
  return <OAuthButtons providers={["google", "github", "apple"]} onSelect={(id) => startOAuth(id)} />;
}

declare function startOAuth(id: string): Promise<void>;
```

### Vue

```vue
<script setup lang="ts">
import { NqOAuthButtons } from "@fadymondy/nasaq/vue";

function startOAuth(id: string) {
  return new Promise<void>((resolve) => setTimeout(resolve, 1500));
}
</script>

<template>
  <div class="w-72">
    <NqOAuthButtons :providers="['google', 'github', 'apple']" :on-select="startOAuth" />
  </div>
</template>
```

### Blade

```blade
<div class="w-72">
    <x-nq::oauth-buttons :providers="['google', 'github', 'apple']" />
</div>
```

### HTML + Alpine

```html
<div class="w-72">
    <div role="group" aria-label="Sign in with a provider" data-slot="oauth-buttons" data-layout="stack"
    x-data="nqOAuthButtons(null, false)" x-modelable="pending"
    class="flex w-full flex-col gap-2">
                    <button data-slot="oauth-button"
     type="button"                         data-provider="google" x-on:click="select(&#039;google&#039;)" x-bind:disabled="isDisabled(&#039;google&#039;)" x-bind:aria-busy="isBusy(&#039;google&#039;) ? &#039;true&#039; : null" x-bind:data-disabled="(disabled || isBusy(&#039;google&#039;)) ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)] relative w-full min-w-0 justify-center gap-3 [&_svg]:size-auto">
        <span class="contents" x-show="! isBusy('google')" >
                                    <svg viewBox="10 10 20 20" width="20" height="20" aria-hidden="true" focusable="false" >
    <path d="M29.6 20.2273C29.6 19.5182 29.5364 18.8364 29.4182 18.1818H20V22.05H25.3818C25.15 23.3 24.4455 24.3591 23.3864 25.0682V27.5773H26.6182C28.5091 25.8364 29.6 23.2727 29.6 20.2273Z" fill="#4285F4" />
    <path d="M20 30C22.7 30 24.9636 29.1045 26.6181 27.5773L23.3863 25.0682C22.4909 25.6682 21.3454 26.0227 20 26.0227C17.3954 26.0227 15.1909 24.2636 14.4045 21.9H11.0636V24.4909C12.7091 27.7591 16.0909 30 20 30Z" fill="#34A853" />
    <path d="M14.4045 21.9C14.2045 21.3 14.0909 20.6591 14.0909 20C14.0909 19.3409 14.2045 18.7 14.4045 18.1V15.5091H11.0636C10.3864 16.8591 10 18.3864 10 20C10 21.6136 10.3864 23.1409 11.0636 24.4909L14.4045 21.9Z" fill="#FBBC04" />
    <path d="M20 13.9773C21.4681 13.9773 22.7863 14.4818 23.8227 15.4727L26.6909 12.6045C24.9591 10.9909 22.6954 10 20 10C16.0909 10 12.7091 12.2409 11.0636 15.5091L14.4045 18.1C15.1909 15.7364 17.3954 13.9773 20 13.9773Z" fill="#E94235" />
</svg>
                            </span>
            <span class="contents" x-show="isBusy('google')"  style="display: none" >
                <svg data-slot="spinner" aria-hidden="true" class="animate-spin motion-reduce:animate-none size-4!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>            </span>
                                                <span class="truncate">Continue with <bdi>Google</bdi></span></button>
                    <button data-slot="oauth-button"
     type="button"                         data-provider="github" x-on:click="select(&#039;github&#039;)" x-bind:disabled="isDisabled(&#039;github&#039;)" x-bind:aria-busy="isBusy(&#039;github&#039;) ? &#039;true&#039; : null" x-bind:data-disabled="(disabled || isBusy(&#039;github&#039;)) ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)] relative w-full min-w-0 justify-center gap-3 [&_svg]:size-auto">
        <span class="contents" x-show="! isBusy('github')" >
                                    <svg viewBox="0 0 98 96" width="20" height="20" aria-hidden="true" focusable="false" >
    <path d="M41.4395 69.3848C28.8066 67.8535 19.9062 58.7617 19.9062 46.9902C19.9062 42.2051 21.6289 37.0371 24.5 33.5918C23.2559 30.4336 23.4473 23.7344 24.8828 20.959C28.7109 20.4805 33.8789 22.4902 36.9414 25.2656C40.5781 24.1172 44.4062 23.543 49.0957 23.543C53.7852 23.543 57.6133 24.1172 61.0586 25.1699C64.0254 22.4902 69.2891 20.4805 73.1172 20.959C74.457 23.543 74.6484 30.2422 73.4043 33.4961C76.4668 37.1328 78.0937 42.0137 78.0937 46.9902C78.0937 58.7617 69.1934 67.6621 56.3691 69.2891C59.623 71.3945 61.8242 75.9883 61.8242 81.252L61.8242 91.2051C61.8242 94.0762 64.2168 95.7031 67.0879 94.5547C84.4102 87.9512 98 70.6289 98 49.1914C98 22.1074 75.9883 6.69539e-07 48.9043 4.309e-07C21.8203 1.92261e-07 -1.9479e-07 22.1074 -4.3343e-07 49.1914C-6.20631e-07 70.4375 13.4941 88.0469 31.6777 94.6504C34.2617 95.6074 36.75 93.8848 36.75 91.3008L36.75 83.6445C35.4102 84.2188 33.6875 84.6016 32.1562 84.6016C25.8398 84.6016 22.1074 81.1563 19.4277 74.7441C18.375 72.1602 17.2266 70.6289 15.0254 70.3418C13.877 70.2461 13.4941 69.7676 13.4941 69.1934C13.4941 68.0449 15.4082 67.1836 17.3223 67.1836C20.0977 67.1836 22.4902 68.9063 24.9785 72.4473C26.8926 75.2227 28.9023 76.4668 31.2949 76.4668C33.6875 76.4668 35.2187 75.6055 37.4199 73.4043C39.0469 71.7773 40.291 70.3418 41.4395 69.3848Z"  class="fill-black dark:fill-white"  />
</svg>
                            </span>
            <span class="contents" x-show="isBusy('github')"  style="display: none" >
                <svg data-slot="spinner" aria-hidden="true" class="animate-spin motion-reduce:animate-none size-4!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>            </span>
                                                <span class="truncate">Continue with <bdi>GitHub</bdi></span></button>
                    <button data-slot="oauth-button"
     type="button"                         data-provider="apple" x-on:click="select(&#039;apple&#039;)" x-bind:disabled="isDisabled(&#039;apple&#039;)" x-bind:aria-busy="isBusy(&#039;apple&#039;) ? &#039;true&#039; : null" x-bind:data-disabled="(disabled || isBusy(&#039;apple&#039;)) ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 h-control px-[var(--nq-control-pad)] relative w-full min-w-0 justify-center gap-3 border-black bg-black text-white hover:bg-black/90 dark:border-white dark:bg-white dark:text-black dark:hover:bg-white/90 [&_svg]:size-auto">
        <span class="contents" x-show="! isBusy('apple')" >
                                    <svg viewBox="20 15.5 16 20" width="14" height="17.5" aria-hidden="true" focusable="false" >
    <path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" fill-rule="nonzero"  class="fill-white dark:fill-black"  />
</svg>
                            </span>
            <span class="contents" x-show="isBusy('apple')"  style="display: none" >
                <svg data-slot="spinner" aria-hidden="true" class="animate-spin motion-reduce:animate-none size-4!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>            </span>
                                                <span class="truncate">Continue with <bdi>Apple</bdi></span></button>
    </div>
</div>
```
