From 41a7bbed070eee15bf07b9952337484f90582d79 Mon Sep 17 00:00:00 2001 From: Maxime Lardenois Date: Mon, 23 Feb 2026 14:43:36 +0100 Subject: [PATCH 01/44] feat: use aria-invalid and :user-invalid instead of is-invalid and :invalid --- scss/forms/_control-item.scss | 107 ++++++------------ scss/forms/_validation.scss | 6 +- site/src/content/docs/components/checkbox.mdx | 22 ++-- .../content/docs/components/radio-button.mdx | 18 +-- site/src/content/docs/components/switch.mdx | 14 +-- 5 files changed, 66 insertions(+), 101 deletions(-) diff --git a/scss/forms/_control-item.scss b/scss/forms/_control-item.scss index f855d336c5..22f1b1089b 100644 --- a/scss/forms/_control-item.scss +++ b/scss/forms/_control-item.scss @@ -13,8 +13,8 @@ } // stylelint-disable selector-no-qualifying-type - &.was-validated:has(input:invalid) .control-item-error-message, - &:has(input.is-invalid) .control-item-error-message { + &:has(input:user-invalid) .control-item-error-message, + &:has(input[aria-invalid="true"]) .control-item-error-message { display: block; } // stylelint-enable selector-no-qualifying-type @@ -79,7 +79,8 @@ } // Handle invalid state - &:has(input.is-invalid) { + &:has(input[aria-invalid="true"]), + &:has(input:user-invalid) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-enabled}; padding-right: #{$ouds-control-item-space-padding-inline + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap}; @@ -93,15 +94,18 @@ } } - &:has(input.is-invalid:hover) { + &:has(input[aria-invalid="true"]:hover), + &:has(input:user-invalid:hover) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-hover}; } - &:has(input.is-invalid:focus-visible) { + &:has(input[aria-invalid="true"]:focus-visible), + &:has(input:user-invalid:focus-visible) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-focus}; } - &:has(input.is-invalid:active) { + &:has(input[aria-invalid="true"]:active), + &:has(input:user-invalid:active) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-pressed}; } @@ -121,36 +125,6 @@ } } -// common invalid state management -.was-validated .checkbox-item, -.was-validated .radio-button-item, -.was-validated .switch-item { - &:has(input:invalid) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-enabled}; - - padding-right: #{$ouds-control-item-space-padding-inline + 2 * $ouds-control-item-space-padding-inline-error-icon + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap}; - - .control-item-assets-container:last-child { - display: none; - } - - &::after { - content: ""; - } - } - - &:has(input:invalid:hover) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-hover}; - } - - &:has(input:invalid:focus-visible) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-focus}; - } - - &:has(input:invalid:active) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-pressed}; - } -} // // Control item containers @@ -247,11 +221,28 @@ mask-repeat: no-repeat; } - &[type="checkbox"]:indeterminate, + &:indeterminate:where([type="checkbox"]), &:checked { --#{$prefix}control-item-indicator-color: #{$ouds-color-action-selected}; } + &:user-invalid, + &[aria-invalid="true"] { + --#{$prefix}control-item-indicator-color: #{$ouds-color-action-negative-enabled}; + + &:hover { + --#{$prefix}control-item-indicator-color: #{$ouds-color-action-negative-hover}; + } + + &:focus-visible { + --#{$prefix}control-item-indicator-color: #{$ouds-color-action-negative-focus}; + } + + &:active { + --#{$prefix}control-item-indicator-color: #{$ouds-color-action-negative-pressed}; + } + } + &[type="checkbox"], &[role="checkbox"] { @include border-radius($form-check-input-border-radius); @@ -418,7 +409,7 @@ padding-bottom: calc($ouds-control-item-space-padding-block-default - $ouds-divider-border-width); border-bottom: $ouds-divider-border-width solid $ouds-color-border-default; - &:has(.is-invalid), + &:has([aria-invalid="true"]), .was-validated &:has(input:invalid) { border-color: var(--#{$prefix}control-item-label-color); } @@ -428,7 +419,7 @@ flex-direction: row-reverse; // stylelint-disable selector-no-qualifying-type - &:has(input.is-invalid), + &:has(input[aria-invalid="true"]), .was-validated &:has(input:invalid) { --#{$prefix}control-item-error-icon-offset-right: unset; --#{$prefix}control-item-error-icon-offset-left: var(--#{$prefix}control-item-error-icon-offset); @@ -623,13 +614,13 @@ border: $ouds-divider-border-width solid $ouds-color-border-default; // stylelint-disable selector-no-qualifying-type - &:has(input.is-invalid) { + &:has(input[aria-invalid="true"]) { --#{$prefix}control-item-error-icon-offset: #{$ouds-control-item-space-padding-inline + $ouds-control-item-space-padding-inline-error-icon - $ouds-divider-border-width}; } // stylelint-enable selector-no-qualifying-type } - &:has(input:checked:not(.is-invalid, :invalid)) { + &:has(input:checked:not([aria-invalid="true"], :invalid)) { border-color: $ouds-color-action-selected; &:hover { @@ -650,7 +641,7 @@ } // stylelint-disable selector-no-qualifying-type - &:has(input.is-invalid) { + &:has(input[aria-invalid="true"]) { border-color: var(--#{$prefix}control-item-label-color); } @@ -676,7 +667,7 @@ flex-direction: row-reverse; // stylelint-disable selector-no-qualifying-type - &:has(input.is-invalid) { + &:has(input[aria-invalid="true"]) { --#{$prefix}control-item-error-icon-offset-right: unset; --#{$prefix}control-item-error-icon-offset-left: var(--#{$prefix}control-item-error-icon-offset); @@ -689,7 +680,7 @@ flex-direction: initial; // stylelint-disable selector-no-qualifying-type - &:has(input.is-invalid) { + &:has(input[aria-invalid="true"]) { --#{$prefix}control-item-error-icon-offset-right: var(--#{$prefix}control-item-error-icon-offset); --#{$prefix}control-item-error-icon-offset-left: unset; @@ -728,34 +719,6 @@ align-items: center; } - .was-validated .form-check { - &:has(input:invalid) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-enabled}; - - padding-right: #{$ouds-control-item-space-padding-inline + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap}; - - .control-item-assets-container:last-child { - display: none; - } - - &::after { - content: ""; - } - } - - &:has(input:invalid:hover) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-hover}; - } - - &:has(input:invalid:focus-visible) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-focus}; - } - - &:has(input:invalid:active) { - --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-pressed}; - } - } - .form-check-input { @extend .control-item-indicator; } diff --git a/scss/forms/_validation.scss b/scss/forms/_validation.scss index c48123a716..c74374e44c 100644 --- a/scss/forms/_validation.scss +++ b/scss/forms/_validation.scss @@ -6,7 +6,9 @@ // server-side validation. // scss-docs-start form-validation-states-loop -@each $state, $data in $form-validation-states { - @include form-validation-state($state, $data...); +@if $enable-bootstrap-compatibility { // OUDS mod + @each $state, $data in $form-validation-states { + @include form-validation-state($state, $data...); + } } // scss-docs-end form-validation-states-loop diff --git a/site/src/content/docs/components/checkbox.mdx b/site/src/content/docs/components/checkbox.mdx index f9e8e63c49..5d776bc121 100644 --- a/site/src/content/docs/components/checkbox.mdx +++ b/site/src/content/docs/components/checkbox.mdx @@ -440,12 +440,12 @@ To create a read only checkbox, the input should be replaced by a `span` element -To display an invalid checkbox, add `.is-invalid` to a `.control-item-indicator`. An error icon will be automatically shown, if there is a decorative icon specified it will be hidden. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). +To display an invalid checkbox, add `aria-invalid="true"` to a `.control-item-indicator`. An error icon will be automatically shown, if there is a decorative icon specified it will be hidden. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]).
- +
@@ -459,7 +459,7 @@ To display an invalid checkbox, add `.is-invalid` to a `.control-item-indicator`
- +
@@ -473,7 +473,7 @@ To display an invalid checkbox, add `.is-invalid` to a `.control-item-indicator`
- +
@@ -489,10 +489,10 @@ To display an invalid checkbox, add `.is-invalid` to a `.control-item-indicator` `} /> -To display an invalid checkbox, add `.is-invalid` to a `.form-check-input`. +To display an invalid checkbox, add `aria-invalid="true"` to a `.form-check-input`. - + @@ -507,7 +507,7 @@ If the layout is horizontal, the `
` must enclose the flex containers s
- +
@@ -515,7 +515,7 @@ If the layout is horizontal, the `
` must enclose the flex containers s
- +
@@ -532,7 +532,7 @@ It is possible to use a `
    ` instead of `
    ` but you will need to hand
  • - +
    @@ -546,7 +546,7 @@ It is possible to use a `
      ` instead of `
      ` but you will need to hand
    • - +
      @@ -568,7 +568,7 @@ If you have a checkbox that is not part of a group but needs validation control,
      - +
      diff --git a/site/src/content/docs/components/radio-button.mdx b/site/src/content/docs/components/radio-button.mdx index b2280cf260..9cdc871fa0 100644 --- a/site/src/content/docs/components/radio-button.mdx +++ b/site/src/content/docs/components/radio-button.mdx @@ -424,12 +424,12 @@ To create a read only radio button, the input should be replaced by a `span` ele -To display an invalid radio button, add `.is-invalid` to a `.control-item-indicator`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). +To display an invalid radio button, add `aria-invalid="true"` to a `.control-item-indicator`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]).
      - +
      @@ -439,7 +439,7 @@ To display an invalid radio button, add `.is-invalid` to a `.control-item-indica
      - +
      @@ -451,10 +451,10 @@ To display an invalid radio button, add `.is-invalid` to a `.control-item-indica -To display an invalid radio button, add `.is-invalid` to a `.form-check-input`. +To display an invalid radio button, add `aria-invalid="true"` to a `.form-check-input`. - + @@ -468,7 +468,7 @@ This also works for outlined variant of the component.
      - +
      @@ -476,7 +476,7 @@ This also works for outlined variant of the component.
      - +
      @@ -493,7 +493,7 @@ If the layout is horizontal, the `
      ` must enclose the flex containers s
      - +
      @@ -501,7 +501,7 @@ If the layout is horizontal, the `
      ` must enclose the flex containers s
      - +
      diff --git a/site/src/content/docs/components/switch.mdx b/site/src/content/docs/components/switch.mdx index 915f77f563..477eb48dbd 100644 --- a/site/src/content/docs/components/switch.mdx +++ b/site/src/content/docs/components/switch.mdx @@ -316,7 +316,7 @@ To create a read only switch, the input should be replaced by a `span` element w -To display an invalid switch, add `.is-invalid` to a `.control-item-indicator`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). +To display an invalid switch, add `aria-invalid="true"` to a `.control-item-indicator`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). #### Error text on one switch item @@ -325,7 +325,7 @@ To automatically display an error text on a single switch item, enclose it insid
      - +
      @@ -337,10 +337,10 @@ To automatically display an error text on a single switch item, enclose it insid -To display an invalid switch, add `.is-invalid` to a `.form-check-input`. +To display an invalid switch, add `aria-invalid="true"` to a `.form-check-input`. - + @@ -353,7 +353,7 @@ To add an error message on a single switch item inside a list, the `.switch-item
    • - +
      @@ -383,7 +383,7 @@ If the switch items list is part of a form, add a `
      ` with the class `.
      - +
      @@ -392,7 +392,7 @@ If the switch items list is part of a form, add a `
      ` with the class `.
      - +
      From cefc8abd67c091d25eab0676b1d8043bc34130e2 Mon Sep 17 00:00:00 2001 From: Maxime Lardenois Date: Mon, 23 Feb 2026 15:59:03 +0100 Subject: [PATCH 02/44] feat: improve css in control item and use aria-invalid/:user-invalid to select input --- scss/forms/_control-item.scss | 50 +++++-------------- scss/forms/_select-input.scss | 6 +-- .../content/docs/components/select-input.mdx | 10 ++-- 3 files changed, 20 insertions(+), 46 deletions(-) diff --git a/scss/forms/_control-item.scss b/scss/forms/_control-item.scss index 22f1b1089b..45f4842a5f 100644 --- a/scss/forms/_control-item.scss +++ b/scss/forms/_control-item.scss @@ -12,12 +12,9 @@ display: none; } - // stylelint-disable selector-no-qualifying-type - &:has(input:user-invalid) .control-item-error-message, - &:has(input[aria-invalid="true"]) .control-item-error-message { + &:has(input:is(:user-invalid, [aria-invalid="true"])) .control-item-error-message { display: block; } - // stylelint-enable selector-no-qualifying-type } .control-item-error-message { @@ -79,8 +76,7 @@ } // Handle invalid state - &:has(input[aria-invalid="true"]), - &:has(input:user-invalid) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-enabled}; padding-right: #{$ouds-control-item-space-padding-inline + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap}; @@ -94,18 +90,15 @@ } } - &:has(input[aria-invalid="true"]:hover), - &:has(input:user-invalid:hover) { + &:has(input:hover:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-hover}; } - &:has(input[aria-invalid="true"]:focus-visible), - &:has(input:user-invalid:focus-visible) { + &:has(input:focus-visible:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-focus}; } - &:has(input[aria-invalid="true"]:active), - &:has(input:user-invalid:active) { + &:has(input:active:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-label-color: #{$ouds-color-action-negative-pressed}; } @@ -226,8 +219,7 @@ --#{$prefix}control-item-indicator-color: #{$ouds-color-action-selected}; } - &:user-invalid, - &[aria-invalid="true"] { + &:is(:user-invalid, [aria-invalid="true"]) { --#{$prefix}control-item-indicator-color: #{$ouds-color-action-negative-enabled}; &:hover { @@ -409,8 +401,7 @@ padding-bottom: calc($ouds-control-item-space-padding-block-default - $ouds-divider-border-width); border-bottom: $ouds-divider-border-width solid $ouds-color-border-default; - &:has([aria-invalid="true"]), - .was-validated &:has(input:invalid) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { border-color: var(--#{$prefix}control-item-label-color); } } @@ -418,16 +409,13 @@ .control-item-reverse { flex-direction: row-reverse; - // stylelint-disable selector-no-qualifying-type - &:has(input[aria-invalid="true"]), - .was-validated &:has(input:invalid) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-error-icon-offset-right: unset; --#{$prefix}control-item-error-icon-offset-left: var(--#{$prefix}control-item-error-icon-offset); padding-right: $ouds-control-item-space-padding-inline; padding-left: calc($ouds-control-item-space-padding-inline + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap); } - // stylelint-enable selector-no-qualifying-type } @@ -613,14 +601,12 @@ padding-inline: calc($ouds-control-item-space-padding-inline - $ouds-divider-border-width); border: $ouds-divider-border-width solid $ouds-color-border-default; - // stylelint-disable selector-no-qualifying-type - &:has(input[aria-invalid="true"]) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-error-icon-offset: #{$ouds-control-item-space-padding-inline + $ouds-control-item-space-padding-inline-error-icon - $ouds-divider-border-width}; } - // stylelint-enable selector-no-qualifying-type } - &:has(input:checked:not([aria-invalid="true"], :invalid)) { + &:has(input:checked:not([aria-invalid="true"], :user-invalid)) { border-color: $ouds-color-action-selected; &:hover { @@ -640,8 +626,7 @@ border-color: $ouds-color-action-pressed; } - // stylelint-disable selector-no-qualifying-type - &:has(input[aria-invalid="true"]) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { border-color: var(--#{$prefix}control-item-label-color); } @@ -649,11 +634,6 @@ padding: $ouds-control-item-space-padding-block-default $ouds-control-item-space-padding-inline; border-width: 0; } - // stylelint-enable selector-no-qualifying-type -} - -.was-validated .radio-button-item-outlined:has(input:invalid) { - border-color: var(--#{$prefix}control-item-label-color); } @@ -666,28 +646,24 @@ @extend %control-item; flex-direction: row-reverse; - // stylelint-disable selector-no-qualifying-type - &:has(input[aria-invalid="true"]) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-error-icon-offset-right: unset; --#{$prefix}control-item-error-icon-offset-left: var(--#{$prefix}control-item-error-icon-offset); padding-right: $ouds-control-item-space-padding-inline; padding-left: calc($ouds-control-item-space-padding-inline + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap); } - // stylelint-enable selector-no-qualifying-type &.control-item-reverse { flex-direction: initial; - // stylelint-disable selector-no-qualifying-type - &:has(input[aria-invalid="true"]) { + &:has(input:is([aria-invalid="true"], :user-invalid)) { --#{$prefix}control-item-error-icon-offset-right: var(--#{$prefix}control-item-error-icon-offset); --#{$prefix}control-item-error-icon-offset-left: unset; padding-right: calc($ouds-control-item-space-padding-inline + $ouds-control-item-size-icon + $ouds-control-item-space-column-gap); padding-left: $ouds-control-item-space-padding-inline; } - // stylelint-enable selector-no-qualifying-type } } diff --git a/scss/forms/_select-input.scss b/scss/forms/_select-input.scss index 4028aa4ae2..c34d91af9b 100644 --- a/scss/forms/_select-input.scss +++ b/scss/forms/_select-input.scss @@ -62,8 +62,7 @@ } } -.was-validated .select-input-field:invalid, -.select-input-field.is-invalid { +.select-input-field:is(:user-invalid, [aria-invalid="true"]) { --#{$prefix}text-input-background-color: #{$ouds-color-surface-status-negative-muted}; --#{$prefix}text-input-border-color: #{$ouds-color-action-negative-enabled}; padding-right: calc(var(--#{$prefix}text-input-trailing-action-padding-right) - var(--#{$prefix}text-input-border-width-right) + var(--#{$prefix}text-input-column-gap) + var(--#{$prefix}text-input-trailing-action-width) + var(--#{$prefix}text-input-column-gap-trailing-error) + px-to-rem($ouds-button-size-icon-only)); @@ -225,8 +224,7 @@ } } -.was-validated .select-input-container:has(:invalid), -.select-input-container:has(.is-invalid) { +.select-input-container:has(:user-invalid, [aria-invalid="true"]) { color: $ouds-color-action-negative-enabled; &::before { diff --git a/site/src/content/docs/components/select-input.mdx b/site/src/content/docs/components/select-input.mdx index 228f18b444..1294fe162b 100644 --- a/site/src/content/docs/components/select-input.mdx +++ b/site/src/content/docs/components/select-input.mdx @@ -284,7 +284,7 @@ Add the `readonly` boolean attribute on an input to prevent modification of the -To display an invalid select, add `.is-invalid` to a `.select-input-field` within the `.select-input-container`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). +To display an invalid select, add `aria-invalid="true"` to a `.select-input-field` within the `.select-input-container`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). For accessibility purposes, the invalid state should be associated with a `.error-text` as a sibling of a `.select-input-container` and related to it with an `aria-describedby` attribute when displayed. Note that the `.error-text` will replace the helper text, so it should be descriptive enough to convey the error note that the `.error-text` will replace the helper text, so it should be descriptive enough to convey the error and you must dynamically replace the `aria-describedby` attribute when the select input becomes invalid. @@ -293,7 +293,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro
      - @@ -314,7 +314,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro
      - @@ -335,7 +335,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro
      - @@ -355,7 +355,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro - From 29556e0392ebe6349ce63d69ddce72e278f4a55e Mon Sep 17 00:00:00 2001 From: Maxime Lardenois Date: Mon, 23 Feb 2026 16:18:35 +0100 Subject: [PATCH 03/44] feat: use aria-invalid and user-invalid on text inputs --- scss/forms/_text-input.scss | 28 ++++++++----------- .../content/docs/components/text-input.mdx | 14 +++++----- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/scss/forms/_text-input.scss b/scss/forms/_text-input.scss index d1c131ce21..3b04c68cb7 100644 --- a/scss/forms/_text-input.scss +++ b/scss/forms/_text-input.scss @@ -163,7 +163,7 @@ // Hover state styles - &:hover:has(.text-input-field:not(:focus, :disabled, :read-only, .is-invalid)) { + &:hover:has(.text-input-field:not(:focus, :disabled, :read-only, :user-invalid, [aria-invalid="true"])) { --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-hover}; &:not(.text-input-container-outlined) { --#{$prefix}text-input-background-color: #{$ouds-color-action-support-hover}; @@ -180,7 +180,7 @@ } } - &:has(.text-input-field:focus:not(:disabled, :read-only, .is-invalid)) { + &:has(.text-input-field:focus:not(:disabled, :read-only, :user-invalid, [aria-invalid="true"])) { --#{$prefix}text-input-background-color: var(--#{$prefix}color-action-support-pressed); --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-focus}; &.text-input-container-outlined { @@ -279,7 +279,7 @@ } // Invalid text inputs - &:has(.text-input-field.is-invalid) { + &:has(.text-input-field:is(:user-invalid, [aria-invalid="true"])) { --#{$prefix}text-input-border-color: var(--#{$prefix}color-action-negative-enabled); --#{$prefix}text-input-label-color: var(--#{$prefix}color-action-negative-enabled); @@ -329,7 +329,7 @@ } } - &:has(.text-input-field:focus.is-invalid) { + &:has(.text-input-field:is(:user-invalid, [aria-invalid="true"]):focus) { --#{$prefix}text-input-border-color: var(--#{$prefix}color-action-negative-pressed); --#{$prefix}text-input-label-color: var(--#{$prefix}color-action-negative-pressed); --#{$prefix}text-input-caret-color: var(--#{$prefix}color-action-negative-pressed); @@ -342,13 +342,11 @@ } // Helper text and error text visibility handling - .text-input-container:has(.text-input-field.is-invalid):has(~ .error-text) ~ .helper-text, - .text-input-container:has(.text-input-field:invalid):has(~ .error-text) ~ .helper-text { + .text-input-container:has(.text-input-field:is(:user-invalid, [aria-invalid="true"])):has(~ .error-text) ~ .helper-text { display: none; } - .text-input-container:has(.text-input-field.is-invalid) ~ .error-text, - .text-input-container:has(.text-input-field:invalid) ~ .error-text { + .text-input-container:has(.text-input-field:is(:user-invalid, [aria-invalid="true"])) ~ .error-text { display: block; } } @@ -442,7 +440,7 @@ } // Hover state styles - &:hover:has(.text-area-field:not(:focus, :disabled, :read-only, .is-invalid)) { + &:hover:has(.text-area-field:not(:focus, :disabled, :read-only, :user-invalid, [aria-invalid="true"])) { --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-hover}; &:not(.text-area-container-outlined) { --#{$prefix}text-input-background-color: #{$ouds-color-action-support-hover}; @@ -460,7 +458,7 @@ } } - &:has(.text-area-field:focus:not(:disabled, :read-only, .is-invalid)) { + &:has(.text-area-field:focus:not(:disabled, :read-only, :user-invalid, [aria-invalid="true"])) { --#{$prefix}text-input-background-color: var(--#{$prefix}color-action-support-pressed); --#{$prefix}text-input-border-color: #{$ouds-text-input-color-border-focus}; &.text-area-container-outlined { @@ -502,7 +500,7 @@ } // Invalid text inputs - &:has(.text-area-field.is-invalid) { + &:has(.text-area-field:is(:user-invalid, [aria-invalid="true"])) { --#{$prefix}text-input-border-color: var(--#{$prefix}color-action-negative-enabled); --#{$prefix}text-input-label-color: var(--#{$prefix}color-action-negative-enabled); @@ -537,7 +535,7 @@ } } - &:has(.text-area-field:focus.is-invalid) { + &:has(.text-area-field:is(:user-invalid, [aria-invalid="true"]):focus) { --#{$prefix}text-input-border-color: var(--#{$prefix}color-action-negative-pressed); --#{$prefix}text-input-label-color: var(--#{$prefix}color-action-negative-pressed); --#{$prefix}text-input-caret-color: var(--#{$prefix}color-action-negative-pressed); @@ -550,13 +548,11 @@ } // Helper text and error text visibility handling - .text-area-container:has(.text-area-field.is-invalid):has(~ .error-text) ~ .helper-text, - .text-area-container:has(.text-area-field:invalid):has(~ .error-text) ~ .helper-text { + .text-area-container:has(.text-area-field:is(:user-invalid, [aria-invalid="true"])):has(~ .error-text) ~ .helper-text { display: none; } - .text-area-container:has(.text-area-field.is-invalid) ~ .error-text, - .text-area-container:has(.text-area-field:invalid) ~ .error-text { + .text-area-container:has(.text-area-field:is(:user-invalid, [aria-invalid="true"])) ~ .error-text { display: block; } } diff --git a/site/src/content/docs/components/text-input.mdx b/site/src/content/docs/components/text-input.mdx index 9f41ee7c50..749c56b091 100644 --- a/site/src/content/docs/components/text-input.mdx +++ b/site/src/content/docs/components/text-input.mdx @@ -364,7 +364,7 @@ Add the `readonly` boolean attribute on an input to prevent modification of the -To display an invalid input, add `.is-invalid` to a `.text-input-field` within the `.text-input-container`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). +To display an invalid input, add `aria-invalid="true"` to a `.text-input-field` within the `.text-input-container`. Please take a look at our [Validation page to learn more]([[docsref:/foundation/form-validation]]). For accessibility purposes, the invalid state should be associated with a `.error-text` as a sibling of a `.text-input-container` and related to it with an `aria-describedby` attribute when displayed. Note that the `.error-text` will replace the helper text, so it should be descriptive enough to convey the error and you must dynamically replace the `aria-describedby` attribute when switching the text input to invalid. @@ -373,7 +373,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro
      - +

      Please choose a username. @@ -385,7 +385,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro

      - +

      Address is required. @@ -412,7 +412,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro

      - +

      City is invalid. @@ -425,7 +425,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro

      - +

      @@ -439,7 +439,7 @@ For accessibility purposes, the invalid state should be associated with a `.erro

      - +
      + +
      +
      + + +
      +

      + Username is required. +

      +
      + +
      +
      + + +
      +

      + Comments is required. +

      +
      + +
      +
      +
      + +
      +
      + +

      Description text

      +
      +
      + +
      +
      +
      +
      + +
      +
      + +

      Description text

      +
      +
      + +
      +
      +
      +
      + +
      +
      + +

      Description text

      +
      +
      + +
      +
      +

      Incompatible choices.

      +
      + +
      +
      +
      + +
      +
      + +

      Extra label

      +

      Description text

      +
      +
      +
      +
      + +
      +
      + +

      Description text

      +
      +
      +

      There is an error.

      +
      + +
        +
      • +
        +
        + +
        +
        + +

        Description text

        +
        +
        +

        Cannot be activated at this time.

        +
      • +
      • +
        +
        + +
        +
        + +

        Description text

        +
        +
        +

        Cannot be activated at this time.

        +
      • +
      + +
      +
      +
      + +
      +
      + +

      Description text

      +
      +
      +
      +
      + +
      +
      + +

      Description text

      +
      +
      +

      Incompatible choices.

      +
      + + + `} /> + + From 65a7a8a5cb7295e8809529103ffa401002552a17 Mon Sep 17 00:00:00 2001 From: Hannah Issermann Date: Wed, 11 Feb 2026 18:43:38 +0100 Subject: [PATCH 07/44] Restore form-validation documentation page --- .../docs/foundation/form-validation.mdx | 294 +++++++++++------- .../[version]/assets/js/validate-forms.js | 30 +- 2 files changed, 219 insertions(+), 105 deletions(-) diff --git a/site/src/content/docs/foundation/form-validation.mdx b/site/src/content/docs/foundation/form-validation.mdx index 8e7ee0bd40..6f2725a5c7 100644 --- a/site/src/content/docs/foundation/form-validation.mdx +++ b/site/src/content/docs/foundation/form-validation.mdx @@ -1,6 +1,6 @@ --- title: Form validation -description: Provide valuable, actionable feedback to your users with HTML5 form validation, via browser default behaviors or custom styles and JavaScript. +description: Provide valuable, actionable feedback to your users with HTML5 form validation, via custom styles and JavaScript. aliases: - "/docs/forms/validation/" - "/docs/foundation/form-validation/" @@ -34,39 +34,58 @@ Here’s how form validation works with OUDS Web: - OUDS Web scopes the `:invalid` and `:valid` styles to parent `.was-validated` class, usually applied to the `
      `. Otherwise, any required field without a value shows up as invalid on page load. This way, you may choose when to activate them (typically after form submission is attempted). - To reset the appearance of the form (for instance, in the case of dynamic form submissions using Ajax), remove the `.was-validated` class from the `` again after submission. {/* for [server-side validation](#server-side) */} -- As a fallback, `.is-invalid` and `.is-valid` classes may be used instead of the pseudo-classes. They do not require a `.was-validated` parent class. +- As a fallback, `.is-invalid` class may be used instead of the pseudo-classe `:invalid`. It doesn't require a `.was-validated` parent class. - All modern browsers support the [constraint validation API](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-constraint-validation-api), a series of JavaScript methods for validating form controls. -- Feedback messages may utilize the [browser defaults](#browser-defaults) (different for each browser, and unstylable via CSS) or our custom feedback styles with additional HTML and CSS. +- Feedback messages shouldn't use the browser defaults (different for each browser, and unstylable via CSS) but our custom feedback styles with additional HTML and CSS. +- Feedback messages should use our custom feedback styles with additional HTML and CSS, rather than the browser defaults (which differ for each browser and can't be styled via CSS). - You may provide custom validity messages with `setCustomValidity` in JavaScript. +With that in mind, consider the following demos for our custom form validation styles and optional server-side classes. + ## Custom styles -For custom OUDS Web form validation messages, you’ll need to add the `novalidate` boolean attribute to your ``. This disables the browser default feedback tooltips, but still provides access to the form validation APIs in JavaScript. Try to submit the form below; our JavaScript will intercept the submit button and relay feedback to you. When attempting to submit, you’ll see the `:invalid` and `:valid` styles applied to your form controls. +For custom OUDS Web form validation messages, you’ll need to add the `novalidate` boolean attribute to your ``. This disables the browser default feedback tooltips, but still provides access to the form validation APIs in JavaScript. + +Try to submit the form below; our JavaScript will intercept the submit button, add `.was-validated` to the ``, and you’ll see the `:invalid` and `:valid` styles applied to the fields. For invalid fields, it also associates the invalid feedback/error message with the relevant form field using `aria-describedby` (noting that this attribute allows more than one `id` to be referenced, in case the field already points to additional description/helper text). -Custom feedback styles apply custom colors, borders, focus styles, and background icons to better communicate feedback. Background icons for ` - - - - - - - +
      + Title +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      + +
      -

      - Title is required. +

      + You must choose an option for the title.

      -
      +
      -

      @@ -74,135 +93,202 @@ Custom feedback styles apply custom colors, borders, focus styles, and backgroun

      +
      +
      + + + +
      +

      + Password is required. +

      +
      + +
      +
      + + +
      +

      + Continent is required. +

      +
      +
      - +

      Comments is required.

      -
      -
      +
      +
      - +
      - -

      Description text

      -
      -
      - +
      -
      +

      + You must confirm that you have read the terms and conditions. +

      +
      + +
      +
      - +
      - -

      Description text

      -
      -
      - +
      -
      +

      + You must accept the terms and condition to proceed. +

      +
      + + + `} /> + + + +## Server-side + +We recommend using client-side validation before server-side validation. If server-side validation returns any invalid field, you can indicate it with `.is-invalid`. + +For invalid fields, ensure that the invalid feedback/error message is associated with the relevant form field using `aria-describedby` (noting that this attribute allows more than one `id` to be referenced, in case the field already points to additional description/helper text). + + + +
      + Title +
      - +
      - -

      Description text

      -
      -
      - +
      -

      Incompatible choices.

      -
      - -
      -
      +
      - +
      - -

      Extra label

      -

      Description text

      +
      -
      +
      - +
      - -

      Description text

      +
      -

      There is an error.

      +

      + You must choose an option for the title. +

      -
        -
      • -
        -
        - -
        -
        - -

        Description text

        -
        -
        -

        Cannot be activated at this time.

        -
      • -
      • -
        -
        - -
        -
        - -

        Description text

        -
        -
        -

        Cannot be activated at this time.

        -
      • -
      +
      +
      + + +
      +

      + Username is required. +

      +
      -
      -
      +
      +
      + + + +
      +

      + Password is required. +

      +
      + +
      +
      + + +
      +

      + Continent is required. +

      +
      + +
      +
      + + +
      +

      + Comments is required. +

      +
      + +
      +
      - +
      - -

      Description text

      +
      -
      +

      + You must confirm that you have read the terms and conditions. +

      +
      + +
      +
      - +
      - -

      Description text

      +
      -

      Incompatible choices.

      -
      - - +

      + You must accept the terms and condition to proceed. +

      +
      `} /> - - diff --git a/site/static/[brand]/docs/[version]/assets/js/validate-forms.js b/site/static/[brand]/docs/[version]/assets/js/validate-forms.js index 30ea0aa6b1..75aa2f2629 100644 --- a/site/static/[brand]/docs/[version]/assets/js/validate-forms.js +++ b/site/static/[brand]/docs/[version]/assets/js/validate-forms.js @@ -1,13 +1,41 @@ -// Example starter JavaScript for disabling form submissions if there are invalid fields +// Example starter JavaScript for managing forms with custom validation styles. (() => { 'use strict' + function manageFeedbackMessage(field) { + // Get the ID of the feedback message from the attribute data-feedback-id + const feedbackId = field.getAttribute('data-feedback-id') + + if (feedbackId && field.checkValidity()) { + field.removeAttribute('aria-describedby') + } else if (!field.checkValidity()) { + field.setAttribute('aria-describedby', feedbackId) + } + } + + let inputListenersAdded = false; + // Fetch all the forms we want to apply custom Bootstrap validation styles to const forms = document.querySelectorAll('.needs-validation') // Loop over them and prevent submission Array.from(forms).forEach(form => { + // Gets all the fields of the form (input, select, textarea) + const fields = form.querySelectorAll('input, select, textarea') + form.addEventListener('submit', event => { + // Initially manages feedback messages and add input listeners + if (!inputListenersAdded) { + fields.forEach(field => { + manageFeedbackMessage(field) + + field.addEventListener('input', () => { + manageFeedbackMessage(field) + }) + }) + inputListenersAdded = true + } + if (!form.checkValidity()) { event.preventDefault() event.stopPropagation() From d4103034a42a0340f365e3d02caf748fe9d77a94 Mon Sep 17 00:00:00 2001 From: Hannah Issermann Date: Wed, 11 Feb 2026 18:46:16 +0100 Subject: [PATCH 08/44] Restore form-validation documentation page --- site/src/content/docs/foundation/form-validation.mdx | 1 - site/static/[brand]/docs/[version]/assets/js/validate-forms.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/content/docs/foundation/form-validation.mdx b/site/src/content/docs/foundation/form-validation.mdx index 6f2725a5c7..2b0b21f477 100644 --- a/site/src/content/docs/foundation/form-validation.mdx +++ b/site/src/content/docs/foundation/form-validation.mdx @@ -176,7 +176,6 @@ We recommend using client-side validation before server-side validation. If serv For invalid fields, ensure that the invalid feedback/error message is associated with the relevant form field using `aria-describedby` (noting that this attribute allows more than one `id` to be referenced, in case the field already points to additional description/helper text). -
      Title diff --git a/site/static/[brand]/docs/[version]/assets/js/validate-forms.js b/site/static/[brand]/docs/[version]/assets/js/validate-forms.js index 75aa2f2629..a8be00f7b7 100644 --- a/site/static/[brand]/docs/[version]/assets/js/validate-forms.js +++ b/site/static/[brand]/docs/[version]/assets/js/validate-forms.js @@ -13,7 +13,7 @@ } } - let inputListenersAdded = false; + let inputListenersAdded = false // Fetch all the forms we want to apply custom Bootstrap validation styles to const forms = document.querySelectorAll('.needs-validation') @@ -29,6 +29,7 @@ fields.forEach(field => { manageFeedbackMessage(field) + // eslint-disable-next-line max-nested-callbacks field.addEventListener('input', () => { manageFeedbackMessage(field) }) From 46b80d42380c34668781b281480c63b4d9650f53 Mon Sep 17 00:00:00 2001 From: Hannah Issermann Date: Fri, 13 Feb 2026 08:59:48 +0100 Subject: [PATCH 09/44] fix form ids and needs validation --- .../docs/foundation/form-validation.mdx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/site/src/content/docs/foundation/form-validation.mdx b/site/src/content/docs/foundation/form-validation.mdx index 2b0b21f477..cd984a6ee0 100644 --- a/site/src/content/docs/foundation/form-validation.mdx +++ b/site/src/content/docs/foundation/form-validation.mdx @@ -13,7 +13,7 @@ import { getDocsRelativePath } from '@libs/path' ## Accessibility -Ensure that all form components have an appropriate accessible name so that their purpose can be conveyed to users of assistive technologies. The simplest way to achieve this is to use a `