From 17eb8e03e0c48fe7c6ee14805603d9ff60170aab Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Wed, 8 Apr 2026 16:38:50 +0200 Subject: [PATCH] fix(ui): wire up aria-describedby for form field descriptions Form field descriptions were visually rendered but not connected to inputs via aria-describedby, making them invisible to screen readers. Added a descriptionSlot prop to FieldLabel that renders the description as a React Aria element, letting React Aria automatically wire up aria-describedby on the associated input. Applied to TextField, PasswordField, SearchField, Select, RadioGroup, and CheckboxGroup. Slider already handled this manually and is unchanged. Also added a missing WithDescription story for RadioGroup. Signed-off-by: Johan Persson --- .changeset/spotty-hounds-teach.md | 7 +++++++ packages/ui/report.api.md | 2 ++ .../src/components/CheckboxGroup/CheckboxGroup.tsx | 1 + .../ui/src/components/FieldLabel/FieldLabel.tsx | 12 +++++++++--- .../ui/src/components/FieldLabel/definition.ts | 1 + packages/ui/src/components/FieldLabel/types.ts | 5 +++++ .../src/components/PasswordField/PasswordField.tsx | 1 + .../components/RadioGroup/RadioGroup.stories.tsx | 14 ++++++++++++++ .../ui/src/components/RadioGroup/RadioGroup.tsx | 1 + .../ui/src/components/SearchField/SearchField.tsx | 1 + packages/ui/src/components/Select/Select.tsx | 1 + packages/ui/src/components/TextField/TextField.tsx | 1 + 12 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 .changeset/spotty-hounds-teach.md diff --git a/.changeset/spotty-hounds-teach.md b/.changeset/spotty-hounds-teach.md new file mode 100644 index 0000000000..b078646184 --- /dev/null +++ b/.changeset/spotty-hounds-teach.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed form field descriptions not being connected to inputs via `aria-describedby`, making them accessible to screen readers. Added a `descriptionSlot` prop to `FieldLabel` that uses React Aria's slot mechanism to automatically wire up the connection. + +**Affected components:** FieldLabel, TextField, PasswordField, SearchField, Select, RadioGroup, CheckboxGroup diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 3b79537f54..181fa48731 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1243,6 +1243,7 @@ export const FieldLabelDefinition: { readonly htmlFor: {}; readonly id: {}; readonly descriptionId: {}; + readonly descriptionSlot: {}; readonly className: {}; }; }; @@ -1255,6 +1256,7 @@ export type FieldLabelOwnProps = { htmlFor?: string; id?: string; descriptionId?: string; + descriptionSlot?: string; className?: string; }; diff --git a/packages/ui/src/components/CheckboxGroup/CheckboxGroup.tsx b/packages/ui/src/components/CheckboxGroup/CheckboxGroup.tsx index 708b6e4b86..d835e0da52 100644 --- a/packages/ui/src/components/CheckboxGroup/CheckboxGroup.tsx +++ b/packages/ui/src/components/CheckboxGroup/CheckboxGroup.tsx @@ -68,6 +68,7 @@ export const CheckboxGroup = forwardRef( label={label} secondaryLabel={secondaryLabelText} description={description} + descriptionSlot="description" />
{children}
diff --git a/packages/ui/src/components/FieldLabel/FieldLabel.tsx b/packages/ui/src/components/FieldLabel/FieldLabel.tsx index 1b451be909..516d48b3a8 100644 --- a/packages/ui/src/components/FieldLabel/FieldLabel.tsx +++ b/packages/ui/src/components/FieldLabel/FieldLabel.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Label } from 'react-aria-components'; +import { Label, Text } from 'react-aria-components'; import { forwardRef } from 'react'; import type { FieldLabelProps } from './types'; import { useDefinition } from '../../hooks/useDefinition'; @@ -35,6 +35,7 @@ export const FieldLabel = forwardRef( htmlFor, id, descriptionId, + descriptionSlot, } = ownProps; if (!label) return null; @@ -52,9 +53,14 @@ export const FieldLabel = forwardRef( )} {description && ( -
+ {description} -
+
)} ); diff --git a/packages/ui/src/components/FieldLabel/definition.ts b/packages/ui/src/components/FieldLabel/definition.ts index 399ea9e54d..5d9364177f 100644 --- a/packages/ui/src/components/FieldLabel/definition.ts +++ b/packages/ui/src/components/FieldLabel/definition.ts @@ -37,6 +37,7 @@ export const FieldLabelDefinition = defineComponent()({ htmlFor: {}, id: {}, descriptionId: {}, + descriptionSlot: {}, className: {}, }, }); diff --git a/packages/ui/src/components/FieldLabel/types.ts b/packages/ui/src/components/FieldLabel/types.ts index ca18c6c8c1..b20a1cf59f 100644 --- a/packages/ui/src/components/FieldLabel/types.ts +++ b/packages/ui/src/components/FieldLabel/types.ts @@ -46,6 +46,11 @@ export type FieldLabelOwnProps = { */ descriptionId?: string; + /** + * The slot name to set on the description's React Aria `` element. + */ + descriptionSlot?: string; + className?: string; }; diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx index cbff6063a4..4a7718555e 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -69,6 +69,7 @@ export const PasswordField = forwardRef( label={label} secondaryLabel={secondaryLabelText} description={description} + descriptionSlot="description" />
( + + Bulbasaur + Charmander + Squirtle + + ), +}); + export const Horizontal = meta.story({ args: { ...Default.input.args, diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.tsx b/packages/ui/src/components/RadioGroup/RadioGroup.tsx index 8e869da988..b1175a701a 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.tsx +++ b/packages/ui/src/components/RadioGroup/RadioGroup.tsx @@ -64,6 +64,7 @@ export const RadioGroup = forwardRef( label={label} secondaryLabel={secondaryLabelText} description={description} + descriptionSlot="description" />
{children}
diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx index 1df389d5a3..d8f892edab 100644 --- a/packages/ui/src/components/SearchField/SearchField.tsx +++ b/packages/ui/src/components/SearchField/SearchField.tsx @@ -96,6 +96,7 @@ export const SearchField = forwardRef( label={label} secondaryLabel={secondaryLabelText} description={description} + descriptionSlot="description" />
diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index a2318d3d08..16f8940fdb 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -59,6 +59,7 @@ export const TextField = forwardRef( label={label} secondaryLabel={secondaryLabelText} description={description} + descriptionSlot="description" />