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,
Stack,
Text,
TextInput,
} from '@helixui/core';
import { GoogleColor, AppleFilled } from '@helixui/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(--helixui-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(--helixui-color-border-subtle)' }} />
<Text size="xs" tone="muted">or</Text>
<div style={{ flex: 1, height: 1, background: 'var(--helixui-color-border-subtle)' }} />
</Stack>
</Stack>
) : null}
<form onSubmit={submit}>
<Stack gap={3}>
<Field label="Email">
<TextInput
type="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</Field>
<Field label="Password" hint={<a href={forgotHref}>Forgot?</a>}>
<TextInput
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
/>
</Field>
{error ? <Text size="sm" tone="danger">{error}</Text> : null}
<Button type="submit" loading={busy}>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 @helixui/icons ships GoogleColor and AppleFilled — if you’re on an older version, swap in your own SVGs.