EXEPERTAI LAB

Research alpha

Time Machine
EXEPERTAI LAB
GALLERY / COLLECTION
← Browse Astryx gallery
Indicator·component·@astryxdesign/core

Indicator

Decorative control visuals: the mark on a chosen option, the box a checkbox draws, the circle a radio draws. Rendered by Selector, CheckboxInput, RadioList, and menu selection rows. Replace one by name through defineTheme({indicators}) and every component drawing it follows.

@astryxdesign/core/Indicator

Authored source examples

Source examples recorded in the documentation for this entry.

Restyle an indicator (the common path)
// Indicators render the same stable class targets wherever they appear, so
// one component override reaches the form control, the menu row, and any
// selection slot themed to use it. No indicator-specific API needed.
defineTheme({
  name: 'brand',
  components: {
    'checkbox-indicator': {
      base: {borderRadius: 'var(--radius-full)', borderWidth: '2px'},
      checked: {
        backgroundColor: 'var(--color-accent)',
        borderColor: 'var(--color-accent)',
      },
      'checked+disabled': {backgroundColor: 'var(--color-background-muted)'},
    },
    'radio-indicator': {base: {borderWidth: '2px'}},
    'radio-indicator-dot': {base: {borderRadius: '2px'}},
  },
});
Replace an indicator with your own component
// When the shape itself is wrong, hand the theme a component. It receives
// {state, size, isDisabled, children} and nothing else.
//
// Use theme tokens, never raw values — run `npx astryx docs tokens` for the
// full set. Color: --color-accent, --color-on-accent, --color-border,
// --color-border-emphasized, --color-background-surface,
// --color-background-muted. Radius: --radius-inner, --radius-full.
// Border width: --border-width.
import {isRenderable} from '@astryxdesign/core/utils';

function BrandCheckbox({state, size = 'md', isDisabled, children}) {
  return (
    <span
      aria-hidden="true"
      style={{
        width: size === 'sm' ? 20 : 24,
        height: size === 'sm' ? 20 : 24,
        borderRadius: 'var(--radius-inner)',
        border: 'var(--border-width) solid var(--color-border-emphasized)',
        color: 'var(--color-accent)',
        opacity: isDisabled ? 0.5 : 1,
      }}>
      {/* children first: the owner passes a loading Spinner through it.
          isRenderable, NOT `children ??` — a host writes
          children={isBusy && <Spinner/>}, and `false` is neither null nor
          caught by ??, so a nullish check takes the children branch, renders
          nothing in it, and deletes your mark on every chosen row. */}
      {isRenderable(children)
        ? children
        : state === 'checked' && <StarGlyph />}
    </span>
  );
}

defineTheme({name: 'brand', indicators: {checkbox: BrandCheckbox}});
Use radio visuals for single selection
// Replacement is by NAME, so one entry reaches every component that draws
// that indicator. Here every option in a Selector listbox draws a radio —
// including the unselected ones, which a check mark cannot do.
import {RadioIndicator} from '@astryxdesign/core/Indicator';

defineTheme({name: 'brand', indicators: {check: RadioIndicator}});

Usage

Indicators are the componentized selection visuals shared by CheckboxInput, RadioList, and menu selection rows. They are decorative: the owning component keeps the input, role, accessible name, focus, and keyboard behavior, while the indicator turns state into a picture. That split is what makes them themeable: restyle one through its class targets, or replace the component outright.

  • Reach for the canonical components['checkbox-indicator'] override first. Replacing the component is the heavier path, for when the shape itself is wrong.
  • Match the shipped replacement-content branch with isRenderable(children), not children != null or children ?? mark. The helper is deliberately shallow: it excludes nullish values, booleans, and the empty string, while React elements and containers take the replacement path even when their descendants render nothing. The owning control passes children={isBusy && <Spinner/>}, so a nullish check deletes the state mark whenever that value is false (#4893).
  • A replacement must set aria-hidden. The owning control provides the role and accessible name; a visible indicator would be announced twice.
  • Use theme tokens for every color, radius, and border width in a replacement. Run npx astryx docs tokens for the set.
  • Render a single root ELEMENT, and let it keep the border-radius you want the focus ring to follow. A control whose real input is visually hidden cannot show focus on that input, so the owner paints the standard ring onto the indicator element itself at focus time (useIndicatorFocusRing), and outline then picks up that element's radius. A replacement needs no cooperation and can forget nothing: the ring is never missing (WCAG 2.4.7), it is only the wrong shape if the root has no radius of its own. Do not draw a focus ring yourself; the owner already did.
  • Thread hover or pressed state in as props. Interaction state reaches an indicator through the owner's CSS ancestor marker, so hovering the row tints the control with no props involved.
  • Assume you are only mounted when selected. The host renders its indicator unconditionally and passes state, in every state; that is what lets a replacement draw where the default draws nothing (a radio's empty circle on an unchosen row). Drawing nothing in a state is a decision the indicator makes, not one the host makes for it.

Typed props

CheckboxIndicator

PropType and behavior
state'unchecked' | 'checked' | 'indeterminate' · required
Which state to draw. An indicator draws in EVERY state: the unchecked box is an empty box, not nothing.
size'sm' | 'md' · default 'md'
Control size: 20px or 24px.
isDisabledboolean · default false
Whether the owning control is disabled. Purely visual; the owner keeps the real disabled semantics.
childrenReactNode
Rendered inside the chrome INSTEAD of the state mark when the shallow isRenderable(children) check accepts the value. The helper excludes null, undefined, booleans, and the empty string; React elements and containers take the replacement path even when their descendants render nothing. CheckboxInput passes its loading Spinner through as children={isBusy && <Spinner/>}, so replacements should use the same helper rather than a nullish check.

CheckIndicator

PropType and behavior
state'unchecked' | 'checked' · required
Which state to draw. The default renders nothing when unchecked; a replacement may draw in both states.
size'sm' | 'md' · default 'md'
Control size, matching the other indicators.
isDisabledboolean · default false
Whether the owning row is disabled. Purely visual; the owner keeps the real disabled semantics.
childrenReactNode
Rendered INSTEAD of the mark, in every state: a host showing a pending Spinner passes it through whether or not the row is chosen.

RadioIndicator

PropType and behavior
state'unchecked' | 'checked' · required
Which state to draw. Radio belongs to the singleSelection family, which has no partial state.
size'sm' | 'md' · default 'md'
Control size: 20px or 24px.
isDisabledboolean · default false
Whether the owning control is disabled. Purely visual; the owner keeps the real disabled semantics.
childrenReactNode
Rendered inside the chrome INSTEAD of the state mark.

Anatomy

Chrome · required

The persistent box or circle, present in every state. Carries the astryx-checkbox-indicator / astryx-radio-indicator theme target (the pre-indicator astryx-checkbox / astryx-radio names are still emitted on the same element).

State mark · optional

The checkmark, indeterminate bar, or radio dot shown inside the chrome for the current state.

Theming

Targets

astryx-checkbox-indicator

Visual props: size

States: checked, disabled

astryx-checkbox-indicator-check

Visual props: size

astryx-checkbox-indicator-dash

Visual props: size

astryx-radio-indicator

Visual props: size

States: checked, disabled

astryx-radio-indicator-dot

Visual props: size

astryx-checkbox

Visual props: size

States: checked, disabled

Deprecated; use checkbox-indicator.

astryx-radio

Visual props: size

States: checked, disabled

Deprecated; use radio-indicator.

astryx-radio-dot

Visual props: size

Deprecated; use radio-indicator-dot.