Interaction

How SquareKit 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

SquareKit does not define its own interaction states. It inherits them: React Aria decides when a state is active and exposes it to styling; SquareKit 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
SquareKitDefault styling and component APIRarely
SquareKitTokens 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 SquareKit. 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 SquareKit:

  • 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

SquareKit 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.

Cursor decisions

A cursor confirms what a pointer can do at this moment. It supplements visible affordance and never replaces labels, state, or keyboard behavior.

Pointer means an action will run

Use cursor-pointer for controls and links that perform an action. Do not use it as decoration or as the only sign that something is interactive.

Default means no pointer action

Keep the default cursor on noninteractive text and surfaces, including a current item when activating it again does nothing.

Text means text can be edited or selected

Use the text cursor for editable text and text-selection surfaces, not for the surrounding control.

Not allowed means blocked

Use cursor-not-allowed for disabled or blocked controls. Do not remove pointer events when the control must still expose a hover explanation.

Grab means real dragging

Use grab and grabbing only on actual drag affordances, and provide a keyboard or menu alternative to dragging.

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
  • Cursor communicates the available pointer action and is not the only affordance
  • States use the house layers: native variants by default, React Aria variants (selected:, pressed:) where CSS can’t
  • Every state treatment resolves to a SquareKit token
  • Verified on a real touch device, not a simulator

Reference

The authoritative sources this page leans on.