How to Implement Form Validation with Astryx FormLayout and Field Components

Use the Field component inside a FormLayout and pass validationState and validationMessage props to trigger accessible error UI, while FormLayoutContext automatically handles directional layout for labels and inputs.

Form validation in the Astryx design system relies on a composable architecture where layout and validation concerns remain separate. In facebook/astryx, the FormLayout component provides directional context to its children, while the Field component consumes that context to render labels, controls, and validation feedback. This pattern allows you to implement robust, accessible form validation across vertical, horizontal, or mixed layouts without duplicating layout logic.

Understanding the FormLayout Architecture

FormLayout and Direction Context

The FormLayout component acts as a container that establishes a visual direction for all nested fields. According to the source code in [FormLayout.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/FormLayout/FormLayout.tsx), the component accepts a direction prop with values 'vertical', 'horizontal', or 'horizontal-labels'. It renders a semantic container with role="group" and passes the direction value into FormLayoutContext.

FormLayoutContext Propagation

[FormLayoutContext.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/FormLayout/FormLayoutContext.tsx) defines the React context that carries the FormLayoutDirection enum to descendant components. When you wrap fields in FormLayout, each Field component automatically accesses this context via use(FormLayoutContext) to determine whether to stack labels above inputs or align them side-by-side.

Field Component Validation Interface

The [Field.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/Field/Field.tsx) source file exposes the primary interface for validation. The component accepts validationState ('error' | 'warning' | 'success') and validationMessage props. When validationState is set to 'error', the component injects aria-invalid="true" and associates the message via aria-describedby. The [FieldStatus.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/FieldStatus/FieldStatus.tsx) sub-component renders visual indicators and manages live-region announcements for screen readers.

Implementing Validation with Field Components

Basic Vertical Form with Required Field Validation

The most common pattern uses direction="vertical" to stack fields. Here is a complete example showing client-side validation logic passed to the Field component:

import {FormLayout} from '@astryxdesign/core/FormLayout';
import {Field} from '@astryxdesign/core/Field';
import {TextInput} from '@astryxdesign/core/TextInput';
import {useState} from 'react';

export function SimpleForm() {
  const [name, setName] = useState('');
  const [error, setError] = useState<string | null>(null);

  const validate = () => {
    if (!name.trim()) {
      setError('Name is required');
    } else {
      setError(null);
    }
  };

  return (
    <FormLayout direction="vertical" data-testid="my-form" role="group">
      <Field
        label="Name"
        validationState={error ? 'error' : undefined}
        validationMessage={error}
      >
        <TextInput
          value={name}
          onChange={(e) => setName(e.target.value)}
          onBlur={validate}
        />
      </Field>
    </FormLayout>
  );
}

Key implementation details:

  • Parent-controlled validation: The validate function runs on blur, updating React state that flows back into validationState and validationMessage.
  • Automatic ARIA attributes: When error is truthy, Field automatically links the TextInput to the error message via aria-describedby and sets aria-invalid.
  • Live region announcements: The validation message appears in an assertive live region managed by FieldStatus, ensuring screen readers announce errors immediately.

Horizontal-Labels Layout with Mixed Field Types

For denser forms, use direction="horizontal-labels" to place labels to the left of inputs while maintaining vertical stacking of field groups:

import {FormLayout} from '@astryxdesign/core/FormLayout';
import {Field} from '@astryxdesign/core/Field';
import {TextInput, NumberInput, Select} from '@astryxdesign/core';
import {useState} from 'react';

export function MixedForm() {
  const [age, setAge] = useState('');
  const [ageError, setAgeError] = useState<string | null>(null);

  const validateAge = () => {
    const num = Number(age);
    if (Number.isNaN(num) || num < 0) {
      setAgeError('Enter a valid age');
    } else {
      setAgeError(null);
    }
  };

  return (
    <FormLayout direction="horizontal-labels" style={{maxWidth: 600}}>
      <Field label="First name">
        <TextInput placeholder="John" />
      </Field>

      <Field
        label="Age"
        validationState={ageError ? 'error' : undefined}
        validationMessage={ageError}
      >
        <NumberInput
          value={age}
          onChange={(e) => setAge(e.target.value)}
          onBlur={validateAge}
        />
      </Field>

      <Field label="Country">
        <Select options={['US', 'CA', 'UK']} />
      </Field>
    </FormLayout>
  );
}

Layout considerations:

  • The FormLayoutContext value 'horizontal-labels' causes Field to render labels and inputs in a side-by-side grid without additional CSS.
  • Each Field operates independently, allowing different validation states (error, warning, success) to coexist in the same layout.

Handling Complex Layouts and Nested Forms

You can nest FormLayout components to create sectioned forms with varying directions. Because each FormLayout provides its own context value, nested fields adapt their layout while outer containers preserve group semantics:

<FormLayout direction="vertical">
  {/* Section 1: Contact details */}
  <FormLayout direction="horizontal-labels">
    <Field label="Email" validationState={emailError ? 'error' : undefined} validationMessage={emailError}>
      <TextInput type="email" />
    </Field>
    <Field label="Phone">
      <TextInput type="tel" />
    </Field>
  </FormLayout>

  {/* Section 2: Address */}
  <FormLayout direction="horizontal">
    <Field label="City">...</Field>
    <Field label="State">...</Field>
    <Field label="ZIP" validationState={zipError ? 'error' : undefined} validationMessage={zipError}>
      <TextInput />
    </Field>
  </FormLayout>
</FormLayout>

Nesting behavior:

  • Inner layouts override the direction for their immediate children only.
  • Validation props function identically at every nesting level because Field always reads the nearest FormLayoutContext for layout while receiving validation state via props.

Accessibility and Screen Reader Support

The validation system in Astryx is designed to meet WCAG standards through the following mechanisms implemented in the source:

  • aria-invalid: Set automatically when validationState="error" is passed to Field.
  • aria-describedby: Links the input control to the validation message container rendered by FieldStatus.
  • Live regions: FieldStatus uses aria-live="assertive" to announce validation changes without requiring focus movement, as verified in [Field.test.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/Field/Field.test.tsx).
  • Semantic grouping: FormLayout renders role="group" by default, giving screen reader users context about related fields.

Summary

  • FormLayout provides direction context via FormLayoutContext to arrange labels and inputs consistently.
  • Field consumes the layout context and accepts validationState and validationMessage props to render accessible validation UI.
  • FieldStatus handles visual indicators and live-region announcements for screen readers.
  • Validation logic remains in parent components; Field is strictly presentational, allowing integration with any validation library (Yup, Zod, etc.).
  • Nested FormLayout components support complex, multi-section forms while preserving accessibility attributes.

Frequently Asked Questions

How does FormLayout know which direction to apply to nested fields?

FormLayout wraps its children in a FormLayoutContext.Provider that passes the direction prop value. When a Field component renders, it calls use(FormLayoutContext) to read the current direction and adjusts its internal CSS grid accordingly. This mechanism is defined in [FormLayoutContext.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/FormLayout/FormLayoutContext.tsx) and consumed in [Field.tsx](https://github.com/facebook/astryx/blob/main/packages/core/src/Field/Field.tsx).

Can I use custom input components with Astryx Field validation?

Yes. The Field component uses React composition via its children prop. As long as the child component accepts standard HTML attributes like aria-invalid and aria-describedby, it will inherit accessibility properties automatically. Pass your custom input as a child of Field and supply validationState and validationMessage props to the Field wrapper.

What happens if I don't provide a validationMessage but set validationState to error?

The Field component will still mark the input as invalid with aria-invalid="true", but screen reader users will not hear a descriptive error message. It is recommended to always provide a validationMessage when validationState is 'error' or 'warning' to ensure compliance with accessibility guidelines.

Does Astryx FormLayout handle form submission or prevent invalid submissions?

No. FormLayout and Field are strictly presentational and do not manage form state or submission logic. You must implement form-level validation and submission handling in your parent component (e.g., using React Hook Form, Formik, or native form events) and pass the resulting validation states down to individual Field components.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →