24 CSS Tags & Chips03 / 24
Badge with Online Status Indicator Dot
The status language every chat and SaaS product speaks — online, away, busy, offline — rendered the way the big messengers render it: a dot cut into the avatar's corner with a ring that matches the surface behind it, so it reads as punched-through, not stuck-on. Online pulses a soft sonar ring, away is a true crescent cut with a CSS mask, busy carries the do-not-disturb dash, offline hollows out — and a bonus three-dot typing chip bounces. One data-status attribute drives everything.
Published
The code
<section class="ctc-03" aria-label="Online status indicator dot demo">
<div class="ctc-03__stage">
<div class="ctc-03__wrap">
<header class="ctc-03__head">
<p class="ctc-03__kicker">Status dots · one attribute, five states</p>
<h1>Presence you can read at 15px</h1>
</header>
<ul class="ctc-03__hero" aria-label="Status dot states at hero size">
<li data-status="online"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=160&q=60);--h:250"><i data-dot></i></span><b>Online</b><small>sonar ping ::after</small></li>
<li data-status="away"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=160&q=60);--h:20"><i data-dot></i></span><b>Away</b><small>crescent via <code>mask</code></small></li>
<li data-status="busy"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=160&q=60);--h:140"><i data-dot></i></span><b>Busy · DND</b><small>gradient dash</small></li>
<li data-status="offline"><span class="ctc-03__avatar ctc-03__avatar--xl" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Offline</b><small>inset box-shadow ring</small></li>
<li data-status="typing"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=160&q=60);--h:330"><i data-dot></i></span><b>Typing</b><small>staggered bounce</small></li>
</ul>
<div class="ctc-03__roster" role="group" aria-label="The same dots in a roster at 40px">
<p class="ctc-03__label">In context — 40px roster, same CSS</p>
<ul class="ctc-03__list">
<li data-status="online"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=120&q=60);--h:250"><i data-dot></i></span><b>Jonas Reid</b><span class="ctc-03__state">Active now</span></li>
<li data-status="typing"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&q=60);--h:330"><i data-dot></i></span><b>Mara Voss</b><span class="ctc-03__typing" aria-label="typing"><i></i><i></i><i></i></span></li>
<li data-status="away"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=120&q=60);--h:20"><i data-dot></i></span><b>Priya Anand</b><span class="ctc-03__state">Back at 2:30</span></li>
<li data-status="busy"><span class="ctc-03__avatar" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Kai Lindgren</b><span class="ctc-03__state">In a call</span></li>
<li data-status="offline"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=120&q=60);--h:80"><i data-dot></i></span><b>Ana Duarte</b><span class="ctc-03__state">Last seen 1d</span></li>
</ul>
</div>
<p class="ctc-03__note">Every row is just <code><li data-status="away"></code> — dot color, dot shape and the trailing label all key off that one attribute. Shape + text back up every color, so the system survives color-blindness and grayscale.</p>
</div>
</div>
</section><section class="ctc-03" aria-label="Online status indicator dot demo">
<div class="ctc-03__stage">
<div class="ctc-03__wrap">
<header class="ctc-03__head">
<p class="ctc-03__kicker">Status dots · one attribute, five states</p>
<h1>Presence you can read at 15px</h1>
</header>
<ul class="ctc-03__hero" aria-label="Status dot states at hero size">
<li data-status="online"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=160&q=60);--h:250"><i data-dot></i></span><b>Online</b><small>sonar ping ::after</small></li>
<li data-status="away"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=160&q=60);--h:20"><i data-dot></i></span><b>Away</b><small>crescent via <code>mask</code></small></li>
<li data-status="busy"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=160&q=60);--h:140"><i data-dot></i></span><b>Busy · DND</b><small>gradient dash</small></li>
<li data-status="offline"><span class="ctc-03__avatar ctc-03__avatar--xl" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Offline</b><small>inset box-shadow ring</small></li>
<li data-status="typing"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=160&q=60);--h:330"><i data-dot></i></span><b>Typing</b><small>staggered bounce</small></li>
</ul>
<div class="ctc-03__roster" role="group" aria-label="The same dots in a roster at 40px">
<p class="ctc-03__label">In context — 40px roster, same CSS</p>
<ul class="ctc-03__list">
<li data-status="online"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=120&q=60);--h:250"><i data-dot></i></span><b>Jonas Reid</b><span class="ctc-03__state">Active now</span></li>
<li data-status="typing"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&q=60);--h:330"><i data-dot></i></span><b>Mara Voss</b><span class="ctc-03__typing" aria-label="typing"><i></i><i></i><i></i></span></li>
<li data-status="away"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=120&q=60);--h:20"><i data-dot></i></span><b>Priya Anand</b><span class="ctc-03__state">Back at 2:30</span></li>
<li data-status="busy"><span class="ctc-03__avatar" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Kai Lindgren</b><span class="ctc-03__state">In a call</span></li>
<li data-status="offline"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=120&q=60);--h:80"><i data-dot></i></span><b>Ana Duarte</b><span class="ctc-03__state">Last seen 1d</span></li>
</ul>
</div>
<p class="ctc-03__note">Every row is just <code><li data-status="away"></code> — dot color, dot shape and the trailing label all key off that one attribute. Shape + text back up every color, so the system survives color-blindness and grayscale.</p>
</div>
</div>
</section>.ctc-03,
.ctc-03 *,
.ctc-03 *::before,
.ctc-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ctc-03 {
--bg: oklch(0.2 0.02 275);
--panel: oklch(0.25 0.022 275);
--ink: oklch(0.95 0.008 275);
--mut: oklch(0.66 0.02 275);
--line: oklch(0.33 0.025 275);
--on: oklch(0.75 0.19 148);
--away: oklch(0.8 0.15 80);
--busy: oklch(0.62 0.21 22);
--off: oklch(0.55 0.015 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.ctc-03__stage {
width: 100%;
height: 100vh;
height: 100svh;
overflow-y: auto;
display: grid;
place-items: center;
padding: clamp(16px,4cqi,48px);
background: radial-gradient(70cqi 55cqi at 85% -10%,oklch(0.3 0.06 148/.45),transparent 62%),var(--bg);
container: ctc03/inline-size;
}
.ctc-03__wrap {
width: min(760px,100%);
display: flex;
flex-direction: column;
gap: 24px;
}
.ctc-03__kicker {
font-size: 11.5px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--on);
}
.ctc-03__head h1 {
font-size: clamp(24px,3.8cqi,38px);
font-weight: 800;
letter-spacing: -.03em;
margin-top: 8px;
}
.ctc-03__hero {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: clamp(14px,3cqi,30px);
justify-content: space-between;
}
.ctc-03__hero li {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
}
.ctc-03__hero b {
font-size: 13px;
font-weight: 750;
margin-top: 8px;
}
.ctc-03__hero small {
font-size: 11px;
color: var(--mut);
}
.ctc-03__hero code,
.ctc-03__note code {
font-family: ui-monospace,Menlo,monospace;
font-size: .9em;
background: oklch(1 0 0/.09);
padding: 1px 5px;
border-radius: 4px;
color: var(--ink);
}
.ctc-03__avatar {
position: relative;
flex: none;
width: 40px;
height: 40px;
border-radius: 13px;
background-image: var(--img,none),linear-gradient(140deg,oklch(0.65 0.13 var(--h,275)),oklch(0.45 0.12 calc(var(--h,275) + 50)));
background-size: cover;
background-position: center;
}
.ctc-03__avatar--xl {
width: clamp(56px,7cqi,72px);
height: clamp(56px,7cqi,72px);
border-radius: 22px;
}
.ctc-03__avatar[data-fallback]::before {
content: attr(data-fallback);
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: .42em;
font-weight: 800;
color: oklch(0.98 0 0/.92);
font-size: 14px;
}
.ctc-03 [data-dot] {
position: absolute;
right: -3px;
bottom: -3px;
width: 15px;
height: 15px;
border-radius: 50%;
border: 3px solid var(--bg);
background: var(--off);
z-index: 1;
}
.ctc-03__avatar--xl [data-dot] {
width: 24%;
aspect-ratio: 1;
height: auto;
right: -2px;
bottom: -2px;
border-width: 4px;
}
.ctc-03__list [data-dot] {
border-color: var(--panel);
}
[data-status="online"] [data-dot],
[data-status="typing"] [data-dot] {
background: var(--on);
}
[data-status="online"] [data-dot]::after {
content: "";
position: absolute;
inset: -3px;
border-radius: 50%;
border: 2px solid var(--on);
animation: ctc03-ping 2.2s ease-out infinite;
}
@keyframes ctc03-ping {
0% {
scale: .6;
opacity: .9;
}
70%,
100% {
scale: 1.9;
opacity: 0;
}
}
[data-status="away"] [data-dot] {
background: var(--away);
-webkit-mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
}
[data-status="busy"] [data-dot] {
background: linear-gradient(0deg,var(--busy) 32%,oklch(0.99 0 0) 33% 67%,var(--busy) 68%) no-repeat center/64% 100%,var(--busy);
}
[data-status="offline"] [data-dot] {
background: transparent;
box-shadow: inset 0 0 0 2.5px var(--off);
}
.ctc-03__roster {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 14px;
}
.ctc-03__label {
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mut);
padding: 4px 10px 10px;
}
.ctc-03__list {
list-style: none;
display: flex;
flex-direction: column;
}
.ctc-03__list li {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 10px;
border-radius: 12px;
transition: background .18s;
}
.ctc-03__list li:hover {
background: oklch(1 0 0/.06);
}
.ctc-03__list b {
font-size: 14px;
font-weight: 700;
flex: 1;
}
.ctc-03__list li[data-status="offline"] {
opacity: .6;
}
.ctc-03__state {
font-size: 11.5px;
font-weight: 700;
color: var(--mut);
background: oklch(1 0 0/.07);
border: 1px solid var(--line);
padding: 5px 11px;
border-radius: 99px;
}
[data-status="online"] .ctc-03__state {
color: var(--on);
border-color: color-mix(in oklch,var(--on) 35%,transparent);
background: color-mix(in oklch,var(--on) 12%,transparent);
}
[data-status="away"] .ctc-03__state {
color: var(--away);
border-color: color-mix(in oklch,var(--away) 35%,transparent);
background: color-mix(in oklch,var(--away) 12%,transparent);
}
[data-status="busy"] .ctc-03__state {
color: oklch(0.78 0.14 22);
border-color: color-mix(in oklch,var(--busy) 45%,transparent);
background: color-mix(in oklch,var(--busy) 15%,transparent);
}
.ctc-03__typing {
display: inline-flex;
gap: 3px;
align-items: center;
min-height: 26px;
padding: 0 12px;
border-radius: 99px;
background: color-mix(in oklch,var(--on) 12%,transparent);
border: 1px solid color-mix(in oklch,var(--on) 35%,transparent);
}
.ctc-03__typing i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--on);
animation: ctc03-bounce 1.1s ease-in-out infinite;
}
.ctc-03__typing i:nth-child(2) {
animation-delay: .15s;
}
.ctc-03__typing i:nth-child(3) {
animation-delay: .3s;
}
@keyframes ctc03-bounce {
30% {
translate: 0 -4px;
}
60% {
translate: 0 0;
}
}
.ctc-03__note {
font-size: 12.5px;
line-height: 1.6;
color: var(--mut);
border-left: 3px solid var(--on);
padding-left: 12px;
max-width: 60ch;
}
@container ctc03 (width < 560px) {
.ctc-03__hero {
justify-content: flex-start;
gap: 18px;
}
.ctc-03__state {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ctc-03 [data-dot]::after {
animation: none;
opacity: 0;
}
.ctc-03__typing i {
animation: none;
}
}.ctc-03,
.ctc-03 *,
.ctc-03 *::before,
.ctc-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ctc-03 {
--bg: oklch(0.2 0.02 275);
--panel: oklch(0.25 0.022 275);
--ink: oklch(0.95 0.008 275);
--mut: oklch(0.66 0.02 275);
--line: oklch(0.33 0.025 275);
--on: oklch(0.75 0.19 148);
--away: oklch(0.8 0.15 80);
--busy: oklch(0.62 0.21 22);
--off: oklch(0.55 0.015 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.ctc-03__stage {
width: 100%;
height: 100vh;
height: 100svh;
overflow-y: auto;
display: grid;
place-items: center;
padding: clamp(16px,4cqi,48px);
background: radial-gradient(70cqi 55cqi at 85% -10%,oklch(0.3 0.06 148/.45),transparent 62%),var(--bg);
container: ctc03/inline-size;
}
.ctc-03__wrap {
width: min(760px,100%);
display: flex;
flex-direction: column;
gap: 24px;
}
.ctc-03__kicker {
font-size: 11.5px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--on);
}
.ctc-03__head h1 {
font-size: clamp(24px,3.8cqi,38px);
font-weight: 800;
letter-spacing: -.03em;
margin-top: 8px;
}
.ctc-03__hero {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: clamp(14px,3cqi,30px);
justify-content: space-between;
}
.ctc-03__hero li {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
}
.ctc-03__hero b {
font-size: 13px;
font-weight: 750;
margin-top: 8px;
}
.ctc-03__hero small {
font-size: 11px;
color: var(--mut);
}
.ctc-03__hero code,
.ctc-03__note code {
font-family: ui-monospace,Menlo,monospace;
font-size: .9em;
background: oklch(1 0 0/.09);
padding: 1px 5px;
border-radius: 4px;
color: var(--ink);
}
.ctc-03__avatar {
position: relative;
flex: none;
width: 40px;
height: 40px;
border-radius: 13px;
background-image: var(--img,none),linear-gradient(140deg,oklch(0.65 0.13 var(--h,275)),oklch(0.45 0.12 calc(var(--h,275) + 50)));
background-size: cover;
background-position: center;
}
.ctc-03__avatar--xl {
width: clamp(56px,7cqi,72px);
height: clamp(56px,7cqi,72px);
border-radius: 22px;
}
.ctc-03__avatar[data-fallback]::before {
content: attr(data-fallback);
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: .42em;
font-weight: 800;
color: oklch(0.98 0 0/.92);
font-size: 14px;
}
.ctc-03 [data-dot] {
position: absolute;
right: -3px;
bottom: -3px;
width: 15px;
height: 15px;
border-radius: 50%;
border: 3px solid var(--bg);
background: var(--off);
z-index: 1;
}
.ctc-03__avatar--xl [data-dot] {
width: 24%;
aspect-ratio: 1;
height: auto;
right: -2px;
bottom: -2px;
border-width: 4px;
}
.ctc-03__list [data-dot] {
border-color: var(--panel);
}
[data-status="online"] [data-dot],
[data-status="typing"] [data-dot] {
background: var(--on);
}
[data-status="online"] [data-dot]::after {
content: "";
position: absolute;
inset: -3px;
border-radius: 50%;
border: 2px solid var(--on);
animation: ctc03-ping 2.2s ease-out infinite;
}
@keyframes ctc03-ping {
0% {
scale: .6;
opacity: .9;
}
70%,
100% {
scale: 1.9;
opacity: 0;
}
}
[data-status="away"] [data-dot] {
background: var(--away);
-webkit-mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
}
[data-status="busy"] [data-dot] {
background: linear-gradient(0deg,var(--busy) 32%,oklch(0.99 0 0) 33% 67%,var(--busy) 68%) no-repeat center/64% 100%,var(--busy);
}
[data-status="offline"] [data-dot] {
background: transparent;
box-shadow: inset 0 0 0 2.5px var(--off);
}
.ctc-03__roster {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 14px;
}
.ctc-03__label {
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mut);
padding: 4px 10px 10px;
}
.ctc-03__list {
list-style: none;
display: flex;
flex-direction: column;
}
.ctc-03__list li {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 10px;
border-radius: 12px;
transition: background .18s;
}
.ctc-03__list li:hover {
background: oklch(1 0 0/.06);
}
.ctc-03__list b {
font-size: 14px;
font-weight: 700;
flex: 1;
}
.ctc-03__list li[data-status="offline"] {
opacity: .6;
}
.ctc-03__state {
font-size: 11.5px;
font-weight: 700;
color: var(--mut);
background: oklch(1 0 0/.07);
border: 1px solid var(--line);
padding: 5px 11px;
border-radius: 99px;
}
[data-status="online"] .ctc-03__state {
color: var(--on);
border-color: color-mix(in oklch,var(--on) 35%,transparent);
background: color-mix(in oklch,var(--on) 12%,transparent);
}
[data-status="away"] .ctc-03__state {
color: var(--away);
border-color: color-mix(in oklch,var(--away) 35%,transparent);
background: color-mix(in oklch,var(--away) 12%,transparent);
}
[data-status="busy"] .ctc-03__state {
color: oklch(0.78 0.14 22);
border-color: color-mix(in oklch,var(--busy) 45%,transparent);
background: color-mix(in oklch,var(--busy) 15%,transparent);
}
.ctc-03__typing {
display: inline-flex;
gap: 3px;
align-items: center;
min-height: 26px;
padding: 0 12px;
border-radius: 99px;
background: color-mix(in oklch,var(--on) 12%,transparent);
border: 1px solid color-mix(in oklch,var(--on) 35%,transparent);
}
.ctc-03__typing i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--on);
animation: ctc03-bounce 1.1s ease-in-out infinite;
}
.ctc-03__typing i:nth-child(2) {
animation-delay: .15s;
}
.ctc-03__typing i:nth-child(3) {
animation-delay: .3s;
}
@keyframes ctc03-bounce {
30% {
translate: 0 -4px;
}
60% {
translate: 0 0;
}
}
.ctc-03__note {
font-size: 12.5px;
line-height: 1.6;
color: var(--mut);
border-left: 3px solid var(--on);
padding-left: 12px;
max-width: 60ch;
}
@container ctc03 (width < 560px) {
.ctc-03__hero {
justify-content: flex-start;
gap: 18px;
}
.ctc-03__state {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ctc-03 [data-dot]::after {
animation: none;
opacity: 0;
}
.ctc-03__typing i {
animation: none;
}
}Here's a working CSS Tags & Chip from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Badge with Online Status Indicator Dot
Source: https://codefronts.com/snippets/css-tags-chips/badge-with-online-status-indicator-dot/
The status language every chat and SaaS product speaks — online, away, busy, offline — rendered the way the big messengers render it: a dot cut into the avatar's corner with a ring that matches the surface behind it, so it reads as punched-through, not stuck-on. Online pulses a soft sonar ring, away is a true crescent cut with a CSS mask, busy carries the do-not-disturb dash, offline hollows out — and a bonus three-dot typing chip bounces. One data-status attribute drives everything.
## HTML
```html
<section class="ctc-03" aria-label="Online status indicator dot demo">
<div class="ctc-03__stage">
<div class="ctc-03__wrap">
<header class="ctc-03__head">
<p class="ctc-03__kicker">Status dots · one attribute, five states</p>
<h1>Presence you can read at 15px</h1>
</header>
<ul class="ctc-03__hero" aria-label="Status dot states at hero size">
<li data-status="online"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=160&q=60);--h:250"><i data-dot></i></span><b>Online</b><small>sonar ping ::after</small></li>
<li data-status="away"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=160&q=60);--h:20"><i data-dot></i></span><b>Away</b><small>crescent via <code>mask</code></small></li>
<li data-status="busy"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=160&q=60);--h:140"><i data-dot></i></span><b>Busy · DND</b><small>gradient dash</small></li>
<li data-status="offline"><span class="ctc-03__avatar ctc-03__avatar--xl" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Offline</b><small>inset box-shadow ring</small></li>
<li data-status="typing"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=160&q=60);--h:330"><i data-dot></i></span><b>Typing</b><small>staggered bounce</small></li>
</ul>
<div class="ctc-03__roster" role="group" aria-label="The same dots in a roster at 40px">
<p class="ctc-03__label">In context — 40px roster, same CSS</p>
<ul class="ctc-03__list">
<li data-status="online"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=120&q=60);--h:250"><i data-dot></i></span><b>Jonas Reid</b><span class="ctc-03__state">Active now</span></li>
<li data-status="typing"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&q=60);--h:330"><i data-dot></i></span><b>Mara Voss</b><span class="ctc-03__typing" aria-label="typing"><i></i><i></i><i></i></span></li>
<li data-status="away"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=120&q=60);--h:20"><i data-dot></i></span><b>Priya Anand</b><span class="ctc-03__state">Back at 2:30</span></li>
<li data-status="busy"><span class="ctc-03__avatar" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Kai Lindgren</b><span class="ctc-03__state">In a call</span></li>
<li data-status="offline"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=120&q=60);--h:80"><i data-dot></i></span><b>Ana Duarte</b><span class="ctc-03__state">Last seen 1d</span></li>
</ul>
</div>
<p class="ctc-03__note">Every row is just <code><li data-status="away"></code> — dot color, dot shape and the trailing label all key off that one attribute. Shape + text back up every color, so the system survives color-blindness and grayscale.</p>
</div>
</div>
</section>
```
## CSS
```css
.ctc-03,
.ctc-03 *,
.ctc-03 *::before,
.ctc-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ctc-03 {
--bg: oklch(0.2 0.02 275);
--panel: oklch(0.25 0.022 275);
--ink: oklch(0.95 0.008 275);
--mut: oklch(0.66 0.02 275);
--line: oklch(0.33 0.025 275);
--on: oklch(0.75 0.19 148);
--away: oklch(0.8 0.15 80);
--busy: oklch(0.62 0.21 22);
--off: oklch(0.55 0.015 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.ctc-03__stage {
width: 100%;
height: 100vh;
height: 100svh;
overflow-y: auto;
display: grid;
place-items: center;
padding: clamp(16px,4cqi,48px);
background: radial-gradient(70cqi 55cqi at 85% -10%,oklch(0.3 0.06 148/.45),transparent 62%),var(--bg);
container: ctc03/inline-size;
}
.ctc-03__wrap {
width: min(760px,100%);
display: flex;
flex-direction: column;
gap: 24px;
}
.ctc-03__kicker {
font-size: 11.5px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--on);
}
.ctc-03__head h1 {
font-size: clamp(24px,3.8cqi,38px);
font-weight: 800;
letter-spacing: -.03em;
margin-top: 8px;
}
.ctc-03__hero {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: clamp(14px,3cqi,30px);
justify-content: space-between;
}
.ctc-03__hero li {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
}
.ctc-03__hero b {
font-size: 13px;
font-weight: 750;
margin-top: 8px;
}
.ctc-03__hero small {
font-size: 11px;
color: var(--mut);
}
.ctc-03__hero code,
.ctc-03__note code {
font-family: ui-monospace,Menlo,monospace;
font-size: .9em;
background: oklch(1 0 0/.09);
padding: 1px 5px;
border-radius: 4px;
color: var(--ink);
}
.ctc-03__avatar {
position: relative;
flex: none;
width: 40px;
height: 40px;
border-radius: 13px;
background-image: var(--img,none),linear-gradient(140deg,oklch(0.65 0.13 var(--h,275)),oklch(0.45 0.12 calc(var(--h,275) + 50)));
background-size: cover;
background-position: center;
}
.ctc-03__avatar--xl {
width: clamp(56px,7cqi,72px);
height: clamp(56px,7cqi,72px);
border-radius: 22px;
}
.ctc-03__avatar[data-fallback]::before {
content: attr(data-fallback);
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: .42em;
font-weight: 800;
color: oklch(0.98 0 0/.92);
font-size: 14px;
}
.ctc-03 [data-dot] {
position: absolute;
right: -3px;
bottom: -3px;
width: 15px;
height: 15px;
border-radius: 50%;
border: 3px solid var(--bg);
background: var(--off);
z-index: 1;
}
.ctc-03__avatar--xl [data-dot] {
width: 24%;
aspect-ratio: 1;
height: auto;
right: -2px;
bottom: -2px;
border-width: 4px;
}
.ctc-03__list [data-dot] {
border-color: var(--panel);
}
[data-status="online"] [data-dot],
[data-status="typing"] [data-dot] {
background: var(--on);
}
[data-status="online"] [data-dot]::after {
content: "";
position: absolute;
inset: -3px;
border-radius: 50%;
border: 2px solid var(--on);
animation: ctc03-ping 2.2s ease-out infinite;
}
@keyframes ctc03-ping {
0% {
scale: .6;
opacity: .9;
}
70%,
100% {
scale: 1.9;
opacity: 0;
}
}
[data-status="away"] [data-dot] {
background: var(--away);
-webkit-mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
}
[data-status="busy"] [data-dot] {
background: linear-gradient(0deg,var(--busy) 32%,oklch(0.99 0 0) 33% 67%,var(--busy) 68%) no-repeat center/64% 100%,var(--busy);
}
[data-status="offline"] [data-dot] {
background: transparent;
box-shadow: inset 0 0 0 2.5px var(--off);
}
.ctc-03__roster {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 14px;
}
.ctc-03__label {
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mut);
padding: 4px 10px 10px;
}
.ctc-03__list {
list-style: none;
display: flex;
flex-direction: column;
}
.ctc-03__list li {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 10px;
border-radius: 12px;
transition: background .18s;
}
.ctc-03__list li:hover {
background: oklch(1 0 0/.06);
}
.ctc-03__list b {
font-size: 14px;
font-weight: 700;
flex: 1;
}
.ctc-03__list li[data-status="offline"] {
opacity: .6;
}
.ctc-03__state {
font-size: 11.5px;
font-weight: 700;
color: var(--mut);
background: oklch(1 0 0/.07);
border: 1px solid var(--line);
padding: 5px 11px;
border-radius: 99px;
}
[data-status="online"] .ctc-03__state {
color: var(--on);
border-color: color-mix(in oklch,var(--on) 35%,transparent);
background: color-mix(in oklch,var(--on) 12%,transparent);
}
[data-status="away"] .ctc-03__state {
color: var(--away);
border-color: color-mix(in oklch,var(--away) 35%,transparent);
background: color-mix(in oklch,var(--away) 12%,transparent);
}
[data-status="busy"] .ctc-03__state {
color: oklch(0.78 0.14 22);
border-color: color-mix(in oklch,var(--busy) 45%,transparent);
background: color-mix(in oklch,var(--busy) 15%,transparent);
}
.ctc-03__typing {
display: inline-flex;
gap: 3px;
align-items: center;
min-height: 26px;
padding: 0 12px;
border-radius: 99px;
background: color-mix(in oklch,var(--on) 12%,transparent);
border: 1px solid color-mix(in oklch,var(--on) 35%,transparent);
}
.ctc-03__typing i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--on);
animation: ctc03-bounce 1.1s ease-in-out infinite;
}
.ctc-03__typing i:nth-child(2) {
animation-delay: .15s;
}
.ctc-03__typing i:nth-child(3) {
animation-delay: .3s;
}
@keyframes ctc03-bounce {
30% {
translate: 0 -4px;
}
60% {
translate: 0 0;
}
}
.ctc-03__note {
font-size: 12.5px;
line-height: 1.6;
color: var(--mut);
border-left: 3px solid var(--on);
padding-left: 12px;
max-width: 60ch;
}
@container ctc03 (width < 560px) {
.ctc-03__hero {
justify-content: flex-start;
gap: 18px;
}
.ctc-03__state {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ctc-03 [data-dot]::after {
animation: none;
opacity: 0;
}
.ctc-03__typing i {
animation: none;
}
}
```Here's a working CSS Tags & Chip from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Badge with Online Status Indicator Dot
Source: https://codefronts.com/snippets/css-tags-chips/badge-with-online-status-indicator-dot/
The status language every chat and SaaS product speaks — online, away, busy, offline — rendered the way the big messengers render it: a dot cut into the avatar's corner with a ring that matches the surface behind it, so it reads as punched-through, not stuck-on. Online pulses a soft sonar ring, away is a true crescent cut with a CSS mask, busy carries the do-not-disturb dash, offline hollows out — and a bonus three-dot typing chip bounces. One data-status attribute drives everything.
## HTML
```html
<section class="ctc-03" aria-label="Online status indicator dot demo">
<div class="ctc-03__stage">
<div class="ctc-03__wrap">
<header class="ctc-03__head">
<p class="ctc-03__kicker">Status dots · one attribute, five states</p>
<h1>Presence you can read at 15px</h1>
</header>
<ul class="ctc-03__hero" aria-label="Status dot states at hero size">
<li data-status="online"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=160&q=60);--h:250"><i data-dot></i></span><b>Online</b><small>sonar ping ::after</small></li>
<li data-status="away"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=160&q=60);--h:20"><i data-dot></i></span><b>Away</b><small>crescent via <code>mask</code></small></li>
<li data-status="busy"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=160&q=60);--h:140"><i data-dot></i></span><b>Busy · DND</b><small>gradient dash</small></li>
<li data-status="offline"><span class="ctc-03__avatar ctc-03__avatar--xl" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Offline</b><small>inset box-shadow ring</small></li>
<li data-status="typing"><span class="ctc-03__avatar ctc-03__avatar--xl" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=160&q=60);--h:330"><i data-dot></i></span><b>Typing</b><small>staggered bounce</small></li>
</ul>
<div class="ctc-03__roster" role="group" aria-label="The same dots in a roster at 40px">
<p class="ctc-03__label">In context — 40px roster, same CSS</p>
<ul class="ctc-03__list">
<li data-status="online"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=120&q=60);--h:250"><i data-dot></i></span><b>Jonas Reid</b><span class="ctc-03__state">Active now</span></li>
<li data-status="typing"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&q=60);--h:330"><i data-dot></i></span><b>Mara Voss</b><span class="ctc-03__typing" aria-label="typing"><i></i><i></i><i></i></span></li>
<li data-status="away"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=120&q=60);--h:20"><i data-dot></i></span><b>Priya Anand</b><span class="ctc-03__state">Back at 2:30</span></li>
<li data-status="busy"><span class="ctc-03__avatar" data-fallback="KL" style="--h:300"><i data-dot></i></span><b>Kai Lindgren</b><span class="ctc-03__state">In a call</span></li>
<li data-status="offline"><span class="ctc-03__avatar" style="--img:url(https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=120&q=60);--h:80"><i data-dot></i></span><b>Ana Duarte</b><span class="ctc-03__state">Last seen 1d</span></li>
</ul>
</div>
<p class="ctc-03__note">Every row is just <code><li data-status="away"></code> — dot color, dot shape and the trailing label all key off that one attribute. Shape + text back up every color, so the system survives color-blindness and grayscale.</p>
</div>
</div>
</section>
```
## CSS
```css
.ctc-03,
.ctc-03 *,
.ctc-03 *::before,
.ctc-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ctc-03 {
--bg: oklch(0.2 0.02 275);
--panel: oklch(0.25 0.022 275);
--ink: oklch(0.95 0.008 275);
--mut: oklch(0.66 0.02 275);
--line: oklch(0.33 0.025 275);
--on: oklch(0.75 0.19 148);
--away: oklch(0.8 0.15 80);
--busy: oklch(0.62 0.21 22);
--off: oklch(0.55 0.015 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.ctc-03__stage {
width: 100%;
height: 100vh;
height: 100svh;
overflow-y: auto;
display: grid;
place-items: center;
padding: clamp(16px,4cqi,48px);
background: radial-gradient(70cqi 55cqi at 85% -10%,oklch(0.3 0.06 148/.45),transparent 62%),var(--bg);
container: ctc03/inline-size;
}
.ctc-03__wrap {
width: min(760px,100%);
display: flex;
flex-direction: column;
gap: 24px;
}
.ctc-03__kicker {
font-size: 11.5px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--on);
}
.ctc-03__head h1 {
font-size: clamp(24px,3.8cqi,38px);
font-weight: 800;
letter-spacing: -.03em;
margin-top: 8px;
}
.ctc-03__hero {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: clamp(14px,3cqi,30px);
justify-content: space-between;
}
.ctc-03__hero li {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
}
.ctc-03__hero b {
font-size: 13px;
font-weight: 750;
margin-top: 8px;
}
.ctc-03__hero small {
font-size: 11px;
color: var(--mut);
}
.ctc-03__hero code,
.ctc-03__note code {
font-family: ui-monospace,Menlo,monospace;
font-size: .9em;
background: oklch(1 0 0/.09);
padding: 1px 5px;
border-radius: 4px;
color: var(--ink);
}
.ctc-03__avatar {
position: relative;
flex: none;
width: 40px;
height: 40px;
border-radius: 13px;
background-image: var(--img,none),linear-gradient(140deg,oklch(0.65 0.13 var(--h,275)),oklch(0.45 0.12 calc(var(--h,275) + 50)));
background-size: cover;
background-position: center;
}
.ctc-03__avatar--xl {
width: clamp(56px,7cqi,72px);
height: clamp(56px,7cqi,72px);
border-radius: 22px;
}
.ctc-03__avatar[data-fallback]::before {
content: attr(data-fallback);
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: .42em;
font-weight: 800;
color: oklch(0.98 0 0/.92);
font-size: 14px;
}
.ctc-03 [data-dot] {
position: absolute;
right: -3px;
bottom: -3px;
width: 15px;
height: 15px;
border-radius: 50%;
border: 3px solid var(--bg);
background: var(--off);
z-index: 1;
}
.ctc-03__avatar--xl [data-dot] {
width: 24%;
aspect-ratio: 1;
height: auto;
right: -2px;
bottom: -2px;
border-width: 4px;
}
.ctc-03__list [data-dot] {
border-color: var(--panel);
}
[data-status="online"] [data-dot],
[data-status="typing"] [data-dot] {
background: var(--on);
}
[data-status="online"] [data-dot]::after {
content: "";
position: absolute;
inset: -3px;
border-radius: 50%;
border: 2px solid var(--on);
animation: ctc03-ping 2.2s ease-out infinite;
}
@keyframes ctc03-ping {
0% {
scale: .6;
opacity: .9;
}
70%,
100% {
scale: 1.9;
opacity: 0;
}
}
[data-status="away"] [data-dot] {
background: var(--away);
-webkit-mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
mask: radial-gradient(circle at 30% 28%,transparent 45%,#000 47%);
}
[data-status="busy"] [data-dot] {
background: linear-gradient(0deg,var(--busy) 32%,oklch(0.99 0 0) 33% 67%,var(--busy) 68%) no-repeat center/64% 100%,var(--busy);
}
[data-status="offline"] [data-dot] {
background: transparent;
box-shadow: inset 0 0 0 2.5px var(--off);
}
.ctc-03__roster {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 14px;
}
.ctc-03__label {
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mut);
padding: 4px 10px 10px;
}
.ctc-03__list {
list-style: none;
display: flex;
flex-direction: column;
}
.ctc-03__list li {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 10px;
border-radius: 12px;
transition: background .18s;
}
.ctc-03__list li:hover {
background: oklch(1 0 0/.06);
}
.ctc-03__list b {
font-size: 14px;
font-weight: 700;
flex: 1;
}
.ctc-03__list li[data-status="offline"] {
opacity: .6;
}
.ctc-03__state {
font-size: 11.5px;
font-weight: 700;
color: var(--mut);
background: oklch(1 0 0/.07);
border: 1px solid var(--line);
padding: 5px 11px;
border-radius: 99px;
}
[data-status="online"] .ctc-03__state {
color: var(--on);
border-color: color-mix(in oklch,var(--on) 35%,transparent);
background: color-mix(in oklch,var(--on) 12%,transparent);
}
[data-status="away"] .ctc-03__state {
color: var(--away);
border-color: color-mix(in oklch,var(--away) 35%,transparent);
background: color-mix(in oklch,var(--away) 12%,transparent);
}
[data-status="busy"] .ctc-03__state {
color: oklch(0.78 0.14 22);
border-color: color-mix(in oklch,var(--busy) 45%,transparent);
background: color-mix(in oklch,var(--busy) 15%,transparent);
}
.ctc-03__typing {
display: inline-flex;
gap: 3px;
align-items: center;
min-height: 26px;
padding: 0 12px;
border-radius: 99px;
background: color-mix(in oklch,var(--on) 12%,transparent);
border: 1px solid color-mix(in oklch,var(--on) 35%,transparent);
}
.ctc-03__typing i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--on);
animation: ctc03-bounce 1.1s ease-in-out infinite;
}
.ctc-03__typing i:nth-child(2) {
animation-delay: .15s;
}
.ctc-03__typing i:nth-child(3) {
animation-delay: .3s;
}
@keyframes ctc03-bounce {
30% {
translate: 0 -4px;
}
60% {
translate: 0 0;
}
}
.ctc-03__note {
font-size: 12.5px;
line-height: 1.6;
color: var(--mut);
border-left: 3px solid var(--on);
padding-left: 12px;
max-width: 60ch;
}
@container ctc03 (width < 560px) {
.ctc-03__hero {
justify-content: flex-start;
gap: 18px;
}
.ctc-03__state {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ctc-03 [data-dot]::after {
animation: none;
opacity: 0;
}
.ctc-03__typing i {
animation: none;
}
}
```How this works
The dot is an <i> absolutely positioned on the avatar's bottom-right, and the entire trick that makes it look native is a border the exact color of the surface behind it — the classic "cutout" illusion:
.avatar [data-dot]{ position:absolute; right:-3px; bottom:-3px;
width:15px; height:15px; border-radius:50%;
border:3px solid var(--panel); /* ← match the surface behind it */ }Each state is pure CSS geometry on that one element. Online gets an ::after that scales from .6 to 1.9 while fading — a sonar ping (paused under reduced motion). Away is the interesting one: a crescent is just a circle with another circle bitten out, so mask: radial-gradient(circle at 30% 28%, transparent 45%, #000 47%) carves a moon from the amber dot with zero extra markup. Busy paints its white dash with a centered linear-gradient, and offline hollows out via inset box-shadow.
State lives on the row, not the dot: <li data-status="away"> themes the dot, the trailing label chip and the row's opacity in one attribute — exactly how you would bind presence data. Change one attribute from your WebSocket handler and CSS does the rest. The typing chip is three <i>s sharing a staggered animation-delay, the same rhythm iMessage uses.
Make it yours
- Dot size scales with the avatar: set
width:24%; aspect-ratio:1on the dot instead of px to make it proportional at any avatar size (the hero row here is 64px, the roster 40px — same CSS). - Surface color: the cutout border is
var(--panel)— if avatars sit on a different background, override--panelon that container, not the dot. - Add a state: one
[data-status="…"] [data-dot]rule (color + optional shape) and one label rule. The markup never changes. - Ping cadence: the sonar runs 2.2s; tighten to 1.4s for “active now” urgency, or delete the ::after for a calmer product.
Gotchas — read before shipping
- A dot glued on top without the matching border reads as a sticker and fails on photo avatars — the border must equal the surface color behind the avatar, not the avatar's own color.
- Color alone is not state for color-blind users: this demo pairs every dot with a text chip and a distinct shape (crescent, dash, hollow). Keep at least one non-color signal.
- The crescent needs both -webkit-mask and mask for Safari; if it renders as a plain amber circle, the prefix is missing.
- Animating box-shadow for the ping jank-scales on long lists — the ::after scale/opacity ping is compositor-friendly and handles 500 rows.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Positioning, borders and keyframes are ancient and universal; the floor is oklch()/color-mix() plus mask (universal since 2023, prefixed for older Safari). Without mask the away dot is a plain amber circle — degraded, still legible via its label.