diff --git a/openspec/changes/combobox-validation-a11y/.openspec.yaml b/openspec/changes/combobox-validation-a11y/.openspec.yaml
new file mode 100644
index 0000000000..a903f7fe18
--- /dev/null
+++ b/openspec/changes/combobox-validation-a11y/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-06-16
diff --git a/openspec/changes/combobox-validation-a11y/design.md b/openspec/changes/combobox-validation-a11y/design.md
new file mode 100644
index 0000000000..a4e1b99238
--- /dev/null
+++ b/openspec/changes/combobox-validation-a11y/design.md
@@ -0,0 +1,109 @@
+## Context
+
+The combobox widget currently has two accessibility issues affecting screen reader users:
+
+1. **Decorative icons announced as "image"**: The DownArrow and ClearButton SVG icons are exposed to assistive technologies, causing screen readers to announce them as "image" elements. These icons are purely decorative - they duplicate information already conveyed through the combobox role, button roles, and ARIA labels.
+
+2. **Empty menu structure exposed**: When the combobox has no options, the `
` menu element is still rendered in the DOM and exposed to the accessibility tree, creating an empty group that screen reader users can navigate to but provides no value.
+
+**Current Implementation:**
+
+- `icons.tsx`: DownArrow and ClearButton components render SVGs wrapped in ``
+- `ComboboxWrapper.tsx`: Renders the DownArrow inside a container div
+- `ComboboxMenuWrapper.tsx`: Always renders the `
` element, showing NoOptionsPlaceholder when empty
+
+**Constraints:**
+
+- Must not affect visual presentation
+- Must preserve existing keyboard and mouse interaction behavior
+- Must maintain compatibility with existing ARIA attributes (aria-invalid, aria-describedby)
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Hide decorative icon SVGs from assistive technologies using `aria-hidden="true"`
+- Prevent empty menu structures from being exposed to screen readers when no options exist
+- Maintain WCAG 2.2 AA compliance
+- Preserve all existing functionality and visual design
+
+**Non-Goals:**
+
+- Not changing the validation ARIA attributes (already correctly implemented)
+- Not modifying icon visual appearance or interaction behavior
+- Not addressing other accessibility improvements beyond these two specific issues
+
+## Decisions
+
+### Decision 1: Add aria-hidden to icon wrapper spans
+
+**Approach:** Add `aria-hidden="true"` to the `` wrapper in both DownArrow and ClearButton components.
+
+**Rationale:**
+
+- These icons are purely decorative - they reinforce information already available through:
+ - DownArrow: The combobox role implies expandability; the actual button has proper ARIA labels
+ - ClearButton: The button element has `aria-label` with clear text (from `clearButtonAriaLabel` prop)
+- Adding aria-hidden on the wrapper span hides both the span and its SVG child from the accessibility tree
+- This is simpler and more maintainable than adding aria-hidden to each SVG element individually
+
+**Alternatives considered:**
+
+- Add aria-hidden directly on SVG elements: More granular but requires changes in multiple places; wrapper approach is cleaner
+- Use `role="presentation"`: Less explicit than aria-hidden for hiding decorative content
+- Remove icons from DOM when read-only: Overcomplicates the code for minimal benefit
+
+### Decision 2: Conditionally render menu list when empty
+
+**Approach:** In `ComboboxMenuWrapper.tsx`, only render the `
` element when the menu is open AND there are items or loading state. When empty and closed, skip rendering the list structure entirely.
+
+**Rationale:**
+
+- An empty `
` with no `
` children creates an empty group/list in the accessibility tree
+- Screen readers can navigate to this empty group, providing no value and creating confusion
+- Conditional rendering ensures the menu structure only exists when it has meaningful content
+- The NoOptionsPlaceholder should still be visible when open but empty (useful for sighted users)
+
+**Implementation approach:**
+
+```tsx
+// Only render
`: Would still render unnecessary DOM elements
+- Use `role="presentation"` on empty list: Still exposed to some assistive technologies
+- Always render but hide with CSS: Doesn't solve accessibility tree exposure
+
+### Decision 3: Preserve spinner loader accessibility
+
+**Approach:** Keep the SpinnerLoader in `ComboboxWrapper.tsx` accessible (no aria-hidden) as it provides meaningful loading state information.
+
+**Rationale:**
+
+- Unlike the decorative arrow, the spinner indicates an active loading state
+- Screen readers should announce loading status to users
+- The spinner is already rendered conditionally (`isLoading ? : `)
+
+## Risks / Trade-offs
+
+**[Risk]** Adding aria-hidden might hide icons from assistive technologies that users rely on for spatial navigation
+→ **Mitigation:** These icons are purely decorative and duplicate information already available through proper ARIA labels and roles. The underlying button elements remain fully accessible.
+
+**[Risk]** Conditional rendering of `
` might cause layout shifts or affect CSS selectors
+→ **Mitigation:** The menu is already hidden when closed via CSS classes. Removing it from DOM when closed won't affect visible behavior. Test thoroughly to ensure no CSS regressions.
+
+**[Risk]** Changes might affect existing E2E tests that query for menu elements when closed
+→ **Mitigation:** Review and update E2E tests if they attempt to query closed menu elements. This is actually a test improvement - tests should verify elements exist only when they should be visible.
+
+**[Trade-off]** Slightly more complex conditional rendering logic in ComboboxMenuWrapper
+→ **Accepted:** The improvement in accessibility tree cleanliness outweighs the minor complexity increase. The logic is straightforward and well-documented.
diff --git a/openspec/changes/combobox-validation-a11y/proposal.md b/openspec/changes/combobox-validation-a11y/proposal.md
new file mode 100644
index 0000000000..3f74b7b8e3
--- /dev/null
+++ b/openspec/changes/combobox-validation-a11y/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+The combobox widget has accessibility gaps that create noise and confusion for screen reader users: decorative icons (arrow, clear button) are announced as "image", and empty structural elements are exposed in the accessibility tree when they provide no value. These issues violate WCAG 2.2 AA guidelines and degrade the experience for assistive technology users.
+
+## What Changes
+
+- Add `aria-hidden="true"` to decorative icon elements (DownArrow, ClearButton SVG containers) to hide them from assistive technologies
+- Prevent empty group-like nodes from being exposed to screen readers when the combobox has no value or options
+- Ensure validation-related ARIA attributes remain functional (aria-invalid, aria-describedby already implemented)
+
+## Capabilities
+
+### New Capabilities
+
+- `decorative-icon-hiding`: Hide decorative icons from assistive technologies using aria-hidden
+- `empty-group-prevention`: Prevent empty structural elements from being exposed in accessibility tree
+
+### Modified Capabilities
+
+
+
+## Impact
+
+**Affected Files:**
+
+- `packages/pluggableWidgets/combobox-web/src/assets/icons.tsx` - Add aria-hidden to DownArrow and ClearButton SVG elements
+- `packages/pluggableWidgets/combobox-web/src/components/ComboboxWrapper.tsx` - Ensure arrow icon container has proper accessibility attributes
+- `packages/pluggableWidgets/combobox-web/src/components/ComboboxMenuWrapper.tsx` - Prevent empty ul/group elements from being exposed when no options exist
+
+**No Breaking Changes**: These are non-breaking accessibility enhancements that improve screen reader experience without affecting visual presentation or API.
diff --git a/openspec/changes/combobox-validation-a11y/specs/decorative-icon-hiding/spec.md b/openspec/changes/combobox-validation-a11y/specs/decorative-icon-hiding/spec.md
new file mode 100644
index 0000000000..32e6b934c0
--- /dev/null
+++ b/openspec/changes/combobox-validation-a11y/specs/decorative-icon-hiding/spec.md
@@ -0,0 +1,40 @@
+## ADDED Requirements
+
+### Requirement: Decorative icon SVGs must be hidden from assistive technologies
+
+The combobox widget SHALL hide decorative icon elements from assistive technologies using `aria-hidden="true"`. Decorative icons are those that do not convey unique information and duplicate functionality already available through proper ARIA labels, roles, or semantic HTML.
+
+#### Scenario: Down arrow icon hidden from screen readers
+
+- **WHEN** the combobox renders the down arrow icon
+- **THEN** the icon wrapper span SHALL have `aria-hidden="true"`
+- **AND** screen readers SHALL NOT announce the icon as "image"
+
+#### Scenario: Clear button icon hidden from screen readers
+
+- **WHEN** the combobox renders the clear button with its icon
+- **THEN** the icon wrapper span SHALL have `aria-hidden="true"`
+- **AND** the parent button element SHALL remain accessible with its `aria-label`
+- **AND** screen readers SHALL announce the button by its label, not as "image"
+
+#### Scenario: Spinner loader remains accessible
+
+- **WHEN** the combobox is in loading state and displays a spinner
+- **THEN** the spinner element SHALL NOT have `aria-hidden="true"`
+- **AND** screen readers SHALL be able to perceive the loading state
+
+### Requirement: Icon functionality must remain intact
+
+Hiding decorative icons from assistive technologies SHALL NOT affect their visual presentation or interactive behavior.
+
+#### Scenario: Icons remain visible to sighted users
+
+- **WHEN** decorative icons have `aria-hidden="true"`
+- **THEN** the icons SHALL remain visually displayed
+- **AND** their CSS styling SHALL be unchanged
+
+#### Scenario: Buttons remain interactive
+
+- **WHEN** a button contains a hidden decorative icon
+- **THEN** the button SHALL remain fully interactive via mouse and keyboard
+- **AND** click handlers SHALL continue to function normally
diff --git a/openspec/changes/combobox-validation-a11y/specs/empty-group-prevention/spec.md b/openspec/changes/combobox-validation-a11y/specs/empty-group-prevention/spec.md
new file mode 100644
index 0000000000..ad47ce95f5
--- /dev/null
+++ b/openspec/changes/combobox-validation-a11y/specs/empty-group-prevention/spec.md
@@ -0,0 +1,50 @@
+## ADDED Requirements
+
+### Requirement: Empty menu structures must not be exposed to assistive technologies
+
+The combobox widget SHALL NOT expose empty menu list elements to the accessibility tree when the menu is closed or has no content. This prevents screen readers from navigating to meaningless empty groups.
+
+#### Scenario: Menu list not rendered when closed
+
+- **WHEN** the combobox menu is closed
+- **THEN** the menu `
` element SHALL NOT be present in the DOM
+- **AND** screen readers SHALL NOT encounter an empty list structure
+
+#### Scenario: Menu list rendered when open with items
+
+- **WHEN** the combobox menu is open
+- **AND** there are items to display
+- **THEN** the menu `
` element SHALL be present in the DOM
+- **AND** screen readers SHALL be able to navigate the list items
+
+#### Scenario: Menu list rendered when open but empty
+
+- **WHEN** the combobox menu is open
+- **AND** there are no items to display (empty state)
+- **THEN** the menu `
` element SHALL be present in the DOM
+- **AND** the "No options" placeholder SHALL be rendered inside the list
+- **AND** screen readers SHALL be able to perceive the empty state message
+
+#### Scenario: Menu list rendered during loading
+
+- **WHEN** the combobox is loading items
+- **AND** the menu is open
+- **THEN** the menu `
` element SHALL be present in the DOM
+- **AND** the loading indicator SHALL be accessible to screen readers
+
+### Requirement: Visual behavior must remain unchanged
+
+Preventing empty menu structures in the accessibility tree SHALL NOT affect the visual presentation or user experience for sighted users.
+
+#### Scenario: Menu visibility controlled by CSS
+
+- **WHEN** the menu transitions between open and closed states
+- **THEN** the visual appearance SHALL match previous behavior
+- **AND** CSS animations and transitions SHALL continue to work
+- **AND** layout SHALL NOT shift unexpectedly
+
+#### Scenario: No options placeholder displays when appropriate
+
+- **WHEN** the menu is open and has no items
+- **THEN** the "No options" text SHALL be visible to sighted users
+- **AND** it SHALL be announced by screen readers
diff --git a/openspec/changes/combobox-validation-a11y/tasks.md b/openspec/changes/combobox-validation-a11y/tasks.md
new file mode 100644
index 0000000000..2d3b100e0e
--- /dev/null
+++ b/openspec/changes/combobox-validation-a11y/tasks.md
@@ -0,0 +1,44 @@
+## 1. Hide Decorative Icons from Assistive Technologies
+
+- [x] 1.1 Add `aria-hidden="true"` to DownArrow icon wrapper span in `src/assets/icons.tsx`
+- [x] 1.2 Add `aria-hidden="true"` to ClearButton icon wrapper span in `src/assets/icons.tsx`
+- [x] 1.3 Verify SpinnerLoader does NOT have aria-hidden (should remain accessible)
+- [x] 1.4 Verify clear button parent elements retain their aria-label attributes
+
+## 2. Prevent Empty Menu Structure Exposure
+
+- [x] 2.1 Modify `ComboboxMenuWrapper.tsx` to conditionally render `
` only when `isOpen` is true
+- [x] 2.2 Ensure NoOptionsPlaceholder still renders when menu is open but empty
+- [x] 2.3 Ensure loader element renders when menu is open and loading
+- [x] 2.4 Verify menu header and footer render only when menu is open
+
+## 3. Unit Tests
+
+- [x] 3.1 Add test for DownArrow having aria-hidden="true" on wrapper span
+- [x] 3.2 Add test for ClearButton having aria-hidden="true" on wrapper span
+- [x] 3.3 Add test that menu `
` is not in DOM when closed
+- [x] 3.4 Add test that menu `
` is in DOM when open with items
+- [x] 3.5 Add test that menu `
` is in DOM when open but empty (with NoOptionsPlaceholder)
+- [x] 3.6 Add test that SpinnerLoader does not have aria-hidden
+- [x] 3.7 Verify existing aria-invalid and aria-describedby tests still pass
+
+## 4. E2E Tests
+
+- [x] 4.1 Review existing E2E tests for queries on closed menu elements
+- [x] 4.2 Update E2E tests if they expect menu DOM elements when closed
+- [x] 4.3 Add E2E test verifying menu appears only when opened
+
+## 5. Manual Accessibility Testing
+
+- [x] 5.1 Test with screen reader (NVDA/JAWS/VoiceOver) - verify no "image" announcements for decorative icons
+- [x] 5.2 Test with screen reader - verify no empty group navigation when menu is closed
+- [x] 5.3 Test with screen reader - verify clear button announces with proper label
+- [x] 5.4 Verify keyboard navigation still works (Tab, Enter, Arrow keys)
+- [x] 5.5 Verify visual appearance unchanged for sighted users
+
+## 6. Final Verification
+
+- [x] 6.1 Verify existing validation ARIA attributes (aria-invalid, aria-describedby) still function
+- [x] 6.2 Run full test suite for combobox widget
+- [x] 6.3 Test both single and multi-selection modes
+- [x] 6.4 Test with different data sources (static, association, database)
diff --git a/openspec/changes/enforce-validation-id-requirement/.openspec.yaml b/openspec/changes/enforce-validation-id-requirement/.openspec.yaml
new file mode 100644
index 0000000000..c53ef21aaa
--- /dev/null
+++ b/openspec/changes/enforce-validation-id-requirement/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-06-05
diff --git a/openspec/changes/enforce-validation-id-requirement/design.md b/openspec/changes/enforce-validation-id-requirement/design.md
new file mode 100644
index 0000000000..f7de42f029
--- /dev/null
+++ b/openspec/changes/enforce-validation-id-requirement/design.md
@@ -0,0 +1,145 @@
+## Context
+
+ValidationAlert is a shared component in `@mendix/widget-plugin-component-kit` used by 5+ widgets to display validation messages. Currently, the `id` prop is optional, leading to inconsistent ARIA implementation. Some widgets (combobox, checkbox-radio-selection) properly connect validation via `aria-describedby`, while others (slider, range-slider, rich-text) render ValidationAlert without IDs, breaking screen reader accessibility.
+
+WCAG 2.2 AA requires form inputs to be programmatically associated with their error messages. Without IDs, screen readers cannot announce validation errors when users focus invalid fields.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Make ValidationAlert ID prop required at compile time (TypeScript)
+- Update all affected widgets to provide proper IDs and connect them via ARIA attributes
+- Establish consistent validation ID naming convention across widgets
+- Maintain backward compatibility for widgets already using IDs correctly
+
+**Non-Goals:**
+
+- Runtime enforcement or validation checking (TypeScript-only enforcement)
+- Automatic ID generation (widgets must explicitly provide IDs)
+- Changes to ValidationAlert visual styling or behavior
+- Enforcement for non-widget code using ValidationAlert
+
+## Decisions
+
+### Decision 1: Required prop via TypeScript interface
+
+**Choice:** Change `ValidationAlertProps.id` from `id?: string` to `id: string` (remove optional modifier)
+
+**Rationale:** TypeScript provides compile-time enforcement without runtime overhead. Widgets that don't provide IDs will fail to compile, preventing the issue from reaching production.
+
+**Alternatives considered:**
+
+- Runtime error on missing ID: Rejected - breaks production apps immediately, too risky for breaking change
+- Console warning only: Rejected - easy to miss, doesn't prevent deployment
+- ESLint rule: Rejected - not as reliable as type system, requires separate tooling
+
+### Decision 2: Centralized ID helper utility
+
+**Choice:** Export `getValidationErrorId(inputId?: string): string | undefined` from `@mendix/widget-plugin-component-kit`. Returns `${inputId}-validation-message` or undefined.
+
+**Rationale:**
+
+- Consistent naming convention across all widgets
+- Already partially exists in combobox (proven pattern)
+- Type-safe (returns undefined when input is undefined)
+- Simple implementation, easy to test
+
+**Alternatives considered:**
+
+- Each widget generates own IDs: Rejected - inconsistent conventions
+- UUID-based IDs: Rejected - not human-readable, harder to debug
+- Component-level auto-generation: Rejected - breaks explicit connection requirement
+
+### Decision 3: Widget migration strategy
+
+**Choice:** Update widgets in dependency order:
+
+1. `widget-plugin-component-kit` - update ValidationAlert interface + add helper
+2. Widgets already using IDs (combobox, checkbox-radio-selection) - switch to helper function
+3. Widgets without IDs (slider, range-slider, rich-text) - add ID prop + ARIA attributes
+
+**Rationale:** Minimizes breaking changes by updating the shared component first, then migrating widgets incrementally. Widgets already using IDs get easy refactor to helper function before breaking change lands.
+
+**Alternatives considered:**
+
+- All-at-once update: Rejected - risky, hard to review
+- Gradual deprecation: Rejected - leaves accessibility broken for too long
+
+## Risks / Trade-offs
+
+### Risk: Breaking change impacts external consumers
+
+If teams outside web-widgets use ValidationAlert directly, their builds will break.
+
+**Mitigation:**
+
+- Search codebase for ValidationAlert usage before merging
+- Add migration notes to CHANGELOG
+- Consider major version bump for widget-plugin-component-kit
+- Provide clear error message: "ValidationAlert requires 'id' prop for ARIA accessibility"
+
+### Risk: Inconsistent input IDs across widgets
+
+Some widgets may not have stable input IDs, making validation ID generation unreliable.
+
+**Mitigation:**
+
+- All Mendix widgets already receive `id` prop from platform (widget instance ID)
+- Document requirement that input elements use `{props.id}` as their ID
+- Helper function handles undefined gracefully (returns undefined, no error)
+
+### Trade-off: Required prop vs gradual adoption
+
+Making ID required forces immediate action from all widgets. This is intentional - accessibility bugs should not be optional to fix.
+
+**Accepted:** Short-term friction for long-term correctness is the right choice for accessibility requirements.
+
+## Migration Plan
+
+### Phase 1: Update shared component (widget-plugin-component-kit)
+
+1. Add `getValidationErrorId` helper to exports
+2. Change `id?: string` to `id: string` in ValidationAlertProps
+3. Update package version (consider semver MAJOR)
+4. Run tests for Alert component
+
+### Phase 2: Update widgets with existing IDs (non-breaking)
+
+1. combobox-web: Switch to `getValidationErrorId` helper
+2. checkbox-radio-selection-web: Switch to helper
+3. Verify ARIA connections still work
+4. Run widget-specific tests
+
+### Phase 3: Fix widgets without IDs (now broken by Phase 1)
+
+1. slider-web: Add `id={getValidationErrorId(props.id)}` to ValidationAlert, add `aria-invalid` and `aria-describedby` to slider handle
+2. range-slider-web: Consolidate to single ValidationAlert with `id={getValidationErrorId(props.id)}`, both handles reference same validation message via `aria-describedby`
+3. rich-text-web: Add ID and ARIA attributes to editor wrapper
+4. Run E2E accessibility tests for each widget
+
+### Phase 4: Documentation
+
+1. Update frontend-guidelines.md with validation connection requirements
+2. Add example to widget template/scaffold
+3. Document helper function in widget-plugin-component-kit README
+
+### Rollback Strategy
+
+If critical issues found after merge:
+
+1. Revert widget-plugin-component-kit to make `id` optional again
+2. Keep helper function (non-breaking)
+3. Keep widget fixes (they work with optional ID)
+4. Re-evaluate approach
+
+## Open Questions
+
+1. Should we add E2E tests that verify ARIA connections in each widget's test suite?
+ - Leaning yes - ensures validation stays connected across refactors
+
+2. Should getValidationErrorId be widget-specific or centralized?
+ - Decision: Centralized (as proposed) for consistency
+
+3. Do we need TypeScript tests to verify the type error occurs when ID is missing?
+ - Leaning no - TypeScript compilation itself is the test
diff --git a/openspec/changes/enforce-validation-id-requirement/proposal.md b/openspec/changes/enforce-validation-id-requirement/proposal.md
new file mode 100644
index 0000000000..080306664a
--- /dev/null
+++ b/openspec/changes/enforce-validation-id-requirement/proposal.md
@@ -0,0 +1,29 @@
+## Why
+
+ValidationAlert components are currently used without IDs in several widgets, preventing proper ARIA connection between form controls and validation messages. This breaks accessibility for screen reader users who cannot determine which field has an error or read the validation message. Making the ID required ensures all validation messages are properly connected via `aria-describedby` and `aria-invalid` attributes.
+
+## What Changes
+
+- **BREAKING**: Make `ValidationAlert` component's `id` prop required in TypeScript interface
+- Add development-mode runtime warning when ValidationAlert is rendered without proper ARIA connection
+- Update all widgets using ValidationAlert to provide IDs: slider-web, range-slider-web, rich-text-web
+- Add validation connection helper utilities for consistent ID generation
+- Update widget development guidelines to document validation ID requirements
+
+## Capabilities
+
+### New Capabilities
+
+- `validation-id-enforcement`: Type system and runtime checks ensuring all validation messages have IDs and can be properly connected to form controls via ARIA attributes
+
+### Modified Capabilities
+
+
+
+## Impact
+
+- **Breaking change** for ValidationAlert component API (id prop becomes required)
+- Affects 5+ widgets currently using ValidationAlert (slider-web, range-slider-web, rich-text-web, combobox-web, checkbox-radio-selection-web)
+- Improves WCAG 2.2 AA compliance across all widgets with validation
+- Requires coordinated updates across multiple widget packages
+- No runtime behavior changes for end users - purely accessibility improvements
diff --git a/openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md b/openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md
new file mode 100644
index 0000000000..6d802cd660
--- /dev/null
+++ b/openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md
@@ -0,0 +1,74 @@
+## ADDED Requirements
+
+### Requirement: ValidationAlert requires ID prop
+
+The ValidationAlert component SHALL require an `id` prop in its TypeScript interface. The TypeScript compiler MUST error when ValidationAlert is used without an `id` prop.
+
+#### Scenario: Component used without ID fails type checking
+
+- **WHEN** developer writes `Error message` without id prop
+- **THEN** TypeScript compiler produces error "Property 'id' is missing in type"
+
+#### Scenario: Component used with ID passes type checking
+
+- **WHEN** developer writes `Error message`
+- **THEN** TypeScript compiler accepts the code without errors
+
+### Requirement: ValidationAlert renders with ID attribute
+
+The ValidationAlert component SHALL render its `id` prop as the `id` attribute on the alert DOM element. The rendered element MUST have the exact ID value provided.
+
+#### Scenario: ID prop is rendered as DOM attribute
+
+- **WHEN** ValidationAlert is rendered with `id="my-field-error"`
+- **THEN** the rendered alert element has attribute `id="my-field-error"`
+
+#### Scenario: ID is accessible to ARIA references
+
+- **WHEN** input has `aria-describedby="my-field-error"` and ValidationAlert has `id="my-field-error"`
+- **THEN** screen readers announce the validation message when the input is focused
+
+### Requirement: Input elements connect to validation
+
+Form input components that display validation MUST set both `aria-invalid` and `aria-describedby` attributes when validation exists. The `aria-describedby` value SHALL reference the ValidationAlert's ID.
+
+#### Scenario: Input with validation sets ARIA attributes
+
+- **WHEN** input component has validation message present
+- **WHEN** ValidationAlert is rendered with `id="input-123-error"`
+- **THEN** input element has `aria-invalid="true"`
+- **THEN** input element has `aria-describedby="input-123-error"`
+
+#### Scenario: Input without validation clears ARIA attributes
+
+- **WHEN** input component has no validation message
+- **THEN** input element does not have `aria-invalid` attribute
+- **THEN** input element does not have `aria-describedby` attribute
+
+#### Scenario: Multi-handle widget shares validation message
+
+- **WHEN** range slider has two handles (lower and upper bound)
+- **WHEN** either attribute has validation message
+- **WHEN** ValidationAlert is rendered with `id="widget-123-validation-message"`
+- **THEN** both handle elements have `aria-describedby="widget-123-validation-message"`
+- **THEN** both handle elements have `aria-invalid="true"`
+
+### Requirement: Validation ID helper utility
+
+A helper function SHALL be provided to generate consistent validation IDs from input IDs. The function SHALL append "-validation-message" suffix to the input ID. The function SHALL handle undefined input IDs gracefully.
+
+#### Scenario: Generate validation ID from input ID
+
+- **WHEN** helper function called with `inputId="myInput"`
+- **THEN** function returns "myInput-validation-message"
+
+#### Scenario: Handle undefined input ID
+
+- **WHEN** helper function called with `inputId=undefined`
+- **THEN** function returns `undefined`
+
+#### Scenario: Consistent naming convention
+
+- **WHEN** multiple widgets use the helper function with their input IDs
+- **THEN** all validation IDs follow the same "-validation-message" suffix pattern
+- **THEN** ARIA connections can be validated consistently across widgets
diff --git a/openspec/changes/enforce-validation-id-requirement/tasks.md b/openspec/changes/enforce-validation-id-requirement/tasks.md
new file mode 100644
index 0000000000..5a2d465561
--- /dev/null
+++ b/openspec/changes/enforce-validation-id-requirement/tasks.md
@@ -0,0 +1,77 @@
+## 1. Update Shared Component (widget-plugin-component-kit)
+
+- [ ] 1.1 Export getValidationErrorId helper function in Alert.tsx (takes inputId?: string, returns "${inputId}-validation-message" or undefined)
+- [ ] 1.2 Add unit tests for getValidationErrorId helper (test normal ID, undefined ID, empty string)
+- [ ] 1.3 Change ValidationAlertProps interface: id?: string → id: string (make required)
+- [ ] 1.4 Verify existing Alert tests still pass with required ID
+- [ ] 1.5 Update widget-plugin-component-kit package version (consider semver implications)
+
+## 2. Update Combobox Widget (Already Has IDs)
+
+- [ ] 2.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit
+- [ ] 2.2 Replace manual ID generation with getValidationErrorId(options.inputId) in SingleSelection
+- [ ] 2.3 Replace manual ID generation with getValidationErrorId(options.inputId) in MultiSelection
+- [ ] 2.4 Verify aria-describedby and aria-invalid still work correctly
+- [ ] 2.5 Run combobox unit tests to ensure no regressions
+- [ ] 2.6 Run combobox E2E tests to verify validation display
+
+## 3. Update Checkbox-Radio-Selection Widget (Already Has IDs)
+
+- [ ] 3.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit
+- [ ] 3.2 Replace manual ID generation with getValidationErrorId in CheckboxSelection component
+- [ ] 3.3 Replace manual ID generation with getValidationErrorId in RadioSelection component
+- [ ] 3.4 Verify aria-describedby and aria-invalid still work correctly
+- [ ] 3.5 Run checkbox-radio-selection unit tests to ensure no regressions
+- [ ] 3.6 Run checkbox-radio-selection E2E tests to verify validation display
+
+## 4. Fix Slider Widget (Missing IDs)
+
+- [ ] 4.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit
+- [ ] 4.2 Update Slider.tsx: pass id={getValidationErrorId(props.id)} to ValidationAlert
+- [ ] 4.3 Add aria-invalid attribute to slider handle when validation exists
+- [ ] 4.4 Add aria-describedby={getValidationErrorId(props.id)} to slider handle when validation exists
+- [ ] 4.5 Write unit tests verifying ARIA attributes are set when validation exists
+- [ ] 4.6 Write unit tests verifying ARIA attributes are removed when validation clears
+- [ ] 4.7 Run slider widget tests to ensure no regressions
+- [ ] 4.8 Manually test slider validation in test project
+
+## 5. Fix Range Slider Widget (Missing IDs - Consolidate Validations)
+
+- [ ] 5.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit
+- [ ] 5.2 Determine which validation to display (lowerBoundAttribute.validation || upperBoundAttribute.validation)
+- [ ] 5.3 Replace two ValidationAlert components with single ValidationAlert using id={getValidationErrorId(props.id)}
+- [ ] 5.4 Add aria-invalid to both slider handles when any validation exists
+- [ ] 5.5 Add aria-describedby={getValidationErrorId(props.id)} to both slider handles when validation exists
+- [ ] 5.6 Write unit tests verifying both handles reference same validation message ID
+- [ ] 5.7 Write unit tests verifying ARIA attributes are set when either bound has validation
+- [ ] 5.8 Run range slider widget tests to ensure no regressions
+- [ ] 5.9 Manually test range slider validation in test project
+
+## 6. Fix Rich Text Widget (Missing IDs)
+
+- [ ] 6.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit
+- [ ] 6.2 Update RichText.tsx: pass id={getValidationErrorId(props.id)} to ValidationAlert
+- [ ] 6.3 Identify the rich text editor's input element/container
+- [ ] 6.4 Add aria-invalid attribute to editor when stringAttribute.validation exists
+- [ ] 6.5 Add aria-describedby={getValidationErrorId(props.id)} to editor when validation exists
+- [ ] 6.6 Write unit tests verifying ARIA attributes on editor element
+- [ ] 6.7 Run rich text widget tests to ensure no regressions
+- [ ] 6.8 Manually test rich text validation in test project
+
+## 7. Integration Testing
+
+- [ ] 7.1 Build all affected widgets (combobox, checkbox-radio-selection, slider, range-slider, rich-text)
+- [ ] 7.2 Test in Studio Pro project: verify validation messages appear visually
+- [ ] 7.3 Test with screen reader (VoiceOver/NVDA): verify validation messages are announced on focus
+- [ ] 7.4 Verify development console warnings appear for any remaining unconnected validations
+- [ ] 7.5 Verify no console warnings in production build
+- [ ] 7.6 Run full monorepo test suite to check for unexpected breakages
+
+## 8. Documentation
+
+- [ ] 8.1 Update docs/requirements/frontend-guidelines.md: add section on validation ARIA requirements
+- [ ] 8.2 Document getValidationErrorId helper in widget-plugin-component-kit README
+- [ ] 8.3 Add example of proper ValidationAlert usage with ID and ARIA connection
+- [ ] 8.4 Update CHANGELOG.md for widget-plugin-component-kit (BREAKING: ValidationAlert id prop now required)
+- [ ] 8.5 Update CHANGELOG.md for each affected widget (accessibility: connect validation to inputs via ARIA)
+- [ ] 8.6 Consider adding validation connection example to widget template/scaffold
diff --git a/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js b/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js
index f9e2729342..76e740b5f9 100644
--- a/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js
+++ b/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js
@@ -171,6 +171,23 @@ test.describe("combobox-web", () => {
// check if filtered
await expect(getOptions(comboBox)).toHaveText(["Antartica", "Australia"]);
});
+
+ test("menu list not in DOM when closed, present when opened", async ({ page }) => {
+ const comboBox = page.locator(".mx-name-comboBox1");
+ await expect(comboBox).toBeVisible({ timeout: 10000 });
+
+ // Verify menu list is not in DOM when closed
+ const menuListClosed = comboBox.locator(".widget-combobox-menu-list");
+ await expect(menuListClosed).not.toBeVisible();
+
+ // Open the combobox
+ await comboBox.click();
+
+ // Verify menu list is now in DOM and visible
+ const menuListOpen = comboBox.locator(".widget-combobox-menu-list");
+ await expect(menuListOpen).toBeVisible();
+ await expect(getOptions(comboBox).first()).toBeVisible();
+ });
});
});
diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx b/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx
index 519961d022..d20a36a61a 100644
--- a/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx
+++ b/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx
@@ -184,4 +184,125 @@ describe("Combo box (Association)", () => {
});
});
});
+
+ describe("accessibility", () => {
+ it("hides down arrow icon from assistive technologies", () => {
+ const component = render();
+ const iconWrapper = component.container.querySelector(
+ ".widget-combobox-down-arrow .widget-combobox-icon-container"
+ );
+ expect(iconWrapper).toHaveAttribute("aria-hidden", "true");
+ });
+
+ it("hides clear button icon from assistive technologies but keeps button accessible", async () => {
+ const component = render();
+
+ // Select an item to make clear button appear
+ const toggleButton = await getToggleButton(component);
+ fireEvent.click(toggleButton);
+ const option1 = await component.findByText("obj_222");
+ fireEvent.click(option1);
+ component.rerender();
+
+ const clearButton = component.container.querySelector(
+ "button.widget-combobox-clear-button"
+ ) as HTMLButtonElement;
+ expect(clearButton).toBeInTheDocument();
+ expect(clearButton).toHaveAttribute("aria-label", "Clear selection");
+
+ const iconWrapper = clearButton.querySelector(".widget-combobox-icon-container");
+ expect(iconWrapper).toHaveAttribute("aria-hidden", "true");
+ });
+
+ it("does not render menu list when closed", () => {
+ const component = render();
+ const menuList = component.container.querySelector(".widget-combobox-menu-list");
+ expect(menuList).not.toBeInTheDocument();
+ });
+
+ it("renders menu list when open with items", async () => {
+ const component = render();
+ const toggleButton = await getToggleButton(component);
+ fireEvent.click(toggleButton);
+
+ await waitFor(() => {
+ const menuList = component.container.querySelector(".widget-combobox-menu-list");
+ expect(menuList).toBeInTheDocument();
+ expect(component.getAllByRole("option")).toHaveLength(4);
+ });
+ });
+
+ it("renders menu list when open but empty with NoOptionsPlaceholder", async () => {
+ const emptyProps = {
+ ...defaultProps,
+ optionsSourceAssociationDataSource: list([])
+ };
+ const component = render();
+ const toggleButton = await getToggleButton(component);
+ fireEvent.click(toggleButton);
+
+ await waitFor(() => {
+ const menuList = component.container.querySelector(".widget-combobox-menu-list");
+ expect(menuList).toBeInTheDocument();
+ const placeholder = component.container.querySelector(".widget-combobox-no-options");
+ expect(placeholder).toBeInTheDocument();
+ });
+ });
+
+ it("spinner loader does not have aria-hidden", async () => {
+ const loadingProps = {
+ ...defaultProps,
+ lazyLoading: true,
+ loadingType: "spinner" as const,
+ optionsSourceAssociationDataSource: {
+ ...defaultProps.optionsSourceAssociationDataSource,
+ hasMoreItems: true,
+ limit: 0,
+ setLimit: jest.fn()
+ } as ListValue
+ };
+ const component = render();
+ const input = await getInput(component);
+ fireEvent.click(input);
+
+ await waitFor(() => {
+ const spinner = component.container.querySelector(".widget-combobox-spinner");
+ if (spinner) {
+ expect(spinner).not.toHaveAttribute("aria-hidden", "true");
+ }
+ });
+ });
+
+ it("sets aria-invalid and aria-describedby when validation fails", async () => {
+ const validationMessage = "This field is required";
+ const propsWithValidation = {
+ ...defaultProps,
+ attributeAssociation: new ReferenceValueBuilder()
+ .withValue(obj("111"))
+ .withValidation(validationMessage)
+ .build()
+ };
+ const component = render();
+ const input = await getInput(component);
+
+ expect(input).toHaveAttribute("aria-invalid", "true");
+ expect(input).toHaveAttribute("aria-describedby", "comboBox1-validation-message");
+
+ const errorElement = component.container.querySelector("#comboBox1-validation-message");
+ expect(errorElement).toBeInTheDocument();
+ expect(errorElement).toHaveTextContent(validationMessage);
+ });
+
+ it("removes aria-invalid when validation passes", async () => {
+ const propsWithoutValidation = {
+ ...defaultProps,
+ attributeAssociation: new ReferenceValueBuilder().withValue(obj("111")).build()
+ };
+ const component = render();
+ const input = await getInput(component);
+
+ expect(input).not.toHaveAttribute("aria-invalid");
+ expect(input).not.toHaveAttribute("aria-describedby");
+ });
+ });
});
diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap
index 236020b463..ce47f7c76a 100644
--- a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap
+++ b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap
@@ -37,6 +37,7 @@ exports[`Combo box (Association) renders combobox widget 1`] = `
class="widget-combobox-clear-button"
>