# Sign In (Moonarc)

A sign-in card in two steps, email then a one-time code, as the front end of your own auth service. Each step is a real form that posts, so it works with JavaScript off. With JavaScript the core form lib sends each step with fetch, CSS swaps step one for step two when the email is accepted, and the section's script moves the focus to the code field and cleans a pasted code of spaces and dashes. It ships no backend: your service keeps the session and answers both steps.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/sign-in` (license key required)
- Tier B · category section · trigger click
- Readout: `<SignIn email={emailForm.props} code={codeForm.props} success="https://app.example.com/">`
- Browser support: newly (Chrome 119 · Firefox 121 · Safari 16.5); elsewhere: without :has() both steps show at once and each still sends; the code cells keep their resting border.
- Measured cost: 761 B raw JS · 452 B gzip · + runtime + form (with dependencies 5.4 kB raw; CSS 21.2 kB raw)
- Page: https://moonarc.dev/components/sign-in/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `headline` | `string` | none | Headline; required unless the headline slot is filled. |
| `eyebrow` | `string` | none | Short line above the headline. Slot "eyebrow" wins. |
| `copy` | `string` | none | Supporting copy. Slot "copy" wins. |
| `level` | `1 | 2` | `1` | Heading level of the headline: 1 on the sign-in page itself, 2 under another h1 (a demo). |
| `step` | `'email' | 'code'` | `'email'` | email renders both steps, step two hidden until step one answers done. code renders step two alone: the page a native post of step one lands on. |
| `emailLabel` | `string` | `'Email address'` | The email field's visible label (type="email", autocomplete="email", required). |
| `emailName` | `string` | `'email'` | Name of the email field. |
| `emailPlaceholder` | `string` | `'you@example.com'` | Placeholder of the email field. |
| `codeLabel` | `string` | `'Code from the email'` | The code field's visible label (OtpInput, autocomplete="one-time-code", required). |
| `codeName` | `string` | `'code'` | Name of the code field: step two's request carries this field alone. |
| `codeLength` | `number` | `6` | Characters in the code, 4 to 12. |
| `charset` | `'numeric' | 'alphanumeric'` | `'numeric'` | Digits only (a numeric keyboard) or letters and digits. |
| `submitLabels` | `{ idle?, pending?, done?, error? }` | `'Send the code' · 'Sending' · 'Sent' · 'Try again'` | Step one's button faces (StatefulButton); the face that shows is its name. |
| `verifyLabels` | `{ idle?, pending?, done?, error? }` | `'Sign in' · 'Checking' · 'Signed in' · 'Try again'` | Step two's button faces. |
| `resendHref` | `string` | none | A link under step two back to step one, where a new code is asked for: the sign-in page. It works without JavaScript; without it there is no link. |
| `resendLabel` | `string` | `'Send a new code'` | Text of that link. |
| `changeEmailLabel` | `string` | `'Use another email'` | A button under step two that shows step one again with the focus in the email field (two-step form only, JavaScript only: without it the step is a page of its own). |
| `messages` | `{ email?: { pending?, done?, error?, 429?, offline? }, code?: { … } }` | `English texts` | Each step's status texts. Step one's done text ('We sent a code to your email.') is also written into step two's status when the step changes, so a screen reader hears why the page changed. A service's own text is never shown. |
| `demoLabel` | `string` | `'Demo: nothing is sent.'` | The visible note under the card when a step is a demo form. |
| `email` | `{ action, endpoint?, encode?, honeypot?, formAttrs?, demo?, demoResponse? }` | none | Step one's form: formProps() with the custom adapter, or demo. action is required unless demo. A Netlify or Formspree form fails the build: a sign-in step posts to your own endpoint. |
| `code` | `{ action, endpoint?, encode?, honeypot?, formAttrs?, demo?, demoResponse? }` | none | Step two's form, the same shape. action is required unless demo. |
| `success` | `string` | none | Where step two sends the reader after done: your app, a path here or an absolute URL on another origin. Nothing is written to storage for it. Unset, the card says done and stays. |
| `backdrop` | `boolean` | `false` | A GridPattern behind the card (aria-hidden), in the tone's text colour. |
| `tone` | `'surface' | 'muted' | 'ink' | 'brand'` | `unset (paints nothing)` | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. Each paints --background, --ma-tone-muted (over --muted, with a --border hairline), --ma-tone-ink or --ma-tone-brand (over --primary) with the matching text. With theme.css, a painted tone also re-declares --card, --border, --input and --ring inside the section. The card takes the same ground and the buttons invert on ink and brand. |
| `density` | `'compact' | 'cozy' | 'roomy'` | `'cozy'` | Block padding and the card's padding ×0.8 / ×1 / ×1.25 through --ma-density. |
| `align` | `'start' | 'center'` | `'center'` | The card centred, or at the start of the section. |

