Auth layout
StableThe 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.
AuthLayout holds a sign-in, sign-up or recovery page. It responds to its own width (container queries), not the window’s, so it works as a whole page, in a preview or in a dialog: from 48rem the always-dark brand panel joins on the left. It renders no heading of its own; bring one.
A sign-in page
import { useRef, useState, type FormEvent, type MouseEvent } from 'react'import { flushSync } from 'react-dom'import { AuthLayout, PasswordInput, checkEmail, messages, useForm, type AuthLink } from '@rchursin/product/forms'import { Button, Field, TextInput, useToast } from '@rchursin/product/ui'
const link = 'lux-link lux-link-under rounded-sm font-medium text-fg focus-ring'
/** Sample footer links: '#' addresses, so nothing here points at pages this site doesn't have */const demoLinks: AuthLink[] = [ { label: 'Terms', href: '#terms' }, { label: 'Privacy', href: '#privacy' },]
/** * A whole sign-in page: AuthLayout is the <main>, the page has one <h1>, and the form checks * itself on submit. The brand panel joins on the left once the page is 48rem wide. */export default function AuthLayoutPage() { const toast = useToast() const [status, setStatus] = useState('')Usage
import { AuthLayout, type AuthLink } from '@rchursin/product/forms'<AuthLayout as="main" className="min-h-dvh" topRight={<>New here? <a href="/sign-up">Create an account</a></>}> <h1>Sign in to RChursin_</h1> {/* your form */}</AuthLayout>Examples
Your own panel
panel swaps the statement for yours (here, a sample invitation). The logo and the standards line stay. Below 48rem the panel is gone, so the form side says everything needed.
import { useState } from 'react'import { AuthLayout, SocialButtons, type AuthLink, type Provider } from '@rchursin/product/forms'import { FrameCorners } from '@rchursin/home/ui'import { Icon } from '@rchursin/product/icons/Icon'import { useToast } from '@rchursin/product/ui'
const names: Record<Provider, string> = { github: 'GitHub', google: 'Google' }
/** Sample footer links: '#' addresses, so nothing here points at pages this site doesn't have */const demoLinks: AuthLink[] = [ { label: 'Terms', href: '#terms' }, { label: 'Privacy', href: '#privacy' },]
/** * `panel` replaces the statement on the dark side; the logo and the standards line stay. The * panel hides below 48rem, so nothing in it may be needed to finish the form. */export default function AuthLayoutCustomPanel() { const [pending, setPending] = useState<Provider | null>(null)Without the panel
brandPanel={false} for short pages, with four footer links of your own.
import { useState } from 'react'import { AuthLayout, type AuthLink } from '@rchursin/product/forms'import { Icon } from '@rchursin/product/icons/Icon'import { Button, useToast } from '@rchursin/product/ui'
/** Your own footer links. Sample '#' addresses here, so nothing points at pages this site doesn't have */const links: AuthLink[] = [ { label: 'Help', href: '#help' }, { label: 'Status', href: '#status' }, { label: 'Terms', href: '#terms' }, { label: 'Privacy', href: '#privacy' },]
/** brandPanel={false} for short pages (an expired link, a sign-out notice), with your own footer links. */export default function AuthLayoutCompact() { const [state, setState] = useState<'idle' | 'sending' | 'sent'>('idle') const toast = useToast()
function resend() { if (state === 'sending') returnAPI
Auth layout props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The page: a heading and a form. They sit in a column 23rem wide. |
topRight | ReactNode | – | Right of the logo row, in small grey text: “New here? Create an account”. |
brandPanel | boolean | true | Show the dark brand panel when the frame is at least 48rem (768px) wide. |
panel | ReactNode | – | Replaces the panel’s statement. The logo at the top and the standards line at the bottom stay. |
links | AuthLink[] | Terms (/terms), Privacy (/privacy) | Footer links, listed after the © line. |
onLinkClick | (link: AuthLink, e: MouseEvent<HTMLAnchorElement>) => void | – | Intercept the footer links, e.g. to route them yourself or keep a preview on the page. |
as | 'main' | 'div' | 'div' | The element around the form column. Use "main" when the layout is the whole page. |
className | string | – | Classes on the outer frame. A min-height set here (min-h-dvh) is inherited by both columns. |
AuthLink
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | – | The link text. |
hrefrequired | string | – | Where it goes. Also the React key, so keep each one different. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Logo row (topRight links), then your form, then the footer links. The brand panel holds nothing focusable. |
- With as="main" only the form column is the <main>. The footer is a <footer>, so on a page of its own it is the contentinfo landmark; inside another page’s <main> it is just a footer.
- The legal links are a list named “Legal”.
- At any width the logo is read once: when the panel shows, the logo in the top row is visibility: hidden, and below 48rem the whole panel is display: none. The verdict badges in the panel are aria-hidden.
- The panel is always dark (it carries the dark class), so its text keeps its contrast in both themes.
- It renders no heading: a page needs exactly one <h1> inside it.
Guidelines
Do
- Use as="main", one <h1> and min-h-dvh when the layout is the whole page.
- Keep one task per page; the 23rem column is narrow on purpose.
- Put the way to the other flow in topRight: “New here? Create an account”.
Don’t
- Don’t put anything people need into panel: it is hidden below 48rem.
- Don’t use as="main" inside a page that already has a <main>.
- Don’t recolour the panel to match the theme; it stays dark in both.
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.
- Social buttons“Continue with GitHub” and “Continue with Google” at equal weight, with a waiting state while the provider answers.
- LogoThe 09 Frame mark and the RChursin_ wordmark in four lockups, drawn as one SVG that takes the text colour.
- Frame cornersThe logo’s two shapes as a viewfinder: an L bottom-left and a square top-right, drawn around any box.
- Light & grainA ceiling halo for heroes, film grain for flat grounds, a fade for product shots and a shadow that follows torn paper.
- Verdict badgeKeep, fix, merge or cut: a mono badge where each verdict has its own shape, and the word is always there.
Source
The component’s own files, exactly as they run on this site.
/** * The frame for every sign-in page: a quiet brand panel on the left (when there is room), the * form in a calm column on the right, the logo on top and Terms / Privacy at the foot. * * <AuthLayout as="main" topRight={<>New here? <a href="/sign-up">Create an account</a></>}> * <h1>Sign in to RChursin_</h1> … * </AuthLayout> * * It responds to its own width (container queries), not the window's, so it works full-page * and inside a preview frame. The brand panel is always dark: graphite in light mode, the * deepest black in dark mode. */import type { MouseEvent, ReactNode } from "react"import { Logo } from "../../brand/Logo"import { cx } from "../../components/cx"import { FrameCorners, VerdictBadge } from "../../home/ui"import { useCurrentYear } from "../lib/hydration"
export interface AuthLink { label: string