UI Toolkit: Form Validation


Runtime form validation for Unity UI Toolkit. 14 rules, async with stale-result protection, cross-field with cycle detection, focus-first-invalid, 3 demos. Zero dependencies.


by KrookedLilly


Price History +

Unity 6 ships no form validation for UI Toolkit. UGUI's CharacterValidation.EmailAddress only filters which characters can be typed — it does not validate the final string is a valid email. UITK's regex attribute on TextField evaluates per-character: try a full-email or full-IP pattern and all input is blocked because no single character satisfies the full-string pattern. This asset is the runtime validation layer that fills the gap.


Forms are plain C# — no GameObject pollution.

Form.For(VisualElement) and Form.For(UIDocument) give you a plain C# form object — no MonoBehaviour wrapping, no required components, no hidden inspector toggles. Register fields fluently from a controller's OnEnable and drop multiple forms into a single UI sub-tree without GameObject contortions.


Fluent schema with two equivalent styles.

Wrap any UI Toolkit control via its value-change surface — TextField, Toggle, Slider, IntegerField, DropdownField, EnumField, custom controls. Register fields atomically or as chained specs:

form.RegisterText("email")
.WithRule(Rules.Required())
.WithRule(Rules.Email());

form.RegisterText("username")
.WithRule(Rules.Required().MinLength(3).MaxLength(20))
.WithRule(Rules.Regex("[a-zA-Z][a-zA-Z0-9_]*"))
.WithMessage("3–20 characters, must start with a letter.");

Both styles produce the same result. WithMessage overrides per-rule error text.


14 built-in rules.

Required, MinLength, MaxLength, Regex (auto-anchored with ^...$ so full-string patterns actually work), Email, Url, Range<T>, MinValue<T>, MaxValue<T>, OneOf<T>, NotEqual<T>, Custom, MatchesField, RequiredIf. Extend with your own rules via the standard rule interfaces.


Async validation with stale-result protection.

Rules.CustomAsync<T>(handler, debounceMs) wraps any awaitable into a validation rule with built-in cancellation. Field-level debounce defaults to 300ms when any async rule is present. The framework defends against late-arriving results from two directions at once: cancellation tokens stop in-flight work on every new value change, and a generation counter discards stale-but-completed results even when a rule ignores its cancellation token. Bind a spinner to the form's IsValidating flag for "Checking availability..." UI with one line.


Cross-field rules with cycle detection.

MatchesField("password") and RequiredIf("over18", v => v == true) ship built-in. Declare custom cross-field rules with the standard rule interface. The framework runs cycle detection at form-build time — any circular dependency is logged with the full cycle path before validation runs, so the framework never hangs on a cyclic graph. Dependents re-validate when their source field changes without resetting touched/dirty state and without applying their own debounce.


Form-level state aggregation.

Each field cycles through clean, dirty, touched, validating, and valid/invalid states with predictable transitions. Roll the field state up into form.IsValid, form.IsDirty, and form.IsValidating for one-line button-enabling, "save in progress" indicators, and dirty-form prompts. Errors display after first blur by default so empty forms aren't pre-shouted at on first paint — configurable per field.


Three error display strategies.

Inline error labels (default) auto-create a sibling label per field, or honor a buyer-supplied label. A summary-panel strategy appends one label per error into a top-of-form container. The interface is open for custom strategies (banner, accessibility aria, toast). Swap globally via a single static assignment.


Async submit pipeline.

form.OnSubmit(async ctx => ...) registers your handler. await form.SubmitAsync() validates every field (sync rules + async rules immediately, bypassing debounce), then dispatches to the handler with a submit context that carries the values dictionary, a cancellation token, and a server-error sink. Return Success, Failure(serverErrors), or let it throw — exceptions become an Errored result and the form moves on. Failed submits trigger focus-first-invalid automatically.


Focus-first-invalid is the killer feature.

After a failed submit, the framework focuses the first invalid field and (when a ScrollView ancestor exists) scrolls the field into view. Works standalone via the built-in focus + scroll fallback; upgraded when the UI Toolkit: Focus & Navigation asset is installed and the Focus & Navigation → Form Validation integration is enabled in the unified Tools > KrookedLilly > Setup window (adds gamepad-compatible navigation and indicator-aware focus).


Plays well with the rest of the UI Toolkit Components suite.

If you own other assets from the suite, Form Validation lights up additional polish through a per-pair opt-in. Open the single Tools > KrookedLilly > Setup window, find the integration's row in the cross-asset matrix (e.g. Focus & Navigation → Form Validation), and enable it — the matching integration assembly then compiles in. Integrations ship disabled by default so nothing slips into your build until you ask for it.


With UI Toolkit: Focus & Navigation: failed submits jump focus to the first broken field and scroll it into view smoothly, with gamepad-compatible navigation. Without it, focus still moves to the first invalid field via a built-in fallback — the integration upgrades scroll behavior and adds controller support.


With UI Toolkit: Screen Manager: add a one-line guard to your screens to prompt the user before they navigate away from a form with unsaved changes. No more silent data loss when someone hits Back mid-edit.


With UI Toolkit: Tween Engine: error messages fade and slide into place instead of popping in, and the form briefly shakes when an invalid submit is rejected. Visible, tactile feedback without any animation code on your side.


With UI Toolkit: Modal & Notifications: validation errors can surface as toast notifications instead of inline labels — handy on compact layouts where there's no room beneath the field. The confirm-discard dialog used by the UI Toolkit: Screen Manager guard above also flows through here, so the prompt looks like the rest of your app's modals.


With UI Toolkit: Responsive Layout: the framework switches from inline error labels to toast notifications when your layout enters its mobile breakpoint, so small-screen users see errors that don't crowd the field. Requires UI Toolkit: Modal & Notifications enabled for the toast renderer.


With UI Toolkit: Theme Switcher: the default styling reads theme tokens (--color-error, --color-success, --color-warning, --field-invalid-border) so error/success colors follow your theme automatically. With the integration enabled, the asset logs the token names it expects on first load so theme authors know exactly which variables to define.


Three demo scenes included.

Login — email + password + remember-me. Required + Email + MinLength. The smallest demo; mirrors the quickstart in the documentation. 


Registration — username + email + password + confirm-password + terms checkbox. Cross-field MatchesField("password"), async username availability rule (mocks 800ms latency with a visible "Checking availability..." spinner), strong-password regex with character-class requirements. The killer-feature demo. 


Settings — DropdownField + Slider + IntegerField + Toggle. Proves the framework isn't text-only and demonstrates code-set choices for dropdowns. All three ship with controller scripts, UXML, USS, and configured panel settings.


Full C# source, no DLLs.

XML documentation on every public API. Form is plain C# — no MonoBehaviour wrapping, no Unity-Editor coupling. Zero external dependencies.