Skip to content

SelectMenu/Select: highlighted option fails WCAG 1.4.11 — ~1:1 contrast between highlight and menu background #6588

Description

@OzzyCzech

Environment

  • @nuxt/ui 4.8.1 (Vue + @nuxt/ui/vite plugin), reproducible with the default theme anywhere, including the docs demos
  • Latest release checked: v4.8.2 (theme unchanged on v4 branch)

Is this bug related to Nuxt or Vue?

Both (default theme, framework-independent)

Package

v4.x

Version

v4.8.1 / v4 branch

Reproduction

Open any USelectMenu/USelect demo on the docs with the default theme, e.g. https://ui.nuxt.com/docs/components/select-menu, open the menu and hover an option or navigate with arrow keys. The highlight is barely perceptible in light mode and effectively invisible in dark mode. (No custom playground needed — this is the default theme on the docs site itself; happy to provide a play.ui.nuxt.com link if required.)

Description

Where the problem is

The highlighted (hovered / keyboard-active) option paints its background via a ::before overlay using bg-elevated/50:

data-highlighted:not-data-disabled:before:bg-elevated/50

With the default neutral: 'slate' palette this computes to:

Mode Menu background (bg-default) Highlight (bg-elevated/50 blended) Contrast ratio
light #ffffff slate-100 @ 50% → ≈ #f8fafc ≈ 1.03:1
dark slate-900 #0f172a slate-800 @ 50% → ≈ #162032 ≈ 1.1:1

WCAG 2.1 SC 1.4.11 Non-text Contrast requires 3:1 for visual information that identifies UI component states. Both modes are an order of magnitude below that.

This is more than a cosmetic issue: inside an open listbox/combobox, DOM focus stays on the trigger/input and arrow-key navigation moves only data-highlighted. The highlight therefore is the focus indicator for keyboard users (SC 2.4.7 Focus Visible), and at ~1:1 contrast keyboard users — especially low-vision users — cannot tell which option is active.

Same pattern in sibling components

Also note the active variant (text-highlighted before:bg-elevated, e.g. select.ts active.true) is solid bg-elevated — slate-100 on white ≈ 1.07:1 — though the selected state at least has the trailing check icon as a non-color indicator.

Suggested direction

A solid, sufficiently dark/light highlight — the classic pattern used by native selects and most design systems — passes comfortably, e.g. primary-600 with inverted text in light mode (white on indigo-600 ≈ 6.3:1; bg vs white menu ≈ 6.3:1) and primary-400 with inverted text in dark mode (slate-900 on indigo-400 ≈ 5.8:1). Alternatively keep it neutral but raise the tone well past elevated/50 and pair it with a visible non-background cue.

Workaround we ship today (app config / vite plugin ui option):

select: {
  variants: {
    active: {
      true: { item: 'data-highlighted:not-data-disabled:text-inverted data-highlighted:not-data-disabled:before:bg-(--ui-color-primary-600) dark:data-highlighted:not-data-disabled:before:bg-(--ui-color-primary-400)' },
      false: {
        item: 'data-highlighted:not-data-disabled:text-inverted data-highlighted:not-data-disabled:before:bg-(--ui-color-primary-600) dark:data-highlighted:not-data-disabled:before:bg-(--ui-color-primary-400)',
        itemLeadingIcon: 'group-data-highlighted:not-group-data-disabled:text-inverted'
      }
    }
  }
}

This came out of a WCAG audit of our app (the audit flagged the default USelect highlight at ~1:1) — happy to open a PR if you agree on the direction.

Additional context

Related in spirit to #1284 (default theme failing an accessibility audit), but a different component and criterion.

Logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageAwaiting initial review and prioritization

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions