Skip to content

Custom Select dropdown + pointer cursor on interactable primitives #151

@NicolaasGrobler

Description

@NicolaasGrobler

Problem

Two related polish items surfaced while reviewing the Phase 1 design system primitives (#149, PR #150):

1. Native <select> dropdown ignores dark theme on Windows

The Select primitive I shipped in Phase 1 uses native <select> with our chrome on the trigger button. But the open dropdown menu is rendered by the OS, which on Windows always uses the system theme — white background, dark text — completely breaking the dark-theme aesthetic. Screenshot in the PR review showed the issue clearly: clean dark trigger, then a glaring light dropdown overlay.

This is also true on macOS to a lesser extent — the native menu adopts system theme, not app theme.

2. Interactive elements should show pointer cursor

Tailwind doesn't apply cursor: pointer to <button> by default (browser default is cursor: default for buttons in most engines). Currently buttons across QueryDen show the I-beam or default arrow cursor instead of the pointer, making interactivity ambiguous.

Fix

Custom Select dropdown

Replace the native <select> with a portal-rendered, theme-aware dropdown. Should:

  • Render in a portal so it isn't clipped by overflow:hidden parents
  • Match the existing Input chrome on the trigger
  • Keyboard navigation (Arrow Up/Down, Home/End, type-ahead, Enter, Esc)
  • Proper ARIA (combobox / listbox roles)
  • Open animation (fade + scale, ~120ms)
  • Selected state visible

Implementation: add @radix-ui/react-select and wrap it. Radix handles portal, keyboard nav, focus, animations, and a11y — building from scratch is ~200 lines that we'd get subtly wrong. ~12KB gzipped, tree-shakeable.

Pointer cursor on interactables

Add cursor-pointer to Button, IconButton, and any other clickable primitive in src/components/ui/. Disabled state already uses cursor-not-allowed.

For ad-hoc <button> elements elsewhere in the app, they'll get the cursor as a side effect of being migrated to the primitives in Phase 2 sweeps. Not adding a global button { cursor: pointer; } rule because that would also affect dragging handles, grid headers, and other clickable-but-not-clickable elements.

Scope

Both are landing in the existing Phase 1 PR (#150) since they're foundational primitives, not Phase 2 migration work.

Acceptance

  • Select dropdown renders with dark/light theme correctly
  • Select supports keyboard navigation
  • Hovering any <Button> or <IconButton> shows the pointer cursor
  • Reference page (#design-system) shows the new Select correctly in both themes

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilityAccessibility and a11y improvementsenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions