status-indicator: Animated Status Picker Web Component for Workflow Steps

Category: Animation , Javascript | July 30, 2026
Authorzanwei
Last UpdateJuly 30, 2026
LicenseMIT
Views15 views
status-indicator: Animated Status Picker Web Component for Workflow Steps

status-indicator is a Vanilla JS Web Component that turns a workflow state list into a compact row of selectable dots.

The active dot expands into a labeled pill, while clicks, pointer scrubbing, and keyboard controls update the current state.

Features:

  • Five default workflow states in a compact dot row.
  • Interruptible spring motion from dot to labeled pill.
  • Click, drag, touch, and keyboard selection.
  • Radiogroup semantics with roving keyboard focus.
  • Custom labels, colors, icons, and spinning states.
  • Live theming through six CSS custom properties.
  • Reduced-motion behavior for state changes and icons.
  • Dependency-free ES module with no build step.

Use Cases:

  • Place it beside an issue title to switch between backlog, active work, review, completion, and cancellation.
  • Approval queues gain a compact selector that fits beside assignees, due dates, and action menus.
  • Custom labels and icons map deployment stages such as queued, building, testing, released, and rolled back.
  • In support dashboards, the change payload feeds ticket-state updates and hidden form fields.

How To Use It:

Installation

Download status-indicator.js, place it in your project, and load it as an ES module. Module execution registers the <status-indicator> custom element, and no separate stylesheet is required.

<script type="module" src="/assets/js/status-indicator.js"></script>
# Run from the project directory.
python3 -m http.server 5183

Basic Usage

Add the custom element after the module script. The value attribute selects one of the five default IDs: backlog, in-progress, needs-review, done, or cancel.

<script type="module" src="/assets/js/status-indicator.js"></script>
<status-indicator
  id="task-status"
  value="needs-review"
  aria-label="Task status">
</status-indicator>
<script type="module">
  const taskStatus = document.querySelector("#task-status");
  taskStatus.addEventListener("change", function (event) {
    console.log(event.detail);
    // { value: "done", index: 3, label: "Done" }
  });
</script>

Attributes And Properties

The component exposes a small state API. Use attributes for the initial value and accessible group name. Use properties for dynamic updates and custom status definitions. Programmatic changes update the visual state and reflected value attribute. The snippet after the table shows selection by ID and index.

APIDescription
value attribute and propertyGets or sets the selected status ID. The default list accepts backlog, in-progress, needs-review, done, and cancel.
aria-label attributeSets the accessible name of the internal radiogroup. The default name is Status.
index propertyGets or sets the selected zero-based index. Invalid indexes receive no update.
statuses propertyReplaces the full status list with objects that contain id, label, and optional color, icon, and spin fields.
spring propertyGets or sets the animation response time and damping ratio through { response, zeta }.
const taskStatus = document.querySelector("#task-status");
// Select by status ID.
taskStatus.value = "done";
// Select by zero-based index.
taskStatus.index = 1;
// Read the current selection.
console.log(taskStatus.value);
console.log(taskStatus.index);

Define Custom Statuses

The statuses property replaces all default states. Each object needs a string id and label. The icon field accepts circle-dashed, loader, info, circle-check, circle-x, raw SVG inner markup, or a complete SVG element.

Keep custom SVG markup inside trusted application code. The component inserts that markup into its Shadow DOM.

const releaseStatus = document.querySelector("#task-status");
releaseStatus.statuses = [
  {
    id: "queued",
    label: "Queued",
    color: "#667085",
    icon: "circle-dashed"
  },
  {
    id: "building",
    label: "Building",
    color: "#2563eb",
    icon: "loader",
    spin: true
  },
  {
    id: "testing",
    label: "Testing",
    color: "#d97706",
    icon: "info"
  },
  {
    id: "released",
    label: "Released",
    color: "#15803d",
    icon: "circle-check"
  },
  {
    id: "rolled-back",
    label: "Rolled Back",
    color: "#dc2626",
    icon: "circle-x"
  }
];
releaseStatus.value = "testing";
releaseStatus.setAttribute("aria-label", "Release status");

Tune The Spring Motion

The spring property accepts response in seconds and zeta as the damping ratio. Lower response values settle faster. Higher damping values reduce overshoot. The component limits response to 0.05 through 3 and zeta to 0.05 through 1.5.

const taskStatus = document.querySelector("#task-status");
taskStatus.spring = {
  response: 0.3,
  zeta: 0.95
};

Styling And Customization

CSS Custom PropertyDefaultPurpose
--si-pill-bg#efeff1Selected pill background.
--si-label#1a1b1eSelected label color.
--si-dot#d5d7daResting dot color.
--si-dot-hover#c9ccd1Dot color on hover.
--si-dot-hover-bgrgba(26, 27, 30, 0.06)Circular hover surface behind an unselected dot.
--si-focus-ring#1a1b1eVisible keyboard focus outline.
status-indicator {
  --si-pill-bg: #e8eefc;
  --si-label: #172554;
  --si-dot: #b8c2d8;
  --si-dot-hover: #7c8caf;
  --si-dot-hover-bg: rgb(37 99 235 / 10%);
  --si-focus-ring: #2563eb;
}

Change Event And Form Integration

The component dispatches a bubbling, composed change event after a user selects a different status. Replacing statuses also dispatches the event when the previous status ID no longer exists. event.detail contains value, index, and label. Direct assignments to value or index do not dispatch this event.

The custom element does not submit a native form value. Mirror the selected ID into a hidden input when the status belongs to a standard HTML form.

<form id="ticket-form">
  <status-indicator
    id="ticket-status"
    value="in-progress"
    aria-label="Ticket status">
  </status-indicator>
  <input
    type="hidden"
    id="ticket-status-value"
    name="status"
    value="in-progress">
  <button type="submit">Save Ticket</button>
</form>
<script type="module">
  const indicator = document.querySelector("#ticket-status");
  const statusInput = document.querySelector("#ticket-status-value");
  indicator.addEventListener("change", function (event) {
    statusInput.value = event.detail.value;
  });
</script>

Keyboard Interactions

  • ArrowRight and ArrowDown select the next status and wrap to the first item.
  • ArrowLeft and ArrowUp select the previous status and wrap to the last item.
  • Home selects the first status.
  • End selects the last status.
  • Enter and Space activate the focused internal radio button through native button behavior.

Alternatives:

You Might Be Interested In:


Leave a Reply