Status Pill — Free Online Away Busy Presence JS Snippet

Status Pill · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Data-driven states
Pill and menu both render from one STATES array.
CSS variable colors
One dot class renders any color from --c.
Pulsing live dot
A CSS-only ring marks the active state.
Accessible dropdown
Button + listbox with aria-expanded and aria-selected.
Rotating caret
Driven by an aria-expanded attribute selector.
Click-away and Escape
Robust dismissal without a library.
Thin view layer
paint() and buildMenu() reflect any backend value.
No dependency
Pure HTML/CSS/JS for any profile menu.

About this UI Snippet

Status Pill — Presence Selector with Pulsing Dot

Screenshot of the Status Pill snippet rendered live

A status pill is the small presence control — Online, Away, Do not disturb, Offline — that apps like Slack, Discord, and Teams put on a profile so people know if you're reachable. This snippet builds a complete one in plain HTML, CSS, and vanilla JavaScript: a pill that shows the current state with a colored dot, a dropdown to change it, and a pulsing ring on the active state. No dependency.

One data array drives everything

A STATES array defines each presence with an id, label, color, sub-label, and a pulse flag. Both the menu and the pill render from it, so adding a state ("In a meeting") or recoloring one is a single edit — the dropdown options, the dot colors, and the selected display all stay in sync because they read from the same source.

Color via a CSS custom property

Each dot reads its color from a --c custom property set inline, so the same .sp-dot class renders green, amber, red, or grey purely from data. The "online" pulse is a CSS-only effect: a ::after ring that scales from 0.7× to 1.9× while fading out on an infinite spPulse keyframe, inheriting --c so the halo always matches the dot. Toggling the .pulse class turns the live indicator on only for active states.

An accessible dropdown

The trigger is a real <button> with aria-haspopup="listbox" and aria-expanded that flips as it opens, and the menu is a role="listbox" of role="option" items with aria-selected on the current one. The caret rotates 180° via an attribute selector on aria-expanded, so the open state is reflected in both ARIA and the visual without duplicate flags.

Open/close that behaves

The menu animates with opacity, transform, and visibility (so it's not focusable while hidden). It opens on click, closes when you click anywhere else — achieved by stopping propagation on the pill and listening for clicks on document — and closes on Escape. That's the standard, robust dropdown dismissal pattern without a library.

Wiring to real presence

Call select(id) from a websocket or your auth layer to reflect a user's real status, and POST from inside select() when the user changes their own. Because rendering flows through paint() and buildMenu(), the pill is a thin view over whatever presence value your backend holds.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than guessing how the pieces fit, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to trace exactly how the --c custom property lets one .sp-dot class render four different colors, or why the dropdown is closed with visibility plus opacity rather than display alone. It's a good partner for optimizing too — ask whether rebuilding the whole menu with buildMenu() on every select() call is wasteful for a list this small, or whether it would matter more at fifty states. For extending it, ask for a "Do Not Disturb with a custom message" state, a keyboard-navigable listbox using arrow keys instead of mouse-only selection, or a websocket-driven presence feed that calls select() automatically. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an accessible presence status pill in plain HTML, CSS, and JavaScript with no dependencies.

Requirements:
- A single data array of state objects, each with an id, label, color, sub-label, and a pulse boolean, that drives both the trigger pill and the dropdown menu so there is exactly one source of truth.
- The trigger must be a real button element with aria-haspopup="listbox" and an aria-expanded attribute that flips between "true" and "false" as the menu opens and closes.
- The dropdown must be a role="listbox" containing role="option" items, with aria-selected set on whichever option matches the current state.
- The colored status dot must use a single CSS class whose color comes from a CSS custom property set inline per state, not a separate CSS class per color.
- Only the state flagged as "pulse" should render an animated ring around its dot, built as a CSS keyframe on a ::after pseudo-element scaling outward while fading opacity, not a JS-driven animation.
- The dropdown must close on three triggers: clicking its own option, clicking anywhere outside the component (using a document-level click listener with propagation stopped on the trigger), and pressing Escape.
- Changing the selected state must update the trigger's dot color, label text, and rebuild the option list's active highlighting, all from a single render function reading the shared data array.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA status pill renders showing Online with a pulsing green dot.
  2. 2
    Click the pillA dropdown opens listing every presence state with its color.
  3. 3
    Pick a statusThe pill updates its dot color and label to the chosen state.
  4. 4
    Note the pulseOnly the active Online state shows the pulsing halo.
  5. 5
    Click away or press EscapeThe menu closes the standard way.
  6. 6
    Connect presenceCall select(id) from your websocket and POST on user changes.

Real-world uses

Common Use Cases

Chat and messaging apps
Set presence above a chat UI conversation.
Profile menus
Drop into a profile dropdown header.
Team rosters
Show who's reachable in a team presence list.
Avatars with status
Pair the dot with a status avatar.
Device health
Reuse the dot beside a battery indicator.
Learning dropdowns
A reference for an accessible listbox menu.

Got questions?

Frequently Asked Questions

Each dot reads its color from a --c CSS custom property set inline from the STATES data, so a single .sp-dot class renders green, amber, red, or grey. The pulsing halo is a ::after ring that also inherits --c, so the glow always matches whatever color the state defines.

Each state has a pulse flag in the data. paint() toggles a .pulse class on the dot only when that flag is true, and the class enables a CSS keyframe ring that scales out and fades. So the live, attention-drawing animation appears only for active presence and stays calm for Away, Busy, or Offline.

Yes. The trigger is a button with aria-haspopup="listbox" and an aria-expanded that flips on open, and the menu is a listbox of option items with aria-selected on the current state. The caret rotation is driven by an attribute selector on aria-expanded, so the visual and the ARIA never drift apart.

The pill stops click propagation, and a document-level click listener closes the menu whenever a click reaches it — which only happens for clicks outside the pill. An Escape keydown listener closes it too. The menu uses visibility:hidden while closed so it isn't focusable or interactive in the background.

Keep the current state id and open flag in component state, and render the pill and options from the STATES array. Move the document click and Escape listeners into a mount effect with cleanup on unmount. Call your presence API inside the select handler. In Tailwind, set the dot color with an inline --c style and style the rest with utilities.