Skip to content
design
Foundations

Interaction states

Shared state rules keep controls understandable with a pointer, keyboard or touch in every theme.

On this page

The example is ready to save.

You can focus and copy this value; editing is unavailable.

Use the keyboard and pointer, select Compact view, and toggle unavailable or pending. These are the shared kit controls.
Code
import { useId, useState, type Dispatch, type SetStateAction, type ReactElement } from "react";
import { Check } from "lucide-react";
import { Button } from "@bosunkit/components/button";
import { Input } from "@bosunkit/components/input";
import { Label } from "@bosunkit/components/label";
import { Stack } from "@bosunkit/components/stack";
import { Inline } from "@bosunkit/components/inline";
import { Switch } from "@bosunkit/components/switch";
import { Text } from "@bosunkit/components/text";

export const description: string =
  "Tab through real controls, toggle a selection, and make an action unavailable or pending.";
export default function Example(): ReactElement {
  const id: string = useId();
  const [selected, setSelected]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  const [unavailable, setUnavailable]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  const [pending, setPending]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  const [saved, setSaved]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  return (
    <Stack space="lg">
      <Inline>
        <Switch id={`${id}-disabled`} checked={unavailable} onCheckedChange={setUnavailable} />
        <Label htmlFor={`${id}-disabled`}>Make action unavailable</Label>
      </Inline>
      <Inline>
        <Switch id={`${id}-pending`} checked={pending} onCheckedChange={setPending} />
        <Label htmlFor={`${id}-pending`}>Show pending state</Label>
      </Inline>
      <Inline>
        <Button
          textCase="none"
          aria-pressed={selected}
          variant={selected ? "primary" : "default"}
          onClick={(): void => {
            setSelected(!selected);
          }}
        >
          {selected ? <Check aria-hidden /> : null} Compact view
        </Button>
        <Button
          textCase="none"
          disabled={unavailable || pending}
          aria-busy={pending}
          onClick={(): void => {
            setSaved(true);
          }}
        >
          {pending ? "Saving…" : "Save example"}
        </Button>
      </Inline>
      <div role="status">
        <Text>
          {pending
            ? "Example operation in progress. Turn off the pending switch to finish."
            : unavailable
              ? "Saving is unavailable while this example is locked."
              : saved
                ? "Example saved locally."
                : "The example is ready to save."}
        </Text>
      </div>
      <Stack space="xs">
        <Label htmlFor={`${id}-readonly`}>Read-only value</Label>
        <Input id={`${id}-readonly`} value="Vessel notes" readOnly />
        <Text variant="caption" color="text-dim">
          You can focus and copy this value; editing is unavailable.
        </Text>
      </Stack>
    </Stack>
  );
}
01State describes behaviorDisabled blocks activation, read-only permits inspection, and pending reports unfinished work. Color alone cannot implement any of them.
02Selection and focus coexistSelection shows a choice; focus shows where the next keyboard action goes. A selected item keeps its indicator when it gains focus.
03Every input method worksActions remain visible without hover. Keyboard focus, labels and target sizes provide the same access on a nav station or touch display.

State meanings

StateMeaningRequired behavior
EnabledAvailable to useA label, appropriate role and keyboard activation.
HoverPointer over a targetTemporary feedback; no action or essential instruction exists only here.
PressedA pointer or activation key is heldTemporary feedback; release or cancellation ends it.
Selected / activatedA choice persistsA check, indicator or shape plus the appropriate aria-selected, aria-pressed or aria-checked.
FocusedReceives keyboard inputA visible focus ring that preserves the underlying selected or error state.
DisabledActivation is unavailableNative disabled behavior where possible, a reason nearby, and no save or navigation.
Read-onlyA value cannot be changedKeep inspection, focus and copying available; do not dim it as disabled.
PendingAn operation has not finishedVisible progress text and busy semantics; prevent duplicate submissions when needed. A pending control the crew will press again, such as ACK, stays focusable and marked busy and ignores further presses.
InvalidInput needs correctionAssociated explanatory text and invalid semantics; neutral validation styling, not an alert priority.

hover, pressed, activated, focused and disabled have palette tokens on the Control styles page. Read-only, pending and invalid are behavior and message states: they also need semantics and text, so they cannot be reduced to another fill color.

State precedence

  1. Disabled prevents activation and suppresses hover and press feedback. A selected disabled item retains its choice indicator.
  2. Pending describes work in progress. It does not mean success and must not silently erase an error or selection.
  3. Selection persists while hovered or pressed; use the selected palette’s matching state.
  4. Keyboard focus adds a ring around the current visual state. It does not replace the selection or invalid indication.
  5. Hover and press end when the input gesture ends. They do not persist as a saved choice.

Atlassian’s color guidance makes the same distinction between persistent selection, temporary interaction and additive focus. Bosun keeps its own palette and alert conventions.

Keyboard and touch

Use native controls or the shared component primitives. Tab moves between targets; arrow keys move within radio, list and menu patterns; Enter or Space activates the control according to its role. Escape dismisses an overlay and returns focus to its trigger. A disabled button is unavailable to keyboard activation as well as pointer input.

Touch has no dependable hover state. Keep labels, selected indicators and unavailable reasons visible. Match the spacing and touch-target rules, and do not shrink a target because its icon is small. Touch-sized targets apply on a screen up to 68.75em or under a coarse pointer; a coarse pointer changes a target's size, never a layout.

Feedback without ambiguity

Do

Save is unavailable until a profile name is entered.

Explain an unavailable action where everyone can read the reason.
Don't

Save

A faded action with no reason leaves the user guessing.

Use Inline message or Section message for validation and operation results. Reserve alert priorities for bridge alerts. A destructive action remains neutral; its explicit label and confirmation carry the meaning.

Motion may explain a transition, but values and safety results do not animate toward an assumed outcome. Follow Motion and keep the selected state visible with reduced motion or forced colors.

Verification

Check default, hovered, pressed, selected, selected-and-focused, disabled, selected-and-disabled, pending, read-only and invalid combinations where the component supports them. Repeat in Night, Dusk, Day and Bright; check a themed overlay as well as its trigger. Test keyboard activation, touch targets and focus return, not just screenshots. A token’s presence does not prove a component implements every state correctly.

  • Control styles: token families and live palette swatches.
  • Cursors: pointer feedback for the available interaction.
  • Accessibility: contrast, target size and keyboard access.
  • Themes: theme scopes and palette verification.