## Usage

```astro
---
import SignIn from '@/components/moonarc/pro/SignIn.astro';
// your auth service's two endpoints; with the template kit, formProps({ adapter: 'custom', endpoint, success: false,
// refParam: false, carry: false, adapters: ['custom', 'demo'] }).props gives the same objects
const email = { action: '/api/auth/email', encode: 'json' as const };
const code = { action: '/api/auth/code', encode: 'json' as const };
---
<!-- The contract your service answers, for both steps:
     step one  POST email=…      JSON (fetch): 2xx { ok: true } sends the code; 429 too many tries; any other status fails.
                                 Native post: 303 to the page that renders <SignIn step="code" … />.
     step two  POST code=…       the code alone; the session (a cookie your service set in step one) says whose it is.
                                 JSON (fetch): 2xx { ok: true } signs in and the page goes to success; 4xx fails.
                                 Native post: 303 to your app, or back to the code page on a wrong code.
     Each request sends Accept: application/json; a fetch reads the status and the ok flag only, never your text. -->
<SignIn
  headline="Sign in"
  copy="We email you a six-digit code. No password."
  email={email}
  code={code}
  success="https://app.example.com/"
  resendHref="/sign-in/"
  backdrop
>
  <a slot="providers" href="/api/auth/sso">Continue with single sign-on</a>
</SignIn>

<!-- /sign-in/code/, the page step one's native post lands on (noindex): step two alone -->
<SignIn headline="Check your email" step="code" code={code} success="https://app.example.com/" resendHref="/sign-in/" />
```

## Reduced motion

The step change is instant in every mode (display, nothing travels). Under reduced motion the buttons change faces in place and the code cells and their check draw without animation.

## With ClientRouter

The script binds through the runtime's onMount (bindForms() for the two forms, the paste, the focus move and the back button), so a card that arrives by a ClientRouter navigation is bound fresh and its listeners go with the page. Leaving for success is a full navigation.

## Craft

- The two steps are two forms, each with its own role="status" line present at load: step one's request carries the email, step two's the code alone. Your service keeps the session between them, usually with a cookie set in step one.
- The step change is CSS, :has() on the email form's data-state. The script moves the focus to the code field one frame later, because the button that had it is hidden. That move only ever follows the reader's own submit.
- OtpInput is one native input with maxlength, so a pasted "123-456" would keep "123-45". The script strips spaces and dashes from the pasted text before the field counts it.
- Nothing is written to storage, and only post is accepted, so the email and the code never reach a URL, the history or a log. success may be on another origin (your app); the lib writes its record only for a page on this one.
- Without JavaScript each step posts natively: step one's endpoint answers with a 303 to a page rendering step="code", step two's with a 303 to your app. The resend link goes back to step one and needs no script.
- There is no auth backend in the section, by design: an auth service you cannot audit is a liability, so the section is the front end and the request and response contract is in the usage.

## Replaces

- Sign-in pages (Tailwind Plus)
- Login (shadcnblocks)
- Auth UI OTP flows (Clerk, Supabase UI)

## Source

Pro block. The source is served by the license-gated registry: `npx shadcn@latest add @moonarc-pro/sign-in` with a key in components.json (https://moonarc.dev/account/). Everything it composes is free: otp-input, stateful-button, grid-pattern.
