webdevpuneet.com

Status Pill — Free Online Away Busy Presence JS Snippet

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

CategoryCards

Copy & paste

Source Code

<div class="sp-card">
  <button type="button" class="sp-pill" id="spPill" aria-haspopup="listbox" aria-expanded="false">
    <span class="sp-dot" id="spDot"></span>
    <span class="sp-label" id="spLabel">Online</span>
    <svg class="sp-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  </button>
  <ul class="sp-menu" id="spMenu" role="listbox" tabindex="-1"></ul>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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 presence controls
Let users set Online, Away, Do not disturb or Offline above a chat UI conversation list, as Slack and Discord do.
Profile menu headers
Drop into a profile dropdown header, with pill and menu both rendered from one STATES array.
Team roster availability
Show who is reachable in a team presence list, with a CSS-only ring pulsing on the active state.
Avatar status dots
Pair the dot with a status avatar in a roster, where a single --c custom property colours any presence state.
Accessible dropdown learning
Study a button and listbox using aria-expanded and aria-selected, and reuse the dot beside a battery indicator for device health.

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.