Form Patterns
NYSDS form components -- <nys-textinput>, <nys-textarea>, <nys-select>, <nys-checkbox>, <nys-radiobutton>, <nys-datepicker>, <nys-toggle>, and <nys-fileinput> -- are built to work with native HTML <form> elements. They participate in form submission, respond to form resets, and support constraint validation, all without requiring extra JavaScript wiring.
This page covers the cross-cutting patterns that apply across all form components. For individual component props, events, and options, see each component's documentation.
Form Association with ElementInternals
Every NYSDS form component is a form-associated custom element. This means each component uses the browser's ElementInternals API to register itself with a parent <form>, just like a native <input> or <select> would.
In practice, this means:
- NYSDS components appear in
form.elements. When you place a<nys-textinput>inside a<form>, the form knows about it. You can access it throughform.elementsby itsnameattribute. - Values are included in form submission. When the form is submitted, each component's current value is included in the
FormDataobject, keyed by the component'snameattribute. - Form resets clear component state. Calling
form.reset()or clicking a<nys-button type="reset">triggers each component'sformResetCallback, which clears values, removes error messages, and resets validation state. - Constraint validation works natively. Setting
requiredon a component and then callingform.requestSubmit()triggers the browser's built-in validation. Invalid components receive focus and display error messages automatically.
How it works under the hood
Each NYSDS form component declares static formAssociated = true and calls this.attachInternals() in its constructor. The resulting ElementInternals object provides methods like setFormValue() to push the component's value into the form, and setValidity() to report validation state to the browser.
You do not need to interact with ElementInternals directly. The components handle this internally. What matters to you is that NYSDS components behave like native form controls.
The name attribute
For a component's value to appear in form data, it must have a name attribute. This is the key under which the value is submitted.
Without a name, the component still participates in validation but its value is not included in the submitted FormData.
The form attribute
Every NYSDS form component supports a form property that associates the component with a <form> element elsewhere in the DOM. This mirrors the behavior of the native HTML form attribute on <input>.
Use this when a form component needs to live outside the <form> tag but still submit with that form:
Form Layout
Structure forms using the NYSDS grid system. Place each form field on its own line for single-column forms, or use the grid for multi-column layouts on wider screens.
Single-column form (recommended for most cases)
Single-column layouts are easier to scan, reduce cognitive load, and perform better on mobile devices. Use them for most government forms.
Multi-column form
When placing fields side by side makes semantic sense (like first name and last name), use the grid utilities:
Layout guidance
- Use the
widthproperty on form components (sm,md,lg,full) to match field width to expected input length. A zip code field should besm. A full name should belgorfull. - Stack fields vertically by default. Side-by-side layouts should be reserved for closely related fields (first/last name, city/state/zip).
- Place the submit button at the bottom-left of the form, aligned with the form fields.
- Group related fields with headings. For multi-step forms, consider the
<nys-stepper>component.
Validation
NYSDS form components validate using a combination of native HTML constraint validation and the ElementInternals API. Validation is integrated into the component lifecycle -- you do not need to write custom validation logic for standard cases.
Built-in validation
Each component supports the standard HTML validation attributes appropriate to its type:
| Attribute | Supported by | What it checks |
|---|---|---|
required |
All form components | Value is not empty |
pattern |
<nys-textinput> |
Value matches a regex pattern |
type (email, url, tel) |
<nys-textinput> |
Value matches the expected format |
min / max |
<nys-textinput type="number"> |
Value is within numeric range |
maxlength |
<nys-textinput>, <nys-textarea> |
Value does not exceed character limit |
step |
<nys-textinput type="number"> |
Value matches the step increment |
When validation fails, the component automatically:
- Sets
showErrortotrue - Displays the appropriate error message
- Reports its invalid state to the parent
<form>viaElementInternals
When validation runs
NYSDS form components use an eager/lazy validation strategy:
- On first interaction (lazy): Validation does not run until the user blurs the field (moves focus away). This avoids showing errors while someone is still typing.
- After first error (eager): Once a field has been marked invalid, validation runs on every input change. This gives immediate feedback as the user corrects their entry.
- On form submission: When the form is submitted (via
form.requestSubmit()or a<nys-button type="submit">), all fields are validated. The first invalid field receives focus.
Custom error messages
Set the errorMessage property to override the default validation message:
If errorMessage is set, it takes precedence over the browser's built-in validation messages for all validation failures on that component.
Manual error display
You can also control errors programmatically by setting both errorMessage and showError:
Or in JavaScript:
Checking validity in JavaScript
Each NYSDS form component exposes a checkValidity() method that returns true if the component's current value satisfies its constraints:
Event Handling
NYSDS form components emit custom events prefixed with nys-. The specific events vary by component, but they follow a consistent pattern.
Common events across form components
| Event | Fired when | Components |
|---|---|---|
nys-input |
Value changes (on each keystroke) | <nys-textinput>, <nys-textarea> |
nys-change |
Selection or state changes | <nys-select>, <nys-checkbox>, <nys-radiobutton>, <nys-toggle>, <nys-datepicker> |
nys-focus |
Component gains focus | All form components |
nys-blur |
Component loses focus | All form components |
Event detail structure
Every nys-input and nys-change event includes a detail object with at minimum an id and value. Some components include additional fields:
// nys-textinput: { id, value }
// nys-checkbox: { id, checked, name, value }
// nys-radiobutton: { id, checked, name, value }
// nys-select: { id, value }
Listening for events
Events bubble up through the DOM and cross shadow DOM boundaries (via composed: true). You can listen on individual components or on a parent element:
nys-input vs. nys-change
Text-based components (<nys-textinput>, <nys-textarea>) fire nys-input on every keystroke. Selection-based components (<nys-select>, <nys-checkbox>, <nys-radiobutton>) fire nys-change when the selection state changes. This mirrors the native input vs. change distinction in HTML.
For component-specific event details, see each component's Events section.
Form Submission
Submitting with <nys-button>
Use <nys-button type="submit"> inside or associated with a <form>. When clicked, the button calls form.requestSubmit(), which triggers constraint validation on all form-associated elements before dispatching the submit event.
Handling the submit event
Listen for the standard submit event on the <form>. If all fields pass validation, the event fires. If any field is invalid, submission is blocked and the first invalid field receives focus.
Resetting forms
Use <nys-button type="reset"> or call form.reset() in JavaScript. Every NYSDS form component implements formResetCallback, which:
- Clears the component's value
- Removes error messages and hides error state
- Resets internal validation state
Reading form values without submission
Access current values at any time through the FormData API:
Error Summary Patterns
For long forms, consider displaying a summary of all errors at the top of the form after a failed submission attempt. This helps users understand what needs to be fixed, especially when errors are spread across many fields.
NYSDS does not currently provide a dedicated error summary component, but you can build one using <nys-alert> and the form's validation API. Here is an example of an error summary at the top of a form:
Here is the JavaScript to generate and display error summaries:
Here is the HTML for a form with an error summary and the event handler:
Error summary best practices
- Place the error summary above the form, before any fields.
- Include anchor links from each error to the corresponding field's
id. - Move focus to the error summary when it appears so screen reader users are immediately notified.
- Clear the error summary when the form is successfully submitted or reset.
Complete Example
The following example shows a realistic multi-field form using several NYSDS form components with validation and submission handling.
Here is the JavaScript to handle validation and submission:
Form Components Reference
These NYSDS components are form-associated and work with the patterns described on this page:
| Component | Use for | Events |
|---|---|---|
<nys-textinput> |
Short text: names, emails, numbers, passwords | nys-input, nys-focus, nys-blur |
<nys-textarea> |
Multi-line text: comments, descriptions | nys-input, nys-focus, nys-blur |
<nys-select> |
Single selection from a dropdown list | nys-change, nys-focus, nys-blur |
<nys-checkbox> |
Binary choices or multi-select lists | nys-change, nys-focus, nys-blur |
<nys-radiobutton> |
Single selection from visible options | nys-change, nys-focus, nys-blur |
<nys-datepicker> |
Date selection with calendar | nys-change, nys-focus, nys-blur |
<nys-toggle> |
Binary settings with immediate effect | nys-change, nys-focus, nys-blur |
<nys-fileinput> |
File uploads with drag-and-drop | nys-change |
<nys-button> |
Submit, reset, or custom form actions | nys-click, nys-focus, nys-blur |
For accessibility patterns in forms, see Accessibility.
Edit this page on GitHub (Permissions required)
Last updated: May 28, 2026