Gesso
Data display

Health face

Phosphor's filled smiley with a mouth that springs between on track, at risk, and off track.

Health face is a small mark for the health of an initiative or project. It starts from Phosphor's Smiley at fill weight, and the mouth is one number (1 smile, 0 flat, -1 frown) on a spring, so any health retargets any other. A jump from on track to off track passes through flat on the way, which reads as the project sliding rather than swapping icons. It is inked with the system's success, warning, and danger colors, and it names itself as an image, so it can sit in a row on its own or under a Chip trigger as its mark.

Health On track

On track

Advance steps through the three, and a jump button skips straight across. Play cycle retargets every 700ms.

Usage

import { HealthFace } from '@/components/ui/health-face';

<HealthFace health="at_risk" />

The health values are on_track, at_risk, and off_track. To put the state in words beside the mark, wrap the mark so the state is read once, as described in Name it once.

import { HEALTH_LABELS, HealthFace } from '@/components/ui/health-face';

<span className="inline-flex items-center gap-1.5">
  <HealthFace health={health} label="" />
  {HEALTH_LABELS[health]}
</span>

HEALTH_LABELS holds the display text for each state and HEALTH_INK holds the color each state is drawn in, as a CSS variable.

Size follows the text

size defaults to 1em, so the mark matches the text beside it. Pass a number for pixels or a string for any other CSS length. Keep it at 16px or larger, where the mouth stroke is a full pixel, because below that the three mouths get hard to tell apart. In a sm chip, 1em is only 12px, so match the chip's icon scale with className="size-4", as the property band does.

The three states at 16, 24, and 40px. Mount jumps, so a list does not play in on first paint.

Health On trackHealth On trackHealth On track
On track
Health At riskHealth At riskHealth At risk
At risk
Health Off trackHealth Off trackHealth Off track
Off track

Name it once

The mark is an image named Health On track, Health At risk, or Health Off track. That name is right when the mark is the only signal in the row. When the state is also written next to it, a screen reader reads it twice, so pass label="" to hide the mark from assistive tech and let the text name the state. Pass your own label for a translated name.

On trackAt riskOff track

Inside a chip the same rule applies, and the fix is the trigger's aria-label, as described on Chip trigger. The ink is chosen for the mark, so keep the label in neutral text unless you have checked the ink as text on your surface.

A state the faces don't name

ink overrides the status color with any CSS color, for a state outside the three, like a stale reading. The mouth still follows health, so pick the face that fits, such as at_risk for the flat one, and pass a label that names the real state, because the default name says the health you passed.

<HealthFace health="at_risk" ink="var(--sys-fg-muted)" label="Health stale" />

A change of ink rides the same spring as a change of health, and under reduced motion it jumps.

Motion

One story: health to health on the shared spring (stiffness 420 / damping 40 / mass 0.9), the same one Priority bars and Work mark use.

RuleWhat happens
MouthEnds at x 88 and 168 on the 256 grid, a 16-unit round stroke knocked out of the face. The bow goes from +12 through flat to -12.
EyesPhosphor's, fixed.
ColorThe categorical green and amber, and --destructive, or ink when you pass one. Rides the same spring.
MountSprings start on the current value, with no play-in.
Reduced motionjump to the end state, and the color changes with no transition.
InterruptA new health retargets the same spring mid-flight.

Props

Health face takes only these props, and it forwards its ref to the <svg>.

PropTypeDefaultWhat it does
health"on_track" | "at_risk" | "off_track"Required. Sets the mouth, the ink, and the accessible name.
sizenumber | string"1em"The width and height. A number is pixels and a string is any CSS length.
inkstringAny CSS color. Replaces the status ink, for a state the three faces don't name. The mouth still follows health.
labelstringHealth {state}The accessible name and tooltip. "" hides the mark from assistive tech.
classNamestringAdded to the <svg>, so size-4 sets the size from a class.

Accessibility

  • The root is an <svg role="img"> whose accessible name is built from the state, like Health On track, and it carries the same text in a <title>, which browsers show as a tooltip on hover. label replaces it, for a translated name.
  • When text beside the mark already says the state, the image name repeats it, so pass label="", which makes the mark aria-hidden and drops the tooltip. A Chip trigger with a Health face as its mark and no aria-label is read as "Health On track On track", so give the trigger one that names the property and the value.
  • Shape carries the state as well as color: a smile, a flat mouth, and a frown. In forced colors the ink becomes CanvasText, ink included, and the mouth, which is knocked out of the face, still shows. A state drawn with ink shares its mouth with one of the three, so its label or the text beside it is what tells them apart. Below 16px the mouth stroke is under a pixel, and the states are harder to tell apart.
  • The mark is not focusable and has no press behavior. Put it inside a button for that, and the button's name then includes the image name.
  • It is not an interactive target, so it has no 44px minimum. Size the control around it.
  • Under reduced motion the mouth and the ink jump to the new state with no transition.

When to use something else

  • Badge for a status written as a word, in a role color. Pair Health face with Badge when the health needs both a shape and a label.
  • Category colors for a value whose color the user picks and that keeps its hue in every look, like a select option.
  • Priority bars for a priority and Work mark for where work stands, which are the other two marks drawn the same way.
  • Check mark for a plain completed check, not a three-way state.
  • Avatar for a person, since a face here means health and never identity.

Install

shadcn add @gesso/health-face

On this page