# Check Indicator

## Usage

Indicators are the selection pictures shared by a checkbox, a radio, a selector, and a Markdown task list. They are decorative. The owning control keeps the input, the role, the accessible name, the focus, and the keyboard. The indicator only turns state into a picture. That split is what makes them themeable: restyle one through its class, or replace the component by name.

```vue
<script setup lang="ts">
import { CheckIndicator } from '@astryx-vue/core/Indicator'
</script>

<template>
  <CheckIndicator state="checked" />
</template>
```

This is the mark on a chosen option. Unchecked, and with nothing in the slot, it renders nothing, so a list shows no empty box beside every row. It has no chrome of its own. It is the glyph, so a class passed by the host lands on the same element as the icon.

Map this name to [Radio Indicator](/en/components/radio-indicator) when every single-selection mark should be a circle, including the empty circle on an unchosen row. The box is [Checkbox Indicator](/en/components/checkbox-indicator).

## Anatomy

| Element | Description |
| --- | --- |
| Chrome (checkbox, radio) | Required on the box and the circle, which draw in every state. The check indicator is the state mark itself and has no chrome of its own. |
| State mark | The checkmark shown when the option is chosen. Unchecked draws nothing unless the slot has something renderable. |

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Restyle the stable class first. Replacing the component is the heavier path, for when the shape itself is wrong. |
| Do | Decide whether slot content replaces the mark with isRenderable, not a null check. The helper is shallow: it excludes null, undefined, booleans, and the empty string. An element still counts when its descendants later render nothing. A host writes a spinner with v-if="busy". When that condition is false, a null check deletes the chosen check. |
| Do | A replacement must set aria-hidden. The owning control provides the role and the accessible name. A visible indicator would be announced twice. |
| Do | Use theme tokens for every color, radius, and border width in a replacement. |
| Do | Render a single root element, and let it keep the border radius the focus ring should follow. The control paints that ring onto the indicator when the real input is visually hidden. Do not draw a second ring. |
| Don't | Pass hover or pressed state in as props. Hover reaches the graphic through the ancestor class astryx-indicator-scope. |
| Don't | Assume the graphic is mounted only when it is selected. The host renders it in every state and passes state. That is what lets a replacement draw an empty circle on an unchosen row. |

## Examples

### States

Unchecked reserves no box. Checked draws the glyph at both sizes. Disabled uses the disabled icon color.

```vue
<script setup lang="ts">
import { CheckIndicator } from '@astryx-vue/core/Indicator'
</script>

<template>
  <div class="indicator-board">
    <div v-for="size in (['sm', 'md'] as const)" :key="size" class="indicator-board-row">
      <span class="checkbox-indicator-preview-size">{{ size }}</span>
      <span class="indicator-board-item">
        <CheckIndicator state="unchecked" :size="size" />
        <span>Unchecked</span>
      </span>
      <span class="indicator-board-item">
        <CheckIndicator state="checked" :size="size" />
        <span>Checked</span>
      </span>
      <span class="indicator-board-item">
        <CheckIndicator state="checked" :size="size" is-disabled />
        <span>Disabled</span>
      </span>
    </div>
  </div>
</template>
```

### Replace by name

The same row of marks. The left card is the default check. Inside the theme on the right, that name draws a circle, including the empty circle when nothing is chosen.

```vue
<script setup lang="ts">
import type { IndicatorState } from '@astryx-vue/core/Indicator'
import { RadioIndicator, useIndicator } from '@astryx-vue/core/Indicator'
import { defineTheme, Theme } from '@astryx-vue/core/Theme'
import { defineComponent, h } from 'vue'

const radioMarks = defineTheme({
  name: 'radio-marks',
  indicators: { check: RadioIndicator },
})

const states = ['unchecked', 'checked'] as const satisfies readonly IndicatorState<'singleSelection'>[]

const Marks = defineComponent({
  name: 'IndicatorMarkRow',
  setup() {
    const Mark = useIndicator('check')
    return () => h('div', { class: 'indicator-board-row' }, states.map(state =>
      h(Mark.value, { state, size: 'md' }),
    ))
  },
})
</script>

<template>
  <div class="indicator-replace">
    <div class="indicator-replace-card">
      <Marks />
      <span>Default check</span>
    </div>
    <Theme :theme="radioMarks">
      <div class="indicator-replace-card">
        <Marks />
        <span>Radio instead</span>
      </div>
    </Theme>
  </div>
</template>
```

### Replace the mark while loading

A spinner in the slot holds the check's place whether or not the row is chosen. Turning it off removes that place when the row is unchosen.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { CheckboxIndicator } from '@astryx-vue/core/CheckboxIndicator'
import { CheckIndicator } from '@astryx-vue/core/Indicator'
import { ref } from 'vue'

const busy = ref(true)
</script>

<template>
  <div class="indicator-board">
    <div class="indicator-board-row">
      <span class="indicator-board-item">
        <CheckboxIndicator state="checked">
          <span v-if="busy" class="indicator-spinner" />
        </CheckboxIndicator>
        <span>Checkbox</span>
      </span>
      <span class="indicator-board-item">
        <CheckIndicator state="unchecked">
          <span v-if="busy" class="indicator-spinner" />
        </CheckIndicator>
        <span>Check</span>
      </span>
      <Button size="sm" variant="secondary" :label="busy ? 'Show marks' : 'Show spinner'" @click="busy = !busy" />
    </div>
  </div>
</template>
```

## Props

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `state` | `'unchecked' \| 'checked'` | — | Required. The default draws nothing when unchecked. A replacement may draw in both states. |
| `size` | `'sm' \| 'md'` | `'md'` | Recorded on the element. The glyph itself is 1rem at both sizes, matching the small icon. |
| `isDisabled` | `boolean` | false | Visual only. The chosen glyph uses the disabled icon color. The owning row keeps the disabled semantics. |

## Slots

- The default slot replaces the mark in every state, chosen or not. A host passes a pending spinner through it.
- When the slot has something renderable, a 1rem box holds the place.
- With nothing renderable, an unchosen check renders nothing. Use isRenderable. A false condition must not delete a chosen check.
- Slot content is decorative. Do not put a focusable control in it.

## Accessibility

- The root is `aria-hidden="true"` when the check is drawn or the slot occupies its place.
- The component sets no interactive role and provides no click or keyboard behavior.
- A passed role, tab order, content editable flag, or attribute whose name starts with aria is dropped.
- The exposed element is empty when the mark is unchosen and the slot is empty.

## Theme

There is no chrome class. A chosen check is the icon. A slot is `.astryx-check-indicator`, 1rem square.

- Checked uses the accent icon color. Disabled uses the disabled icon color.
- `data-size` is sm or md.
- `data-disabled` is set when the row is disabled.
