A red dot that softly expands and fades is the fastest way to signal “this content is live.” In this tutorial you will build a pulsing Live indicator with pure CSS. The result is a tiny, reusable badge that drops into any header, player, or dashboard without JavaScript. You will get a clean HTML structure, a robust CSS animation, a theming system with custom properties, and practical accessibility options.
Why a Pulsing “Live” Indicator Matters
Users scan for status cues. A pulsing indicator draws the eye without hogging space. With pure CSS you avoid extra image requests, keep colors and sizing consistent across themes, and remove any runtime dependencies. You can tweak timing, easing, and size by flipping a few variables. No sprites. No Lottie files. No heavy shadows that cause repaints. Just a dot, a ring, and a readable label.
Prerequisites
You do not need a framework or build step. A single HTML snippet and a stylesheet will do. You should be comfortable targeting pseudo-elements and using CSS variables.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The badge holds a visual dot and a text label. The label communicates status for everyone, including screen readers. The dot is purely decorative. The container uses role=”status” so assistive tech treats it as a live region when needed. Here is the complete HTML you will use:
<!-- Live badge component -->
<div class="live-badge" role="status" aria-label="Live">
<span class="live-dot" aria-hidden="true"></span>
<span class="live-label">LIVE</span>
</div>
<!-- Example: a muted variant that keeps the dot but pauses the pulse -->
<div class="live-badge is-muted" role="status" aria-label="Live">
<span class="live-dot" aria-hidden="true"></span>
<span class="live-label">LIVE</span>
</div>
<!-- Example: offline version (no pulse, gray color, different label) -->
<div class="live-badge is-offline" role="status" aria-label="Offline">
<span class="live-dot" aria-hidden="true"></span>
<span class="live-label">OFFLINE</span>
</div>
Step 2: The Basic CSS & Styling
Start with a small set of custom properties for color, size, and timing. Keep the badge an inline-flex pill that aligns well next to titles or controls. The dot will sit to the left, and the label will read clearly on both light and dark backgrounds.
/* CSS */
:root {
--live-color: #e82f2f; /* base red for the dot and ring */
--live-color-contrast: #ffffff; /* text color on the badge background */
--badge-bg: rgba(232, 47, 47, 0.12);
--dot-size: 10px; /* base diameter of the dot */
--ring-scale: 3; /* how far the ring expands */
--pulse-duration: 1.6s; /* pulse timing */
--pulse-ease: cubic-bezier(.22, .61, .36, 1);
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
line-height: 1.4;
padding: 2rem;
background: #0e0f12;
color: #e6e6e6;
}
.live-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.6rem 0.25rem 0.4rem;
border-radius: 999px;
background: var(--badge-bg);
color: var(--live-color-contrast);
user-select: none;
}
.live-label {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}
.live-dot {
position: relative; /* anchor for the pulse ring */
width: var(--dot-size);
height: var(--dot-size);
border-radius: 50%;
background: var(--live-color);
box-shadow: 0 0 0 2px rgba(232, 47, 47, 0.2) inset, 0 0 12px rgba(232, 47, 47, 0.45);
}
Advanced Tip: Put key sizes and colors into custom properties. You can theme a dark player by swapping –badge-bg and –live-color without touching the component rules. For complex design systems, expose only the variables that product teams should adjust.
Step 3: Building the Dot and Static Badge
The dot already looks like a small disc because of border-radius: 50%. That is the lowest-cost way to draw a circle in CSS. If you want a refresher on the technique, see how to make a circle with CSS. The label sits in a rounded rectangle that reads well at small sizes; if you need help with basics, review how to make a rectangle with CSS. Now, wire up the pulse ring as a pseudo-element on the dot.
/* CSS */
/* Pulse ring attached to the dot */
.live-dot::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--live-color);
opacity: 0;
transform: scale(1);
transform-origin: center;
animation: live-pulse var(--pulse-duration) var(--pulse-ease) infinite;
}
/* Muted state: keep the dot, pause the motion */
.live-badge.is-muted .live-dot::after {
animation-play-state: paused;
opacity: 0; /* hide the ring while paused */
}
/* Offline state: swap colors, disable pulse, change glow */
.live-badge.is-offline {
--live-color: #8a8f98;
--badge-bg: rgba(138, 143, 152, 0.12);
color: #cfd2d6;
}
.live-badge.is-offline .live-dot {
background: var(--live-color);
box-shadow: 0 0 0 2px rgba(138, 143, 152, 0.2) inset;
}
.live-badge.is-offline .live-dot::after {
animation: none;
display: none;
}
/* Keyframes: expand and fade the ring */
@keyframes live-pulse {
0% {
opacity: 0.75;
transform: scale(1);
}
70% {
opacity: 0.15;
transform: scale(var(--ring-scale));
}
100% {
opacity: 0;
transform: scale(var(--ring-scale));
}
}
How This Works (Code Breakdown)
The pulse is a border-only circle that scales up and fades out. The ring lives in ::after so you do not need extra DOM. Setting position: relative on .live-dot gives the pseudo-element a local anchor. inset: 0 makes ::after match the dot’s size and center. border-radius: 50% turns that square box into a circle with a crisp edge. The border thickness stays constant during scaling, which looks sharper than animating a blur.
The animation uses a single transform and opacity track. transform is cheap for modern browsers because it avoids layout and uses the compositor. opacity follows the same path, which also stays on the compositor. Avoid animating box-shadow spread for this effect, since that property triggers paint on each frame.
The .is-muted variant pauses the ring without changing the dot. Some teams use that pattern when a stream stalls but the channel is still active. The offline variant demonstrates how the same HTML supports different states by only flipping variables and one or two rules. This is the benefit of a variable-first approach.
Step 4: Tuning Size, Rhythm, and Density
Badge rhythm depends on where you place it. A navigation bar needs a calmer pulse than a video player control. Add a few utility classes that override variables. These classes make the component flexible without altering base rules.
/* CSS */
/* Size presets */
.live-badge--sm { --dot-size: 8px; font-size: 11px; padding: 0.2rem 0.5rem 0.2rem 0.36rem; }
.live-badge--md { --dot-size: 10px; font-size: 12px; }
.live-badge--lg { --dot-size: 12px; font-size: 13px; padding: 0.3rem 0.7rem 0.3rem 0.45rem; }
/* Rhythm presets */
.live-badge--slow { --pulse-duration: 2.2s; --ring-scale: 3.5; }
.live-badge--fast { --pulse-duration: 1.1s; --ring-scale: 2.6; }
/* Color themes */
.live-badge--warning { --live-color: #f59f00; --badge-bg: rgba(245, 159, 0, 0.14); }
.live-badge--success { --live-color: #2fbf71; --badge-bg: rgba(47, 191, 113, 0.14); }
/* Optional: hover/focus treatment for interactive badges */
.live-badge[role="button"],
button.live-badge {
cursor: pointer;
outline: none;
border: none;
background: color-mix(in srgb, var(--badge-bg) 80%, #ffffff 20%);
}
.live-badge[role="button"]:hover .live-dot,
button.live-badge:hover .live-dot {
box-shadow: 0 0 0 2px rgba(255,255,255,0.08) inset, 0 0 14px color-mix(in srgb, var(--live-color) 60%, transparent);
}
/* Motion preferences */
@media (prefers-reduced-motion: reduce) {
.live-dot::after {
animation-duration: 4s; /* slower pulse */
}
}
@media (prefers-reduced-motion: no-preference) {
.live-badge--static .live-dot::after { animation: none; display: none; }
}
How This Works (Code Breakdown)
Size presets only change variables and padding so the click target remains balanced. You can attach these classes anywhere you render a badge. The rhythm presets target duration and scale. A slower pulse with a larger ring reads as gentle and less urgent. A faster, tighter ring feels more urgent.
The color themes show how to repurpose the exact same structure for warning or success events. For streaming content, leave it red. For status dashboards, consider mapping colors to known meanings in your product. The optional interactive rules let you turn the badge into a button with a simple role or element switch. Because the dot glow uses light inset and outer shadows, the hover rule just nudges both without animating expensive properties.
The media query for prefers-reduced-motion slows the animation so the cue remains visible but less forceful. Some teams choose to disable the animation entirely. The .live-badge–static utility provides that route. Place it on badges in busy lists to reduce screen activity while keeping the dot visible.
Advanced Techniques: Subtle Glow, Seamless Loops, and Borders
You can push the visual polish without paying a performance tax. Add a soft emissive glow, smooth the loop with a delayed start, and use borders for contrast on mixed backgrounds. All of this stays on transform and opacity for the primary motion.
/* CSS */
/* Add a faint trailing glow that syncs with the ring */
.live-dot::before {
content: "";
position: absolute;
inset: -4px;
border-radius: 50%;
background: radial-gradient(circle, rgba(232,47,47,0.35) 0%, rgba(232,47,47,0.0) 60%);
filter: blur(2px);
opacity: 0.0;
transform: scale(1);
transform-origin: center;
animation: live-glow var(--pulse-duration) var(--pulse-ease) infinite;
}
/* Offset the glow phase slightly so it starts after the ring begins */
@keyframes live-glow {
0% { opacity: 0.0; transform: scale(1); }
15% { opacity: 0.35; transform: scale(1.3); }
60% { opacity: 0.0; transform: scale(var(--ring-scale)); }
100% { opacity: 0.0; transform: scale(var(--ring-scale)); }
}
/* Optional: add a subtle border to lift the badge on both light and dark UIs */
.live-badge {
box-shadow: 0 0 0 1px rgba(255,255,255,0.05) inset, 0 1px 2px rgba(0,0,0,0.25);
}
/* Dark-on-light alternate theme */
.theme-light {
--badge-bg: rgba(232, 47, 47, 0.09);
color: #1f2328;
}
.theme-light .live-badge {
box-shadow: 0 0 0 1px rgba(0,0,0,0.07) inset, 0 1px 2px rgba(0,0,0,0.08);
}
.theme-light body { background: #f6f8fa; color: #1f2328; }
/* Respect reduced motion here as well */
@media (prefers-reduced-motion: reduce) {
.live-dot::before { animation: none; opacity: 0.0; }
}
The glow is a lightweight radial-gradient in ::before with a tiny blur. The animation lags the ring a bit, which improves readability and avoids a sharp “pop.” Borders and soft inner shadows hold the badge on top of a wide range of backgrounds. If your product often pairs the badge with icons like a play arrow, you can borrow techniques from shape work such as how to make a triangle right with CSS to craft a play glyph without images.
Accessibility & Performance
Small status elements can harm users if they flicker too fast or lack text. Treat the dot as decoration and always include a visible label.
Accessibility
The HTML includes a text label (LIVE) that states the meaning. Keep the dot aria-hidden so screen readers do not announce a “graphic.” The container has role=”status” and an aria-label. For badges that change state at runtime, you can add aria-live=”polite” to the container so updates are announced as they happen. Use clear text like “Live,” “Offline,” or “Standby.” Avoid color-only communication. If the badge needs to function as an interactive control, render it as a button or add role=”button” and keyboard handlers, and provide focus styles that meet contrast guidelines.
Motion sensitivity varies. Respect prefers-reduced-motion. You can slow the pulse as shown or remove it with a static utility class for users who prefer no animation. Keep the visual label visible in all modes so no meaning is lost.
Performance
The pulse runs on transform and opacity, which stay on the compositor. That keeps frames smooth even on mid-tier devices. Avoid animating box-shadow spread or filter intensity on every frame since those trigger paints. If you show many badges in a table, stagger their animation-delay or freeze some with .live-badge–static to reduce visual noise. Use reasonable sizes; a ring-scale around 2.5-3.5 reads well without covering adjacent elements. Skip heavy filters; a light blur on a small pseudo-element is safe, but measure if you add multiple large glows.
CSS that Pings on Cue
You now have a compact, themeable Live badge with a crisp pulsing ring, strong readability, and motion controls. The same pattern adapts to warning or success states by changing variables. Bring this into your player headers, streams, dashboards, and timelines, and keep iterating until the pulse matches your brand’s rhythm.