Skip to main content

Documentation Index

Fetch the complete documentation index at: https://copylabs.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Forms Guide

This guide covers best practices for building forms with Radix Themes Native.

Basic Form

import { Form, TextField, Button, Flex } from 'radix-native-ui';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = () => {
    console.log({ email, password });
  };

  return (
    <Flex direction="column" gap={3}>
      <TextField
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        keyboardType="email-address"
        autoCapitalize="none"
      />
      <TextField
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button onPress={handleSubmit}>Sign In</Button>
    </Flex>
  );
}

Form Validation

Basic Validation

function ValidatedForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const validateEmail = (value: string) => {
    if (!value) {
      setError('Email is required');
    } else if (!/\S+@\S+\.\S+/.test(value)) {
      setError('Please enter a valid email');
    } else {
      setError('');
    }
    setEmail(value);
  };

  return (
    <Flex direction="column" gap={1}>
      <TextField
        placeholder="Email"
        value={email}
        onChangeText={validateEmail}
        style={error ? { borderColor: 'red' } : undefined}
      />
      {error && (
        <Text color="red" size={1}>{error}</Text>
      )}
    </Flex>
  );
}

Form Schema Validation

import { z } from 'zod';

const formSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

function SchemaValidatedForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState<Record<string, string>>({});

  const validate = (field: string, value: string) => {
    try {
      formSchema.shape[field].parse(value);
      setErrors(prev => ({ ...prev, [field]: '' }));
    } catch (err) {
      if (err instanceof z.ZodError) {
        setErrors(prev => ({ ...prev, [field]: err.errors[0].message }));
      }
    }
    setValues(prev => ({ ...prev, [field]: value }));
  };

  const handleSubmit = () => {
    try {
      formSchema.parse(values);
      // Submit form
    } catch (err) {
      if (err instanceof z.ZodError) {
        const fieldErrors: Record<string, string> = {};
        err.errors.forEach(e => {
          if (e.path[0]) {
            fieldErrors[e.path[0] as string] = e.message;
          }
        });
        setErrors(fieldErrors);
      }
    }
  };

  return (
    <Flex direction="column" gap={3}>
      <Flex direction="column" gap={1}>
        <TextField
          placeholder="Email"
          value={values.email}
          onChangeText={(v) => validate('email', v)}
        />
        {errors.email && <Text color="red" size={1}>{errors.email}</Text>}
      </Flex>

      <Flex direction="column" gap={1}>
        <TextField
          placeholder="Password"
          value={values.password}
          onChangeText={(v) => validate('password', v)}
          secureTextEntry
        />
        {errors.password && <Text color="red" size={1}>{errors.password}</Text>}
      </Flex>

      <Button onPress={handleSubmit}>Submit</Button>
    </Flex>
  );
}

Form Components

TextField

<TextField
  placeholder="Enter text"
  value={value}
  onChangeText={setValue}
  size={2}
  color="blue"
  radius="medium"
/>

TextArea

<TextArea
  placeholder="Enter description"
  value={value}
  onChangeText={setValue}
  rows={4}
/>

Select

<Select value={value} onValueChange={setValue}>
  <Select.Trigger>
    <Text>{value || 'Select an option'}</Text>
  </Select.Trigger>
  <Select.Content>
    <Select.Item value="option1">Option 1</Select.Item>
    <Select.Item value="option2">Option 2</Select.Item>
    <Select.Item value="option3">Option 3</Select.Item>
  </Select.Content>
</Select>

Checkbox

<Checkbox
  checked={checked}
  onCheckedChange={setChecked}
>
  I agree to the terms
</Checkbox>

Radio Group

<RadioGroup value={value} onValueChange={setValue}>
  <Flex direction="column" gap={2}>
    <Radio value="option1">Option 1</Radio>
    <Radio value="option2">Option 2</Radio>
    <Radio value="option3">Option 3</Radio>
  </Flex>
</RadioGroup>

Switch

<Flex direction="row" justify="between" align="center">
  <Text>Enable notifications</Text>
  <Switch checked={enabled} onCheckedChange={setEnabled} />
</Flex>

