Auth layout
A full-height page that centers a single form, for log in, sign up and account recovery screens.
<AuthLayout>
<form className="grid w-full max-w-sm grid-cols-1 gap-8">{/* ... */}</form>
</AuthLayout><AuthLayout>
<form class="grid w-full max-w-sm grid-cols-1 gap-8"><!-- ... --></form>
</AuthLayout>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
import type React from 'react'
const card = [
'flex grow items-center justify-center p-6 lg:p-10',
'lg:rounded-lg lg:bg-white dark:lg:bg-zinc-900 lg:shadow-xs lg:ring-1 lg:ring-zinc-950/5 dark:lg:ring-white/10',
].join(' ')
export function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<main className="flex min-h-dvh flex-col p-2">
<div className={card}>{children}</div>
</main>
)
}<script setup lang="ts">
const card = [
'flex grow items-center justify-center p-6 lg:p-10',
'lg:rounded-lg lg:bg-white dark:lg:bg-zinc-900 lg:shadow-xs lg:ring-1 lg:ring-zinc-950/5 dark:lg:ring-white/10',
].join(' ')
</script>
<template>
<main class="flex min-h-dvh flex-col p-2">
<div :class="card">
<slot />
</div>
</main>
</template>Component API
| Prop | Default | Description |
|---|---|---|
AuthLayoutrenders a <main> element | ||
children | - | Content centered on both axes, usually a form. |
| Prop | Default | Description |
|---|---|---|
AuthLayoutrenders a <main> element | ||
#default | - | Slot for the content centered on both axes, usually a form. |
Usage
Use it for screens a visitor sees before entering the app, where one task fills the page and no navigation should compete with it. The layout fills the viewport height and centers its content. From lg up it also draws a white panel with a hairline ring around that content.
The layout does not limit the width of what you put inside. Give the form a max-w-sm and w-full so it stays readable on a wide screen and shrinks to fit on a phone.
Screens
Log in with a passkey or a password
Put the passkey button above the fields so returning people skip the password, and separate it from the email form with a labelled Divider. The password field can show what was typed, and recovery options sit under the submit button.
import { FingerPrintIcon } from '@heroicons/react/16/solid'
import { AuthLayout } from '@/components/auth-layout'
import { Button } from '@/components/button'
import { Divider } from '@/components/divider'
import { Field, Label } from '@/components/fieldset'
import { Heading } from '@/components/heading'
import { Input } from '@/components/input'
import { PasswordInput } from '@/components/password-input'
import { Strong, Text, TextLink } from '@/components/text'
import { Logo } from './logo'
export default function LogIn() {
return (
<AuthLayout>
<form action="/session" method="POST" className="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo className="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Log in to Northwind</Heading>
<Button type="button" color="light" className="w-full">
<FingerPrintIcon />
Sign in with a passkey
</Button>
<Divider>or</Divider>
<Field>
<Label>Work email</Label>
<Input type="email" name="email" autoComplete="email" />
</Field>
<Field>
<Label>Password</Label>
<PasswordInput name="password" autoComplete="current-password" />
</Field>
<Button type="submit" className="w-full">
Log in
</Button>
<Text>
Lost access? <TextLink href="/recover">Get a reset link</TextLink> or{' '}
<TextLink href="/magic-link">email me a one-time sign-in link</TextLink>.
</Text>
<Text>
New to Northwind?{' '}
<TextLink href="/signup">
<Strong>Create a workspace</Strong>
</TextLink>
</Text>
</form>
</AuthLayout>
)
}<script setup lang="ts">
import { FingerPrintIcon } from '@heroicons/vue/16/solid'
import AuthLayout from '@/components/auth-layout.vue'
import Button from '@/components/button.vue'
import Divider from '@/components/divider.vue'
import Field from '@/components/field.vue'
import Heading from '@/components/heading.vue'
import Input from '@/components/input.vue'
import Label from '@/components/label.vue'
import PasswordInput from '@/components/password-input.vue'
import Strong from '@/components/strong.vue'
import Text from '@/components/text.vue'
import TextLink from '@/components/text-link.vue'
import Logo from './Logo.vue'
</script>
<template>
<AuthLayout>
<form action="/session" method="POST" class="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo class="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Log in to Northwind</Heading>
<Button type="button" color="light" class="w-full">
<FingerPrintIcon />
Sign in with a passkey
</Button>
<Divider>or</Divider>
<Field>
<Label>Work email</Label>
<Input type="email" name="email" autocomplete="email" />
</Field>
<Field>
<Label>Password</Label>
<PasswordInput name="password" autocomplete="current-password" />
</Field>
<Button type="submit" class="w-full">Log in</Button>
<Text>
Lost access? <TextLink href="/recover">Get a reset link</TextLink> or
<TextLink href="/magic-link">email me a one-time sign-in link</TextLink>.
</Text>
<Text>
New to Northwind?
<TextLink href="/signup"><Strong>Create a workspace</Strong></TextLink>
</Text>
</form>
</AuthLayout>
</template>Create a workspace
Ask only what the first screen of the app needs. A Description under a label explains where a value shows up, and autocomplete hints let browsers fill the business name and email.
import { AuthLayout } from '@/components/auth-layout'
import { Button } from '@/components/button'
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Description, Field, Label } from '@/components/fieldset'
import { Heading } from '@/components/heading'
import { Input } from '@/components/input'
import { Select } from '@/components/select'
import { Strong, Text, TextLink } from '@/components/text'
import { Logo } from './logo'
export default function CreateWorkspace() {
return (
<AuthLayout>
<form action="/workspaces" method="POST" className="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo className="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Create a workspace</Heading>
<Field>
<Label>Business name</Label>
<Description>Printed on invoices and receipts.</Description>
<Input name="workspace" autoComplete="organization" />
</Field>
<Field>
<Label>Work email</Label>
<Input type="email" name="email" autoComplete="email" />
</Field>
<Field>
<Label>Password</Label>
<Input type="password" name="password" autoComplete="new-password" />
</Field>
<Field>
<Label>Settlement currency</Label>
<Select name="currency">
<option value="XOF">West African CFA franc (XOF)</option>
<option value="NGN">Nigerian naira (NGN)</option>
<option value="KES">Kenyan shilling (KES)</option>
<option value="GHS">Ghanaian cedi (GHS)</option>
<option value="USD">US dollar (USD)</option>
</Select>
</Field>
<CheckboxField>
<Checkbox name="terms" />
<Label>I accept the terms of service</Label>
</CheckboxField>
<Button type="submit" className="w-full">
Create workspace
</Button>
<Text>
Already on Northwind?{' '}
<TextLink href="/login">
<Strong>Log in</Strong>
</TextLink>
</Text>
</form>
</AuthLayout>
)
}<script setup lang="ts">
import AuthLayout from '@/components/auth-layout.vue'
import Button from '@/components/button.vue'
import Checkbox from '@/components/checkbox.vue'
import CheckboxField from '@/components/checkbox-field.vue'
import Description from '@/components/description.vue'
import Field from '@/components/field.vue'
import Heading from '@/components/heading.vue'
import Input from '@/components/input.vue'
import Label from '@/components/label.vue'
import Select from '@/components/select.vue'
import Strong from '@/components/strong.vue'
import Text from '@/components/text.vue'
import TextLink from '@/components/text-link.vue'
import Logo from './Logo.vue'
</script>
<template>
<AuthLayout>
<form action="/workspaces" method="POST" class="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo class="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Create a workspace</Heading>
<Field>
<Label>Business name</Label>
<Description>Printed on invoices and receipts.</Description>
<Input name="workspace" autocomplete="organization" />
</Field>
<Field>
<Label>Work email</Label>
<Input type="email" name="email" autocomplete="email" />
</Field>
<Field>
<Label>Password</Label>
<Input type="password" name="password" autocomplete="new-password" />
</Field>
<Field>
<Label>Settlement currency</Label>
<Select name="currency">
<option value="XOF">West African CFA franc (XOF)</option>
<option value="NGN">Nigerian naira (NGN)</option>
<option value="KES">Kenyan shilling (KES)</option>
<option value="GHS">Ghanaian cedi (GHS)</option>
<option value="USD">US dollar (USD)</option>
</Select>
</Field>
<CheckboxField>
<Checkbox name="terms" />
<Label>I accept the terms of service</Label>
</CheckboxField>
<Button type="submit" class="w-full">Create workspace</Button>
<Text>
Already on Northwind?
<TextLink href="/login"><Strong>Log in</Strong></TextLink>
</Text>
</form>
</AuthLayout>
</template>Send a password reset link
One field and one sentence on what happens next. Keep a way back to the log in screen for people who remember their password halfway through.
import { AuthLayout } from '@/components/auth-layout'
import { Button } from '@/components/button'
import { Field, Label } from '@/components/fieldset'
import { Heading } from '@/components/heading'
import { Input } from '@/components/input'
import { Strong, Text, TextLink } from '@/components/text'
import { Logo } from './logo'
export default function Recover() {
return (
<AuthLayout>
<form action="/recover" method="POST" className="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo className="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Get a reset link</Heading>
<Text>Use the address you log in with. The link in the email works once and expires after 30 minutes.</Text>
<Field>
<Label>Work email</Label>
<Input type="email" name="email" autoComplete="email" />
</Field>
<Button type="submit" className="w-full">
Email me the link
</Button>
<Text>
Remembered it?{' '}
<TextLink href="/login">
<Strong>Back to log in</Strong>
</TextLink>
</Text>
</form>
</AuthLayout>
)
}<script setup lang="ts">
import AuthLayout from '@/components/auth-layout.vue'
import Button from '@/components/button.vue'
import Field from '@/components/field.vue'
import Heading from '@/components/heading.vue'
import Input from '@/components/input.vue'
import Label from '@/components/label.vue'
import Strong from '@/components/strong.vue'
import Text from '@/components/text.vue'
import TextLink from '@/components/text-link.vue'
import Logo from './Logo.vue'
</script>
<template>
<AuthLayout>
<form action="/recover" method="POST" class="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo class="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Get a reset link</Heading>
<Text>Use the address you log in with. The link in the email works once and expires after 30 minutes.</Text>
<Field>
<Label>Work email</Label>
<Input type="email" name="email" autocomplete="email" />
</Field>
<Button type="submit" class="w-full">Email me the link</Button>
<Text>
Remembered it?
<TextLink href="/login"><Strong>Back to log in</Strong></TextLink>
</Text>
</form>
</AuthLayout>
</template>