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.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, MinValue, MaxValue, OneOf, NotEqual, Custom, MatchesField, RequiredIf. Extend with your own rules via the standard rule interfaces.Async validation with stale-result protection.Rules.CustomAsync(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.FormForm.For(VisualElement) / Form.For(UIDocument) POCO constructorForm.IsValid, IsDirty, IsValidating, IsSubmitting aggregate stateForm.OnValidChanged, OnDirtyChanged, OnFieldChanged, OnFieldValidated, Submitting, Submitted eventsForm.GetValues() / SetValues(dict) dictionary DTO bridgeForm.Reset(), Form.MarkAllTouched(), Form.ValidateAllAsync(ct)One form per UI sub-tree supported; multiple instances on a single panel are first-classRules14 built-in rules: Required, MinLength, MaxLength, Regex (auto-anchors with ^...$), Email, Url, Range, MinValue, MaxValue, OneOf, NotEqual, Custom, MatchesField, RequiredIfRules static factory; RuleSpec composite base supports both atomic-pass and chained-spec ergonomicsCached singletons for no-arg rulesIValidationRule / IAsyncValidationRule extension surfacesAsyncIAsyncValidationRule with CancellationToken and per-rule debounce floorDebounceScheduler field-level debounce (default 300ms when any async rule present)Stale-result protection via monotonic generation counter + linked CancellationTokenSource (belt-and-suspenders)Async rule exceptions surface as ValidationResult.Invalid("Validation error: ...") rather than stranding the formCross-fieldMatchesField(string) and RequiredIf(string, predicate) built-inFieldDependencyGraph directed graph; Tarjan's SCC at build time catches cyclesImplicit dependency declaration via CrossFieldRuleSpec; explicit override via FluentFieldBuilder.DependsOn(name)Dependents re-validate without resetting touched/dirty and without their own debounceSchema / BuilderFluentFieldBuilder chainable API: WithRule, WithMessage, WithMessageKey, ValidateOn, ShowErrorsAfter, DependsOn, WithDebounce, ErrorTargetForm.Register(INotifyValueChanged), Register(string), RegisterText(string) overloadsAdaptersFieldAdapter base + concrete adapters for TextField, Toggle, Slider, IntegerField, DropdownFieldValueChangeBridge glue around INotifyValueChanged.RegisterValueChangedCallback with proper Dispose unhookingAdapter API open for custom controlsDisplayIErrorDisplay strategy interfaceInlineLabelDisplay (default) auto-creates sibling label or honors ErrorTarget(label)SummaryPanelDisplay for top-of-form summary containersFormDisplayConfig.DisplayFactory static for global strategy swapStateFieldStateMachine clean → dirty → touched → validating → valid/invalid transitionsFormStateAggregator rolls up field state into form-level booleansErrors display after first blur by default; configurable via ShowErrorsAfterSubmitForm.OnSubmit(handler) and Form.SubmitAsync(ct)SubmitContext with values dictionary + cancellation + server-error sinkForm.BindSubmitButton(button, options) opt-in auto-disable (default off)SubmitResult readonly struct with Success, ServerErrors, Exception; SubmitResult.Succeeded / Failed(errs) / Errored(ex) factoriesSubmit failure triggers focus-first-invalid (FN integration if present, else field.Focus() + ScrollView walk fallback)USS classesPublic sibling-asset contract: field-invalid (read by UI Toolkit: Focus & Navigation for error-state focus styling)BEM-internal long forms: kl-form__field--invalid, kl-form__field--dirty, kl-form__field--touched, kl-form__field--validating, kl-form__field--valid, kl-form__error-label, kl-form__error-label--visible, kl-form--submitting, kl-form--invalidDefault Resources/FormValidation.uss provides minimal visual styling via theme tokensEditorTools > KrookedLilly > Setup — the unified KrookedLilly Setup window, shared by every asset in the suite. It shows the cross-asset integration matrix; enable any row (e.g. Focus & Navigation → Form Validation) and it writes the matching per-pair scripting define into Player Settings (or strips it on disable). One window covers every integration Form Validation participates in.Included demosLogin — email + password + remember-me, the quickstart-driven minimal demoRegistration — cross-field MatchesField, async username availability with simulated 800ms latency and visible spinner, strong-password regex; the killer-feature demoSettings — DropdownField + Slider + IntegerField + Toggle group; proves non-text adapter coverageCompatibilityUnity 6+ (6000.0 and newer)UI Toolkit (com.unity.modules.uielements)Full C# source, no DLLsXML documentation on all public APIsZero external dependenciesAI (Claude Code) was used as a development assistant throughout the package creation process. This includes code generation, architecture design, writing unit tests, documentation authoring, and debugging. All AI-generated code was reviewed, tested, and validated by the developer. The final package is 100% human-supervised C# source code with no AI runtime components.

