# Indicator (/docs/indicator)





`Indicator` wraps another element (an [`Avatar`](/docs/avatar), an [`ActionIcon`](/docs/action-icon), a piece of text) and overlays a small dot or badge on one of its corners or edge midpoints. Hand-built — there's no headless machine to wrap here — and fully static: `position`, `offset`, `color`, `label`, and the `processing` pulse are all render-time props and CSS, with no JavaScript measurement or observer anywhere.

```tsx
import { Indicator } from '@42-central/ui-react/indicator';
import { Avatar } from '@42-central/ui-react/avatar';

<Indicator label={3} color="red">
  <Avatar name="Ada Lovelace" />
</Indicator>
```

## Playground [#playground]

<story.Playground />

### Full prop browser [#full-prop-browser]

<story.WithControl />

## Basic dot [#basic-dot]

With no `label`, `Indicator` renders a plain colored dot — a common status marker on an avatar or an icon button.

<Preview align="block" code="&#x22;<Indicator color=\&#x22;green\&#x22; offset={-6}>\n  <Avatar name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>\n<Indicator color=\&#x22;neutral\&#x22;>\n  <ActionIcon variant=\&#x22;light\&#x22; color=\&#x22;neutral\&#x22;><Wifi /></ActionIcon>\n</Indicator>&#x22;">
  <div className="flex flex-wrap items-center gap-6">
    <Indicator color="green" offset="-6">
      <Avatar name="Ada Lovelace" color="cyan" />
    </Indicator>

    <Indicator color="neutral">
      <ActionIcon variant="light" color="neutral">
        <Wifi />
      </ActionIcon>
    </Indicator>
  </div>
</Preview>

`Avatar` is circular, so its bounding-box corner — where `Indicator` anchors by default — sits in empty space just outside the visible circle. `ActionIcon` is square, so its corner sits right at the visible edge and needs no adjustment. See [Offset](#offset) below for the circular case.

## Positions [#positions]

`position` anchors the dot to any of the 9 cells of a corner/edge grid — logical `start`/`end` names, so they flip correctly under `dir="rtl"`. Default is `"top-end"`.

`Avatar` is circular, so every example below carries the same `offset={-6}` pulling the dot onto the circle's rim — see [Offset](#offset). It's a no-op on the `center`/`middle` axis, so it only affects the 4 true corners.

<Preview align="block" code="&#x22;<Indicator position=\&#x22;top-start\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;top-center\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;top-end\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;middle-start\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;middle-center\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;middle-end\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;bottom-start\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;bottom-center\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator position=\&#x22;bottom-end\&#x22; offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>&#x22;">
  <div className="grid grid-cols-3 gap-8 place-items-center">
    <Indicator position="top-start" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="top-center" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="top-end" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="middle-start" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="middle-center" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="middle-end" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="bottom-start" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="bottom-center" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator position="bottom-end" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>
  </div>
</Preview>

## Offset [#offset]

`position` anchors to a corner of `children`'s rectangular bounding box, not to whatever shape is actually visible inside it. For a square element like `ActionIcon`, that corner sits right at the visible edge, so the default look is already correct. For a circular element like `Avatar`, the box's corner is empty space outside the circle — the dot ends up floating just past the rim instead of sitting on it.

`offset` fixes this: a **negative** value (for both axes, or per-axis via `{ x, y }`) pulls the dot in from the corner, toward center, until it lands back on the circle's edge. A positive value pushes it further out instead — useful for the opposite case, nudging a dot away from a shape that already touches the box edge.

The right magnitude scales with the avatar's own size — there's no single constant that works everywhere, so treat this as a starting point and check it visually against whatever `size` you're using:

<Preview align="block" code="&#x22;{/* md Avatar (40px) */}\n<Indicator color=\&#x22;red\&#x22;>\n  <Avatar name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>\n<Indicator color=\&#x22;green\&#x22; offset={-6}>\n  <Avatar name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>\n\n{/* xl Avatar (64px) needs a bigger pull */}\n<Indicator color=\&#x22;red\&#x22; size=\&#x22;lg\&#x22;>\n  <Avatar size=\&#x22;xl\&#x22; name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>\n<Indicator color=\&#x22;green\&#x22; size=\&#x22;lg\&#x22; offset={-9}>\n  <Avatar size=\&#x22;xl\&#x22; name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>&#x22;">
  <div className="flex flex-wrap items-end gap-10">
    <div className="flex flex-col items-center gap-2">
      <Indicator color="red">
        <Avatar name="Ada Lovelace" color="cyan" />
      </Indicator>

      <span className="text-muted-foreground text-xs">
        No offset — floating
      </span>
    </div>

    <div className="flex flex-col items-center gap-2">
      <Indicator color="green" offset="-6">
        <Avatar name="Ada Lovelace" color="cyan" />
      </Indicator>

      <span className="text-muted-foreground text-xs">
        offset=

        {"{-6}"}

         — on the rim
      </span>
    </div>

    <div className="flex flex-col items-center gap-2">
      <Indicator color="red" size="lg">
        <Avatar size="xl" name="Ada Lovelace" color="cyan" />
      </Indicator>

      <span className="text-muted-foreground text-xs">
        No offset — floating
      </span>
    </div>

    <div className="flex flex-col items-center gap-2">
      <Indicator color="green" size="lg" offset="-9">
        <Avatar size="xl" name="Ada Lovelace" color="cyan" />
      </Indicator>

      <span className="text-muted-foreground text-xs">
        offset=

        {"{-9}"}

         — on the rim
      </span>
    </div>
  </div>
</Preview>

`offset={-6}` reads well for the default `md` `Avatar` (40px); a larger `xl` `Avatar` (64px) needs roughly `offset={-9}` to land in the same spot relative to the circle. As a rule of thumb, a corner dot on a circular child wants an inward offset around 15% of the child's diameter — `Indicator` doesn't measure `children`, so it can't compute this automatically, and this kit's `size` is shared vocabulary across many differently-shaped components (only some of which are circular), so there's no single size-keyed default that would be correct for all of them. Tune the number against whatever you're wrapping.

## Numeric label & maxValue [#numeric-label--maxvalue]

Pass `label` for a count badge instead of a plain dot. `maxValue` caps a numeric `label`'s display at `` `${maxValue}+` `` once it's exceeded — it's a no-op for a text `label`.

<Preview align="block" code="&#x22;<Indicator label={3} offset={-6} color=\&#x22;red\&#x22;><Avatar name=\&#x22;Grace Hopper\&#x22; color=\&#x22;teal\&#x22; /></Indicator>\n<Indicator label={150} maxValue={99} offset={-6} color=\&#x22;red\&#x22;><Avatar name=\&#x22;Zendaya\&#x22; color=\&#x22;purple\&#x22; /></Indicator>\n<Indicator label=\&#x22;New\&#x22; offset={-6} color=\&#x22;blue\&#x22;><Avatar name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>&#x22;">
  <div className="flex flex-wrap items-center gap-6">
    <Indicator label="3" offset="-6" color="red">
      <Avatar name="Grace Hopper" color="teal" />
    </Indicator>

    <Indicator label="150" maxValue="99" offset="-6" color="red">
      <Avatar name="Zendaya" color="purple" />
    </Indicator>

    <Indicator label="New" offset="-6" color="blue">
      <Avatar name="Ada Lovelace" color="cyan" />
    </Indicator>
  </div>
</Preview>

### showZero [#showzero]

A numeric `label` of `0` still renders by default — pass `showZero={false}` to hide the indicator (and only the indicator; `children` always render) when the count is zero.

<Preview align="block" code="&#x22;<Indicator label={0} offset={-6} color=\&#x22;red\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator label={0} showZero={false} offset={-6} color=\&#x22;red\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>&#x22;">
  <div className="flex flex-wrap items-center gap-6">
    <Indicator label="0" offset="-6" color="red">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator label="0" showZero="false" offset="-6" color="red">
      <Avatar name="A" color="cyan" />
    </Indicator>
  </div>
</Preview>

## Processing [#processing]

`processing` renders a second layer behind the dot doing a CSS-only `animate-ping` pulse — no `ResizeObserver`, no interval, nothing to tear down. It respects `prefers-reduced-motion` the same way [`Skeleton`](/docs/skeleton)'s own pulse does.

<Preview align="block" code="&#x22;<Indicator processing offset={-6} color=\&#x22;green\&#x22;>\n  <Avatar name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>&#x22;">
  <Indicator offset="-6" color="green">
    <Avatar name="Ada Lovelace" color="cyan" />
  </Indicator>
</Preview>

## Disabled [#disabled]

`disabled` hides the dot/badge entirely — `children` still render, so toggling it never reflows the surrounding layout.

<Preview align="block" code="&#x22;<Indicator disabled label={5} color=\&#x22;red\&#x22;>\n  <Avatar name=\&#x22;Ada Lovelace\&#x22; color=\&#x22;cyan\&#x22; />\n</Indicator>&#x22;">
  <Indicator label="5" color="red">
    <Avatar name="Ada Lovelace" color="cyan" />
  </Indicator>
</Preview>

## withBorder [#withborder]

Rings the dot in a page-surface color so it reads as cut into whatever it overlays — a light=white/dark=gray-dark-900 guess, since this kit has no dedicated "page background" token. Unlike [`Avatar.Group`](/docs/avatar)'s mask-based notch, `Indicator` has no same-size neighbor to cut a real transparent hole for, so it still falls back to a color guess here.

<Preview align="block" code="&#x22;<Indicator withBorder label={5} offset={-6} color=\&#x22;red\&#x22;>\n  <Avatar name=\&#x22;Grace Hopper\&#x22; color=\&#x22;teal\&#x22; />\n</Indicator>&#x22;">
  <Indicator label="5" offset="-6" color="red">
    <Avatar name="Grace Hopper" color="teal" />
  </Indicator>
</Preview>

The ring color here is a real CSS custom property, `--indicator-border-color`, so it can be overridden without fighting `dark:` specificity or forking the component. `Indicator` can't know what surface it's actually sitting on — the default page background, a colored `Card`, a tinted panel — so the shipped default is only ever a reasonable guess for a plain page. Retarget it for a different surface by setting the property, e.g.:

```tsx
<Indicator withBorder style={{ "--indicator-border-color": "var(--color-brand-50)" }}>
  <Avatar name="Grace Hopper" color="teal" />
</Indicator>
```

## Sizes, radius & colors [#sizes-radius--colors]

`size` scales the dot diameter (or the label badge's height) across the kit's shared `xs`–`xl` steps. `radius` only matters once `label` is set — a bare dot is always a circle — and controls how rounded a multi-digit count's corners are; it defaults to `"xl"`, close to a pill.

Each example below pairs its `Indicator` `size` with an `Avatar` of the matching size, and — per [Offset](#offset) — a bigger `offset` magnitude for a bigger circle so the dot keeps landing on the rim instead of drifting further off it as the avatar grows.

<Preview align="block" code="&#x22;<Indicator size=\&#x22;xs\&#x22; label={3} offset={-4} color=\&#x22;brand\&#x22;><Avatar size=\&#x22;xs\&#x22; name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator size=\&#x22;sm\&#x22; label={3} offset={-5} color=\&#x22;brand\&#x22;><Avatar size=\&#x22;sm\&#x22; name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator size=\&#x22;md\&#x22; label={3} offset={-6} color=\&#x22;brand\&#x22;><Avatar size=\&#x22;md\&#x22; name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator size=\&#x22;lg\&#x22; label={3} offset={-7} color=\&#x22;brand\&#x22;><Avatar size=\&#x22;lg\&#x22; name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator size=\&#x22;xl\&#x22; label={3} offset={-9} color=\&#x22;brand\&#x22;><Avatar size=\&#x22;xl\&#x22; name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>&#x22;">
  <div className="flex flex-wrap items-end gap-6">
    <Indicator size="xs" label="3" offset="-4" color="brand">
      <Avatar size="xs" name="A" color="cyan" />
    </Indicator>

    <Indicator size="sm" label="3" offset="-5" color="brand">
      <Avatar size="sm" name="A" color="cyan" />
    </Indicator>

    <Indicator size="md" label="3" offset="-6" color="brand">
      <Avatar size="md" name="A" color="cyan" />
    </Indicator>

    <Indicator size="lg" label="3" offset="-7" color="brand">
      <Avatar size="lg" name="A" color="cyan" />
    </Indicator>

    <Indicator size="xl" label="3" offset="-9" color="brand">
      <Avatar size="xl" name="A" color="cyan" />
    </Indicator>
  </div>
</Preview>

<Preview align="block" code="&#x22;<Indicator radius=\&#x22;xs\&#x22; label={128} offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator radius=\&#x22;md\&#x22; label={128} offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>\n<Indicator radius=\&#x22;xl\&#x22; label={128} offset={-6} color=\&#x22;brand\&#x22;><Avatar name=\&#x22;A\&#x22; color=\&#x22;cyan\&#x22; /></Indicator>&#x22;">
  <div className="flex flex-wrap items-center gap-6">
    <Indicator radius="xs" label="128" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator radius="md" label="128" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>

    <Indicator radius="xl" label="128" offset="-6" color="brand">
      <Avatar name="A" color="cyan" />
    </Indicator>
  </div>
</Preview>

Any palette color works — unset falls through to the neutral/brand default `--c-solid` cascades at `:root`, the same as [`Badge`](/docs/badge)'s `filled` variant.

## Inline [#inline]

`inline` renders the root as an `<span>` with `display: inline-block` instead of the default `<div>`/`block` — useful when wrapping a short inline run (a word, an icon) that shouldn't force its own line. The tag itself has to change, not just the CSS: a `<div>` is flow content and is never valid inside a paragraph, regardless of its display value.

<Preview align="block" code="`<span>Status: <Indicator inline color=&#x22;green&#x22; offset={2}>online</Indicator> right in the middle of a sentence.</span>`">
  <span className="text-sm">
    Status: 

    <Indicator color="green" offset="2">online</Indicator>

     right in the middle of a sentence.
  </span>
</Preview>

## API [#api]

<TypeTable
  type="{
  position: {
    type: &#x22;'top-start' | 'top-center' | 'top-end' | 'middle-start' | 'middle-center' | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'&#x22;,
    default: &#x22;'top-end'&#x22;,
    description: &#x22;Which corner (or edge midpoint) of children the dot anchors to, straddling it.&#x22;,
  },
  offset: {
    type: &#x22;number | { x: number; y: number }&#x22;,
    description:
      &#x22;Px nudge from the anchor point — positive pushes away, negative pulls in — one number for both axes, or independent x/y. Ignored on an axis whose position has no directional edge (the center/middle axis). A corner position anchors to children's rectangular bounding box, so a circular child (e.g. Avatar) usually needs a negative offset to pull the dot in from the empty box corner onto the visible circle's border — see the Offset section above.&#x22;,
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: &#x22;Dot diameter (no label) or badge height, off the kit's shared size scale.&#x22;,
  },
  color: {
    type: &#x22;'brand' | 'neutral' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'cyan' | 'blue' | 'purple' | 'pink' | (string & {})&#x22;,
    description: &#x22;Accent palette. Unset cascades the neutral/brand default.&#x22;,
  },
  label: {
    type: &#x22;ReactNode&#x22;,
    description: &#x22;Numeric or text content. Omitted renders a plain dot. `0` is still \&#x22;set\&#x22; — see showZero.&#x22;,
  },
  maxValue: {
    type: &#x22;number&#x22;,
    description: &#x22;Caps a numeric label's display at `${maxValue}+` once exceeded. No-ops on a non-numeric label.&#x22;,
  },
  showZero: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description: &#x22;false hides the indicator (not children) when label is the number 0.&#x22;,
  },
  processing: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Renders a second animate-ping pulse layer behind the dot.&#x22;,
  },
  disabled: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Hides the dot/badge entirely — children still render.&#x22;,
  },
  withBorder: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description:
      &#x22;Rings the dot in a page-surface color (the --indicator-border-color custom property, default light=white/dark=gray-dark-900) so it reads as cut into whatever it overlays. Override the property to match a non-default surface, e.g. a colored Card.&#x22;,
  },
  radius: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'xl'&#x22;,
    description: &#x22;Corner rounding of the label badge shape. Irrelevant to the bare dot (always a circle).&#x22;,
  },
  inline: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Root uses display: inline-block instead of the default block.&#x22;,
  },
  zIndex: {
    type: &#x22;number&#x22;,
    description: &#x22;Stacking order of the dot/badge overlay.&#x22;,
  },
  children: {
    type: &#x22;ReactNode&#x22;,
    description: &#x22;The element being decorated. Required.&#x22;,
  },
  testId: {
    type: &#x22;string&#x22;,
    description: &#x22;Renders as `data-testid` on the root wrapper. See [Testing](/docs/testing).&#x22;,
  },
}"
/>

All other props are forwarded to the root `<div>`.

No `asChild` — the root has to wrap two things at once, `children` **and** the absolutely-positioned dot sibling, which is structurally incompatible with `asChild`'s single-element merge. Same reasoning [`Skeleton`](/docs/skeleton) documents for its own wrap mode.

Unlike Mantine's own `Indicator`, there's no `autoContrast` prop: Mantine needs it because its `color` accepts a freeform hex value with no precomputed contrast. This kit's `Color` is a closed, named palette whose `--c-on-solid` / `--c-text` slot vars already precompute a readable foreground per token (see [Colors](/docs/colors)) — there's nothing for a runtime contrast pass to compute.

## Accessibility [#accessibility]

The dot/badge overlay renders `aria-hidden="true"` — it's a visual annotation on `children`, not independent content, so it never gets its own announcement. If a count or status needs to reach assistive tech, expose it separately (an `aria-label` on `children`, or a visually-hidden line of text) rather than relying on the indicator being read aloud.
