Improve tag-list accessibility#3214
Conversation
🦋 Changeset detectedLatest commit: b6ae79f The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR 💥 An error occurred when fetching the changed packages and changesets in this PR |
There was a problem hiding this comment.
Pull request overview
This PR aims to improve accessibility for the sl-tag component (particularly when tags are removable) by shifting removal interaction to an explicit, labeled remove button and updating related localization, styles, stories, and tests.
Changes:
- Replaced the previous “press Backspace/Delete” ARIA instruction with an explicit remove button
aria-label(localized) and updated NL locale resources accordingly. - Updated
sl-tagfocus handling and styling to support a host-level focus outline via custom states anddelegatesFocus. - Adjusted Storybook stories and component tests to reflect the new removable/disabled behaviors.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/locales/src/nl.xlf | Removes old removal-instructions string; adds new sl.tag.remove translation. |
| packages/locales/src/nl.ts | Removes old key and adds a new remove-label entry (currently mismatched vs XLF/component id). |
| packages/components/tag/src/tag.ts | Implements accessible remove button labeling and new focus handling; removes old ARIA description logic. |
| packages/components/tag/src/tag.stories.ts | Simplifies max-width styling and adds a removable+disabled story. |
| packages/components/tag/src/tag.spec.ts | Updates/extends tests for reflectable removable, focus delegation, and remove button labeling/disabled behavior. |
| packages/components/tag/src/tag.scss | Switches focus styling to :state(focus-visible) and tweaks button outline/disabled interaction behavior. |
🕸 Website previewYou can view a preview here (commit |
🕸 Storybook previewYou can view a preview here (commit |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
packages/components/tag/src/tag-list.ts:346
tag.role = 'listitem'andtag.setAttribute('role', 'listitem')are redundant because the IDLroleproperty reflects to theroleattribute. Keeping only one avoids duplication and makes it clearer where the role is set.
tag.role = 'listitem';
tag.size = this.size;
tag.variant = this.variant;
tag.setAttribute('role', 'listitem');
});
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
packages/components/tag/src/tag.ts:184
- Tooltip visibility is only recalculated from the
ResizeObservercallback. If the slotted label text changes (slotchange) without affecting the host element's size,ResizeObservermay not fire andthis.tooltipcan become stale. Consider recalculating overflow (or scheduling#onResize()) after updatingthis.labelin#onSlotChange.
#onSlotChange(event: Event & { target: HTMLSlotElement }): void {
this.label = event.target
.assignedNodes({ flatten: true })
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent?.trim())
.join('');
}
michal-sanoma
left a comment
There was a problem hiding this comment.
suggestion: Small a11y/UX concern: because #onFocus() always adds focus-visible, the host behaves like :focus instead of native :focus-visible. Would it make sense to track input source and apply the ring only for keyboard interaction?
a11ymiko
left a comment
There was a problem hiding this comment.
For Stacked variants keyboard user cannot set focus on tag with tooltip. sl-tag has tabindex=0 but keyboard focus cannot be place on this tag.
a11ymiko
left a comment
There was a problem hiding this comment.
For Stacked Removable variant user cannot place focus on any tag. This happens on Safari, Chrome and Firefox on macOS and Edge and Chrome on Win11. On Firefox on Win11 user can place focus on some removable tag, but it's not the first one in the list.
Screen.Recording.2026-04-22.at.08.48.35.mov
a11ymiko
left a comment
There was a problem hiding this comment.
Minor issue but on Chrome on macOS tag with Overflow has it's content announced twice (once from tag text and once from tooltip text). Not adding 'aria-describedby' to div in Overflow sl-tag will fix that issue in Chrome without making an issue for other OSes and browsers.
Screen.Recording.2026-04-22.at.09.38.10.mov
a11ymiko
left a comment
There was a problem hiding this comment.
For Stacked tag lists the first tag (with number of stacked items) doesn't have role=listitem. It should have role=listitem because it's inside sl-tag-list role="list".
| <source>No later than <x id="0" equiv-text="${format(this.max, this.locale, this.#helperTextFormatOptions)}"/></source> | ||
| <target>Uiterlijk <x id="0" equiv-text="${format(this.max, this.locale, this.#helperTextFormatOptions)}"/></target> | ||
| </trans-unit> | ||
| <trans-unit id="sl.tag.remove"> |
There was a problem hiding this comment.
should we not add it to spanish and polish as well?
| <trans-unit id="sl.tag.removalInstructions"> | ||
| <source>Press the delete or backspace key to remove this item</source> | ||
| <target>Druk op de delete- of backspacetoets om dit item te verwijderen</target> | ||
| </trans-unit> |
There was a problem hiding this comment.
should we keep this in as deprecated? If people update the locales but don't update the tag list component it will break the translation.
What changed
This PR improves the accessibility of the
<sl-tag>component's remove button by reworking how focus, keyboard interaction, and disabled state are handled.Closes #2868
<sl-tag>componentdelegatesFocus: true: Added to shadow root options so that focusing the host element automatically delegates focus to the inner elements. This removes the need to manually managetabindexon the host.:host(:focus-visible)selector with:host(:state(focus-visible))backed byElementInternals. Focus/blur handlers on both the label wrapper and the button toggle thefocus-visiblestate, giving more precise control over when the focus ring is shown.<div part="label">: The slot is now wrapped in a<div>that acts as the label part. It receivestabindex="0"only when a tooltip is shown and the tag is not disabled/removable. Focus/blur events bubble up from this element to drive the custom focus-visible state.removable: Previously the button was hidden whendisabled. Now it is always rendered andaria-disabled="true"is set instead of?disabled. This ensures the button remains discoverable to assistive technologies even when disabled.aria-hidden="true"on the button with a localizedaria-label("Remove tag '<label>'") so screen readers announce the action correctly.@keydownis now handled on the button itself (Backspace/Delete to remove) instead of relying onEventsControlleron the host.Tooltipinstance approach is replaced with a reactive@state() tooltipboolean. When overflow is detected viaResizeObserver, the tooltip is rendered declaratively in the template and referenced viaaria-describedby.tabindexandaria-descriptionmanagement: Theupdated()lifecycle override was removed.tabindexandaria-descriptionare no longer set imperatively on the host element.removablereflects to attribute: The property now usesreflect: trueso CSS can target:host([removable])reliably.@csspart labeland@csspart buttonJSDoc documentation.pointer-events: nonefrom the disabled CSS rule so the remove button is still clickable (but guarded byaria-disabledlogic).outline: 0to the button to suppress the native focus ring (the host's custom focus-visible state handles it).slotto[part='label']to target the new<div>wrapper.<sl-tag-list>componenttag.role = 'listitem'directly (in addition tosetAttribute) for consistency.@customElement sl-tag-listJSDoc tag.Locales
sl.tag.removalInstructions(no longer used).sl.tag.removewith a template string for the button'saria-label.Stories
OverflowRemovablestory to test overflow behavior on removable tags.RemovableDisabledstory to show the disabled + removable state.styleMapwithifDefinedfor the inline style on the tag story.