Skip to content

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

@OzzyCzech

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

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions