|Kit

Indicator

Small dot or numeric/text badge overlaid on a corner or edge midpoint of another element — an unread-count bubble, a status dot, a processing pulse.

Indicator wraps another element (an Avatar, an ActionIcon, 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.

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

Component

Loading preview

Props

Code

<Component
  size="md"
  color="green"
  radius="xl"
/>

Full prop browser

Basic dot

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

AL

Code

<Indicator color="green" offset={-6}>
  <Avatar name="Ada Lovelace" color="cyan" />
</Indicator>
<Indicator color="neutral">
  <ActionIcon variant="light" color="neutral"><Wifi /></ActionIcon>
</Indicator>

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 below for the circular case.

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. It's a no-op on the center/middle axis, so it only affects the 4 true corners.

A
A
A
A
A
A
A
A
A

Code

<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>

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:

AL
No offset — floating
AL
offset={-6} — on the rim
AL
No offset — floating
AL
offset={-9} — on the rim

Code

{/* md Avatar (40px) */}
<Indicator color="red">
  <Avatar name="Ada Lovelace" color="cyan" />
</Indicator>
<Indicator color="green" offset={-6}>
  <Avatar name="Ada Lovelace" color="cyan" />
</Indicator>

{/* xl Avatar (64px) needs a bigger pull */}
<Indicator color="red" size="lg">
  <Avatar size="xl" name="Ada Lovelace" color="cyan" />
</Indicator>
<Indicator color="green" size="lg" offset={-9}>
  <Avatar size="xl" name="Ada Lovelace" color="cyan" />
</Indicator>

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

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.

GH
ZE
AL

Code

<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>

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.

A
A

Code

<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>

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's own pulse does.

AL

Code

<Indicator processing offset={-6} color="green">
  <Avatar name="Ada Lovelace" color="cyan" />
</Indicator>

Disabled

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

AL

Code

<Indicator disabled label={5} color="red">
  <Avatar name="Ada Lovelace" color="cyan" />
</Indicator>

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'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.

GH

Code

<Indicator withBorder label={5} offset={-6} color="red">
  <Avatar name="Grace Hopper" color="teal" />
</Indicator>

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.:

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

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 — 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.

A
A
A
A
A

Code

<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>
A
A
A

Code

<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>

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

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.

Status: online right in the middle of a sentence.

Code

<span>Status: <Indicator inline color="green" offset={2}>online</Indicator> right in the middle of a sentence.</span>

API

Prop

Type

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 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) — there's nothing for a runtime contrast pass to compute.

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.

Changelog

v0.8.0
  • New Indicator component

    #118

On this page