Skip to content

Sign-up form

'use client';
import { useState, type FormEvent } from 'react';
import { Button, Card, Checkbox, Field, Spinner, Stack, Text, TextInput } from '@helix-ui/core';
export interface SignUpFormProps {
onSubmit: (creds: { email: string; password: string }) => Promise<void>;
}
export function SignUpForm({ onSubmit }: SignUpFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [agree, setAgree] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const passwordsMatch = password.length >= 8 && password === confirm;
const canSubmit = email && passwordsMatch && agree && !busy;
const submit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
if (!passwordsMatch) {
setError("Passwords don't match.");
return;
}
setBusy(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError(err instanceof Error ? err.message : 'Sign-up failed.');
} finally {
setBusy(false);
}
};
return (
<Card variant="elevated" style={{ padding: 'var(--helix-ui-space-8)', maxWidth: 400 }}>
<form onSubmit={submit}>
<Stack gap={4}>
<Text size="2xl" weight="semibold">
Create an account
</Text>
<Field>
<Field.Label>Email</Field.Label>
<TextInput
type="email"
autoComplete="email"
value={email}
onChange={setEmail}
isRequired
/>
</Field>
<Field>
<Field.Label>Password</Field.Label>
<Field.Description>At least 8 characters.</Field.Description>
<TextInput
type="password"
autoComplete="new-password"
value={password}
onChange={setPassword}
isRequired
minLength={8}
/>
</Field>
<Field>
<Field.Label>Confirm password</Field.Label>
<TextInput
type="password"
autoComplete="new-password"
value={confirm}
onChange={setConfirm}
isRequired
/>
</Field>
<Checkbox isSelected={agree} onChange={setAgree}>
I agree to the <a href="/terms">terms</a> and <a href="/privacy">privacy policy</a>.
</Checkbox>
{error ? (
<Text size="sm" tone="danger">
{error}
</Text>
) : null}
<Button type="submit" disabled={!canSubmit || busy}>
{busy ? <Spinner size="sm" /> : null}
Create account
</Button>
</Stack>
</form>
</Card>
);
}

The “disabled until everything’s valid” UX matches Apple’s HIG and keeps you out of the “user pressed submit and nothing happened” failure mode. The canSubmit check is reactive — flip any field and the button enables.