Skip to content
design
Foundations

Cursors

The mouse cursor reflects what an element allows, from a steady arrow on the logo to a pointing hand on an action.

On this page

Identity · default arrow

The mark, name, tagline and version keep the same cursor.

Action · pointing hand

Select or edit this name. Changes stay in this example.

Unavailable action

This example does not save changes.

Hover each element, open the details and edit the field. The cursor describes the available interaction.
Code
import { Box } from "@bosunkit/components/box";
import { Button } from "@bosunkit/components/button";
import { FormField } from "@bosunkit/components/form-field";
import { Input } from "@bosunkit/components/input";
import { Logo } from "@bosunkit/components/logo";
import { Stack } from "@bosunkit/components/stack";
import { Text } from "@bosunkit/components/text";
import { useId, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Hover the logo, open the details and edit the field. The cursor follows what each element allows.";

export default function Example(): ReactElement {
  const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  const detailsId: string = useId();
  const unavailableId: string = useId();

  return (
    <div role="group" aria-label="Cursor examples" style={{ width: "100%" }}>
      <Stack space="lg">
        <Box background="surface2" padding="lg" radius="md">
          <Stack space="md">
            <Text variant="label">Identity · default arrow</Text>
            <Logo size="md" version="26.10.2" />
            <Text variant="caption" color="text-dim">
              The mark, name, tagline and version keep the same cursor.
            </Text>
          </Stack>
        </Box>
        <Box background="surface2" padding="lg" radius="md">
          <Stack space="md">
            <Text variant="label">Action · pointing hand</Text>
            <div>
              <Button
                textCase="none"
                motion={false}
                aria-expanded={open}
                aria-controls={detailsId}
                onClick={(): void => {
                  setOpen(!open);
                }}
              >
                {open ? "Hide details" : "Show details"}
              </Button>
            </div>
            <div id={detailsId} hidden={!open}>
              <Text>The button opens and closes these details with a mouse, touch, Enter or Space.</Text>
            </div>
          </Stack>
        </Box>
        <Box background="surface2" padding="lg" radius="md">
          <FormField label="Editable text · I-beam" hint="Select or edit this name. Changes stay in this example.">
            <Input defaultValue="Windgrazer" />
          </FormField>
        </Box>
        <Box background="surface2" padding="lg" radius="md">
          <Stack space="md">
            <Text variant="label">Unavailable action</Text>
            <div>
              <Button disabled textCase="none" aria-describedby={unavailableId}>
                Save changes
              </Button>
            </div>
            <div id={unavailableId}>
              <Text variant="caption" color="text-dim">
                This example does not save changes.
              </Text>
            </div>
          </Stack>
        </Box>
      </Stack>
    </div>
  );
}
01Match the behaviorA pointing hand promises an action. A resize cursor promises a working handle. Identity and decoration keep the default arrow.
02Keep one target consistentA control keeps the same cursor across its icon, label and padding. The logo keeps its arrow across the mark, name, tagline and version.
03Show the meaning without hoverLabels, focus and visible state explain what can happen. A cursor adds feedback for a mouse; keyboard and touch users need the same actions.

Choosing a cursor

Cursor behavior is part of interaction feedback. An affordance is a clue to what someone can do; the cursor is one such clue. In Bosun, enabled buttons and links use a pointing hand, and the logo uses the default arrow.

Use the browser's standard cursor keywords. Their shapes follow the operating system, including the user's cursor preferences. auto lets the browser choose; it can become an I-beam over selectable text. default explicitly requests the platform's usual arrow. CSS cursor definitions

Element or stateCursorRule
Logo, decorative mark, empty surfacedefaultNo action is implied. The logo's text shares the mark's arrow.
Enabled button, link, menu item, selectable optionpointerThe whole target performs its named action.
Editable or selectable texttext or native autoKeep text selection available, including text that can be copied but not edited.
Disabled controlNative behavior or not-allowedExplain why it is unavailable; the cursor alone does not disable it.
Pane dividercol-resize or row-resizeMatch the direction the divider can move.
Resize edge or cornerThe matching *-resize keywordPut it on a working handle, within its actual hit area.
Drag handlegrab, then grabbing during a dragProvide a way to complete the action without dragging.
Help targethelpHelp must be available from that target.
Chart inspection or drawing surfacecrosshairThe pointer marks a point on the drawing.
Lane or grid that takes a new entrycellPressing an empty part marks a slot.

These are usage rules for controls that provide each interaction, not a requirement to add dragging, help or resizing to every component. See the working pane divider for resizing.

The logo stays an arrow

The logo identifies the product. Its mark, name, tagline and version use cursor: default, so moving across them does not turn the cursor into a text caret or a pointing hand. Text selection does not need to be disabled to achieve this.

Use a separate, labeled navigation link when navigation is needed. A logo does not become a home link merely because it appears in a header.

Do
.logo { cursor: default; }
Scope the arrow to the logo. Its children inherit the same cursor.
Don't
* { cursor: default; }
Do not force an arrow on every element. Inputs, links and handles need their own feedback.

Disabled and pending controls

Use the component's disabled state to prevent an action. cursor: not-allowed and pointer-events: none are not substitutes for disabled semantics or keyboard behavior. Put the reason next to the control, where it can be read without hovering.

The kit's disabled Button ignores pointer events, so the visible cursor comes from the element beneath it. A disabled Input uses not-allowed. Preserve these component behaviors; do not add a wrapper just to force a prohibited cursor.

For work in progress, show pending text or the component's pending state. progress means other interaction remains available; wait means the affected area is unavailable while work finishes. Keep the status visible, and do not give the whole page a wait cursor for local work. CSS status cursors

Implementing and checking

Keep cursor rules in the shared component stylesheet. Consumers use the component's behavior across all themes; the cursor keywords do not need theme tokens. Use native cursors without custom images, trails or hidden pointers.

Check the entire target: its icon, text and padding should agree. Test disabled and pending states as well as the enabled state. Preserve native text selection in prose and fields; a global cursor rule can hide that feedback.

Use links for navigation and buttons for actions, with visible keyboard focus. Keep instructions and disabled reasons visible on touch screens. Every action must work from the keyboard, regardless of its cursor. WCAG 2.2 SC 2.1.1 Keyboard

  • Logo: the mark and text with their shared default cursor.
  • Button: enabled and disabled actions.
  • Panes: working resize handles and keyboard controls.
  • Accessibility: focus, touch targets and keyboard operation.