FormField
Lays out a control with its Label, description and error, and links them for screen readers ("Email, text field, Enter a valid email").
Componentforms
Style
Light
Dark
Your app is ready.
Inter body text, Inter headings.
smmdlgxl
you@example.com
Continue
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
$
npx nativecn-cli@latest add form-fieldAlso installs: theme, form-field-context, label, text.
Usage
FormField demo
Email and password fields in a FocusChain, with a description and an error.
import { useState } from "react";
import { FormField } from "@/components/form-field";
import { Input } from "@/components/input";
import { FocusChain } from "@/components/primitives/focus-chain";
export function FormFieldDemo() {
const [email, setEmail] = useState("");
const [error, setError] = useState<string>();
const submit = () => setError(email.includes("@") ? undefined : "Enter a valid email");
return (
<FocusChain onSubmit={submit}>
<FormField label="Email" description="We never share it." error={error} required>
<Input
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
/>
</FormField>
<FormField label="Password" required>
<Input secureTextEntry autoComplete="current-password" />
</FormField>
</FocusChain>
);
}FormField demo
Email and password fields in a FocusChain, with a description and an error.
import { useState } from "react";
import { FormField } from "@/components/form-field";
import { Input } from "@/components/input";
import { FocusChain } from "@/components/primitives/focus-chain";
export function FormFieldDemo() {
const [email, setEmail] = useState("");
const [error, setError] = useState<string>();
const submit = () => setError(email.includes("@") ? undefined : "Enter a valid email");
return (
<FocusChain onSubmit={submit}>
<FormField label="Email" description="We never share it." error={error} required>
<Input
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
/>
</FormField>
<FormField label="Password" required>
<Input secureTextEntry autoComplete="current-password" />
</FormField>
</FocusChain>
);
}Props
| Prop | Type and description |
|---|---|
label | string: the visible label and the control's accessible name |
description | string: helper text under the control, read as its hint |
error | string: replaces the description, sets status error, announced once |
required | boolean: * marker and "required" in the spoken name (default false) |
status | 'error' | 'success' (defaults to 'error' while error is set) |
disabled | boolean: passed down to the control (default false) |
children | the control: Input, Textarea, InputOTP, Checkbox, Switch, Slider |
style | ViewStyle, layout only, merged last |
Accessibility
- The label is the control's accessible name, with ", required" added when required.
- The error, or else the description, is the control's hint.
- The visible label, description and error are hidden from screen readers, so nothing is read twice.
- An error is announced once when it appears or its text changes.
- disabled passes down to the control, which is announced as disabled.
Notes
Wrap each field of a form in FormField, and the fields in FocusChain. FormField is library-neutral: with react-hook-form, render it inside a Controller and pass fieldState.error?.message as error.
Source
The Vega Style's code, exactly as add copies it into your app.
{components}/form-field.tsx
import type { Ref } from "react";
import { View, type StyleProp, type ViewProps, type ViewStyle } from "react-native";
import { Label } from "@/components/label";
import {
FormFieldProvider,
type FormFieldProps as FormFieldContextProps,
} from "@/components/primitives/form-field-context";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
export type FormFieldProps = FormFieldContextProps &
Omit<ViewProps, "children" | "style"> & {
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
ref?: Ref<View>;
};
const useStyles = createStyles((t) => ({
root: { ...{ gap: t.spacing[2] } },
}));
// The label, description and error are read through the control (its name and hint), so their
// visible copies are hidden from screen readers to avoid reading them twice.
const hiddenFromScreenReaders = {
"aria-hidden": true,
importantForAccessibility: "no-hide-descendants",
} as const;
/**
* Lays out a form control with its Label, description and error, and links them to it: the
* control inside (Input, Textarea, ...) reads "Email, text field, Enter a valid email". The
* error replaces the description, sets the `error` status and is announced once when it appears.
* `status` and `disabled` pass down to the control. `style` is merged last.
*/
export function FormField({
label,
description,
error,
required = false,
status,
disabled = false,
children,
style,
...props
}: FormFieldProps) {
const styles = useStyles();
return (
<View style={[styles.root, style]} {...props}>
{label ? (
<Label required={required} {...hiddenFromScreenReaders}>
{label}
</Label>
) : null}
<FormFieldProvider
label={label}
description={description}
error={error}
required={required}
status={status}
disabled={disabled}
>
{children}
</FormFieldProvider>
{error ? (
<Text variant="small" color="destructive" {...hiddenFromScreenReaders}>
{error}
</Text>
) : description ? (
<Text variant="small" color="mutedForeground" {...hiddenFromScreenReaders}>
{description}
</Text>
) : null}
</View>
);
}{components}/form-field.tsx
import type { Ref } from "react";
import { View, type StyleProp, type ViewProps, type ViewStyle } from "react-native";
import { Label } from "@/components/label";
import {
FormFieldProvider,
type FormFieldProps as FormFieldContextProps,
} from "@/components/primitives/form-field-context";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
export type FormFieldProps = FormFieldContextProps &
Omit<ViewProps, "children" | "style"> & {
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
ref?: Ref<View>;
};
const useStyles = createStyles((t) => ({
root: { ...{ gap: t.scaleValue(6) } },
}));
// The label, description and error are read through the control (its name and hint), so their
// visible copies are hidden from screen readers to avoid reading them twice.
const hiddenFromScreenReaders = {
"aria-hidden": true,
importantForAccessibility: "no-hide-descendants",
} as const;
/**
* Lays out a form control with its Label, description and error, and links them to it: the
* control inside (Input, Textarea, ...) reads "Email, text field, Enter a valid email". The
* error replaces the description, sets the `error` status and is announced once when it appears.
* `status` and `disabled` pass down to the control. `style` is merged last.
*/
export function FormField({
label,
description,
error,
required = false,
status,
disabled = false,
children,
style,
...props
}: FormFieldProps) {
const styles = useStyles();
return (
<View style={[styles.root, style]} {...props}>
{label ? (
<Label required={required} {...hiddenFromScreenReaders}>
{label}
</Label>
) : null}
<FormFieldProvider
label={label}
description={description}
error={error}
required={required}
status={status}
disabled={disabled}
>
{children}
</FormFieldProvider>
{error ? (
<Text variant="small" color="destructive" {...hiddenFromScreenReaders}>
{error}
</Text>
) : description ? (
<Text variant="small" color="mutedForeground" {...hiddenFromScreenReaders}>
{description}
</Text>
) : null}
</View>
);
}
