Interaction

How DrumKit components respond to being hovered, focused, pressed, and selected, and how those responses differ depending on whether someone is using a mouse, a finger, or a keyboard.

The three layers

DrumKit does not define its own interaction states. It inherits them: React Aria decides when a state is active and exposes it to styling; DrumKit decides what it looks like. This split is the most important thing on this page: the correct answer to what states a component has is never whichever ones we decided to draw; it is whatever React Aria provides for that component.

Layer
Owns
You change this
React Aria ComponentsBehavior: what counts as a hover, a press, a focusNo
Untitled UIDefault styling and component APIRarely
DrumKitTokens and visual treatmentYes

Rule

We style states, we don’t invent them. If the state you want isn’t in the vocabulary below, that’s a signal to reconsider the design, not to add a class.

Interaction states

Six states carry nearly all the weight in DrumKit. Design every interactive element against all six before it ships. Beyond these, individual components expose their own states (disabled, invalid, read-only, required, indeterminate, dragging, drop target, and others), with availability varying by component; the Styling section of each React Aria component’s documentation is the authoritative list.

Design

State
What it tells the user
How to design it
RestThis is here, and it’s interactiveLegible with no interaction at all; hover can’t be what reveals it
HoverYour pointer is on the targetSubtle. It confirms aim, nothing more
FocusThis element is receiving inputDistinct from the ring, like an active input border
Focus visibleYou are here, and you got here by keyboardUnmissable. A navigation aid, not decoration
PressedYour action registeredImmediate, for click, tap, and Enter/Space alike
SelectedThis is chosen, and stays chosenPersistent. Survives hover and focus without going ambiguous

Live from the kit: hover, tab through, press, and toggle to watch the states

Designing state combinations. States stack. An item can be selected and hovered and focus-visible at the same time. Design the combinations, not just the singles; the common failure is a selected item whose hover treatment makes it read as unselected.

Precedence in DrumKit:

  • Disabled overrides everything. A disabled element receives neither hover nor press. Do not design a disabled-hover treatment; it will never render.
  • Selected is the base for its own hover and press. Style hover-on-selected explicitly rather than letting the unselected hover treatment apply.
  • Focus visible composes, it doesn’t replace. Build the focus ring as an outline so it layers over whatever else is happening rather than competing with the background treatment.

Code

DrumKit styles states in two layers, and the split is the house convention.

Native Tailwind variants are the default. Because React Aria renders real elements, hover:, focus-visible:, and disabled: work directly; the kit uses them hundreds of times, and focus-visible: natively keeps rings off clicked buttons.

<Button className="
  bg-brand-solid text-white
  hover:bg-brand-solid_hover
  focus-visible:outline-2 focus-visible:outline-offset-2
  disabled:cursor-not-allowed disabled:opacity-50
">
  Save changes
</Button>

React Aria’s state variants fill the gaps CSS can’t express. The tailwindcss-react-aria-components plugin is installed (via @plugin in globals.css), so every React Aria state is available as a variant: selected:, pressed:, hovered:, indeterminate:, and the rest. Reach for them when there is no native equivalent (selection), when the native pseudo-class misbehaves (:active is inconsistent and misses keyboard activation; use pressed:), or when touch behavior matters (native :hover can stick after a tap; hovered: never applies on touch).

<ToggleButton className="
  bg-primary text-secondary
  hover:bg-primary_hover
  selected:bg-active selected:text-secondary_hover
  pressed:bg-primary_hover
">
  Bold
</ToggleButton>

Render props when one state drives several classes. Use them when one state needs to drive several classes at once, or when logic gets conditional.

<Radio className={({ isSelected, isFocusVisible }) => `
  flex rounded-lg p-4 ring-1
  ${isSelected ? "ring-brand bg-brand-primary_alt" : "ring-secondary"}
  ${isFocusVisible ? "outline-2 outline-offset-2" : ""}
`} />

The full state-by-state reference (which variant to write and which tokens it draws on) lives in Writing states below.

Input modality

The same component offers different states depending on how it’s being operated. This is the part most interaction documentation gets wrong, and it has direct design consequences.

Design

Modality
Hover
Focus ring
Press
Mouse / trackpad / penYesNoYes
TouchNoNoYes
KeyboardNoYesYes
Screen readerNoYesYes

Hover does not exist on touch

A large share of your users may never see a hover state, so anything that only appears on hover is invisible to them. Hover may confirm, refine, or reassure; it may not be the only path to a control, a label, or an explanation.

The focus ring is keyboard-only, by design

Mouse and touch suppress it; keyboard and screen reader show it. That is why clicking a button leaves no ring stuck on it and tabbing to it does. Never suppress it to “clean up” a design.

Press is the one universal

React Aria normalizes press across mouse, touch, keyboard, and virtual clicks, so the same state fires regardless of input. If you only have budget to make one state excellent, make it this one.

Touch target sizing lives on the Accessibility page.

Code

You do not need to detect modality yourself. React Aria resolves it before any state reaches your styles:

hovered:

Never applied on touch

There is no touch-hover to handle.
focus-visible:

Keyboard and screen reader only

focused: is the looser one: it applies on any focus, including a mouse click.
pressed:

Every input, one state

Mouse down, touch down, Enter/Space, and virtual clicks alike.

Writing states

The state-by-state reference: the variant you write and the tokens it draws on. Every state treatment resolves to a token: a raw Tailwind palette value (bg-blue-600) in a state variant is a bug; it won’t respond to theming and it won’t switch in dark mode. Cursors are part of the idiom too: cursor-pointer on interactive elements, cursor-not-allowed on disabled ones.

State
You write
Tokens it draws on
Restbase classesbg-primary · text-primary · border-primary families
Hoverhover: (hovered: on touch-critical surfaces)the _hover family: bg-primary_hover, bg-brand-solid_hover
Focus visiblefocus-visible:outline-2 + offset-2--color-focus-ring (brand-500)
Pressedpressed:reuses the _hover family
Selectedselected:bg-active + the _hover family
Invalidinvalid: / group-invalid:ring-error(_subtle) · focus-ring-error (rose-500)
Disableddisabled:opacity-* + cursor-not-allowed (no dedicated tokens)
  • Invalid pairs with the error message. Inputs show ring-error_subtle at rest and ring-error ring-2 while focused; the FieldError slot says what’s wrong and how to fix it, never color alone.
  • Disabled is not read-only. A disabled control is skipped by keyboards and screen readers and its text can’t be selected. If someone should still perceive and copy the value (they can’t change it), use readOnly, not isDisabled.

Checklist

Before a component is considered done:

  • All six core states designed, including rest
  • Interactivity is legible at rest, without hover
  • No affordance, label, or control is hover-only
  • Focus ring present via focus-visible:, never suppressed
  • Focus ring meets 3:1 contrast against adjacent colors
  • Press state fires for click, tap, and Enter/Space
  • Selected + hover and selected + focus-visible both designed
  • Disabled state does not rely on color alone
  • States use the house layers: native variants by default, React Aria variants (selected:, pressed:) where CSS can’t
  • Every state treatment resolves to a DrumKit token
  • Verified on a real touch device, not a simulator

Reference

The authoritative sources this page leans on.