
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
changepayload 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.
| API | Description |
|---|---|
value attribute and property | Gets or sets the selected status ID. The default list accepts backlog, in-progress, needs-review, done, and cancel. |
aria-label attribute | Sets the accessible name of the internal radiogroup. The default name is Status. |
index property | Gets or sets the selected zero-based index. Invalid indexes receive no update. |
statuses property | Replaces the full status list with objects that contain id, label, and optional color, icon, and spin fields. |
spring property | Gets 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 Property | Default | Purpose |
|---|---|---|
--si-pill-bg | #efeff1 | Selected pill background. |
--si-label | #1a1b1e | Selected label color. |
--si-dot | #d5d7da | Resting dot color. |
--si-dot-hover | #c9ccd1 | Dot color on hover. |
--si-dot-hover-bg | rgba(26, 27, 30, 0.06) | Circular hover surface behind an unselected dot. |
--si-focus-ring | #1a1b1e | Visible 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
ArrowRightandArrowDownselect the next status and wrap to the first item.ArrowLeftandArrowUpselect the previous status and wrap to the last item.Homeselects the first status.Endselects the last status.EnterandSpaceactivate the focused internal radio button through native button behavior.
Alternatives:
- Claude-Style Effort Slider Web Component – Claude Model Selector
- Animated AI Model and Reasoning Effort Selector – Model Picker
- Accessible Range Slider Custom Element
- Animated Step Progress Bar in Pure JavaScript







