Skip to content

Sign-in form

Drop into src/blocks/SignInForm.tsx. Wire onSubmit to your auth backend.

'use client';
import { useState, type FormEvent } from 'react';
import { Button, Card, Field, Spinner, Stack, Text, TextInput } from '@helix-ui/core';
import { GoogleColor, AppleFilled } from '@helix-ui/icons';
export interface SignInFormProps {
onSubmit: (creds: { email: string; password: string }) => Promise<void>;
onOAuth?: (provider: 'google' | 'apple') => void;
forgotHref?: string;
signUpHref?: string;
}
export function SignInForm({
onSubmit,
onOAuth,
forgotHref = '/forgot',
signUpHref = '/signup',
}: SignInFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const submit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setBusy(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError(err instanceof Error ? err.message : 'Sign-in failed.');
} finally {
setBusy(false);
}
};
return (
<Card
variant="elevated"
style={{ padding: 'var(--helix-ui-space-8)', maxWidth: 380, width: '100%' }}
>
<Stack gap={5}>
<Stack gap={1}>
<Text size="2xl" weight="semibold">
Welcome back
</Text>
<Text size="sm" tone="muted">
Sign in to continue to your workspace.
</Text>
</Stack>
{onOAuth ? (
<Stack gap={2}>
<Button variant="outline" tone="neutral" onClick={() => onOAuth('google')}>
<GoogleColor /> Continue with Google
</Button>
<Button variant="outline" tone="neutral" onClick={() => onOAuth('apple')}>
<AppleFilled /> Continue with Apple
</Button>
<Stack direction="row" align="center" gap={2}>
<div
style={{ flex: 1, height: 1, background: 'var(--helix-ui-color-border-subtle)' }}
/>
<Text size="xs" tone="muted">
or
</Text>
<div
style={{ flex: 1, height: 1, background: 'var(--helix-ui-color-border-subtle)' }}
/>
</Stack>
</Stack>
) : null}
<form onSubmit={submit}>
<Stack gap={3}>
<Field>
<Field.Label>Email</Field.Label>
<TextInput
type="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
isRequired
/>
</Field>
<Field>
<Field.Label>Password</Field.Label>
<Field.Description>{<a href={forgotHref}</Field.Description>Forgot?</a>}>
<TextInput
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
isRequired
minLength={8}
/>
</Field>
{error ? (
<Text size="sm" tone="danger">
{error}
</Text>
) : null}
<Button type="submit" disabled={busy}>
{busy ? <Spinner size="sm" /> : null}
Sign in
</Button>
</Stack>
</form>
<Text size="sm" tone="muted" align="center">
Don't have an account? <a href={signUpHref}>Create one</a>
</Text>
</Stack>
</Card>
);
}

Usage

<SignInForm
onSubmit={async ({ email, password }) => {
await auth.signIn({ email, password });
}}
onOAuth={(provider) => auth.signInWithOAuth(provider)}
/>

What this is and isn’t

  • It’s a pattern. Tweak the copy, the OAuth provider list, the field validation.
  • It’s not a finished auth flow. You wire it to your provider (Supabase Auth, Clerk, Auth.js, your own backend).
  • It assumes @helix-ui/icons ships GoogleColor and AppleFilled — if you’re on an older version, swap in your own SVGs.