Form Layout

Vertical Layout

<Flex direction="column" gap={3}>
  <TextField placeholder="Name" />
  <TextField placeholder="Email" />
  <TextField placeholder="Phone" />
  <Button>Submit</Button>
</Flex>

Horizontal Layout

<Flex direction="row" gap={2}>
  <TextField placeholder="First name" style={{ flex: 1 }} />
  <TextField placeholder="Last name" style={{ flex: 1 }} />
</Flex>

Form Groups

<Card>
  <Flex direction="column" gap={3}>
    <Heading size={3}>Personal Information</Heading>
    <TextField placeholder="Full name" />
    <TextField placeholder="Email" />
    <TextField placeholder="Phone" />
  </Flex>
</Card>

<Card>
  <Flex direction="column" gap={3}>
    <Heading size={3}>Address</Heading>
    <TextField placeholder="Street" />
    <Flex direction="row" gap={2}>
      <TextField placeholder="City" style={{ flex: 1 }} />
      <TextField placeholder="ZIP" style={{ flex: 1 }} />
    </Flex>
  </Flex>
</Card>

Form State Management

Custom Hook

function useForm<T extends Record<string, string>>(initialValues: T) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const handleChange = (field: keyof T, value: string) => {
    setValues(prev => ({ ...prev, [field]: value }));
  };

  const handleBlur = (field: keyof T) => {
    setTouched(prev => ({ ...prev, [field]: true }));
  };

  const setError = (field: keyof T, error: string) => {
    setErrors(prev => ({ ...prev, [field]: error }));
  };

  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    setError,
    reset
  };
}

Usage

function MyForm() {
  const form = useForm({ email: '', password: '' });

  const handleSubmit = () => {
    if (!form.values.email) {
      form.setError('email', 'Email is required');
      return;
    }
    // Submit
  };

  return (
    <Flex direction="column" gap={3}>
      <TextField
        placeholder="Email"
        value={form.values.email}
        onChangeText={(v) => form.handleChange('email', v)}
        onBlur={() => form.handleBlur('email')}
      />
      {form.touched.email && form.errors.email && (
        <Text color="red">{form.errors.email}</Text>
      )}

      <Button onPress={handleSubmit}>Submit</Button>
    </Flex>
  );
}

Submit Button States

function SubmitButton({ loading, disabled, onPress }) {
  return (
    <Button
      onPress={onPress}
      disabled={disabled || loading}
    >
      {loading ? (
        <Flex direction="row" gap={2} align="center">
          <Spinner size={1} />
          <Text>Submitting...</Text>
        </Flex>
      ) : (
        'Submit'
      )}
    </Button>
  );
}

Best Practices

1. Use Proper Keyboard Types

<TextField
  placeholder="Email"
  keyboardType="email-address"
  autoCapitalize="none"
/>

<TextField
  placeholder="Phone"
  keyboardType="phone-pad"
/>

<TextField
  placeholder="Website"
  keyboardType="url"
  autoCapitalize="none"
/>

2. Use Return Key Actions

<TextField
  placeholder="Email"
  returnKeyType="next"
  onSubmitEditing={() => passwordRef.current?.focus()}
/>

<TextField
  ref={passwordRef}
  placeholder="Password"
  returnKeyType="done"
  onSubmitEditing={handleSubmit}
/>

3. Disable Submit During Loading

<Button disabled={loading} onPress={handleSubmit}>
  {loading ? 'Submitting...' : 'Submit'}
</Button>

4. Show Success/Error Feedback

function FormWithFeedback() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  const handleSubmit = async () => {
    setStatus('loading');
    try {
      await submitForm();
      setStatus('success');
    } catch {
      setStatus('error');
    }
  };

  return (
    <Flex direction="column" gap={3}>
      {/* Form fields */}

      <Button onPress={handleSubmit} disabled={status === 'loading'}>
        Submit
      </Button>

      {status === 'success' && (
        <Callout color="green">Form submitted successfully!</Callout>
      )}

      {status === 'error' && (
        <Callout color="red">Failed to submit form. Please try again.</Callout>
      )}
    </Flex>
  );
}