Back to Code Bytes
3 min read
Notification Badge

Description

Notification Badge places a small count indicator on top of a trigger such as a bell icon or inbox button. When the badge appears it slides in diagonally and pops with an elastic scale, drawing the userโ€™s eye without moving the trigger itself. Dismissal plays quickly so it never blocks the UI.

Example

CSS

:root {
  --badge-slide-dur: 260ms;
  --badge-pop-dur: 500ms;
  --badge-pop-close-dur: 180ms;
  --badge-fade-dur: 400ms;
  --badge-fade-close-dur: 180ms;
  --badge-blur: 2px;
  --badge-offset-x: -8.2px;
  --badge-offset-y: 12.4px;
  --badge-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1);
  --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes t-badge-slide-in {
  from {
    transform: translate(var(--badge-offset-x), var(--badge-offset-y));
  }
  to {
    transform: translate(0, 0);
  }
}

/* .t-badge is the absolutely-positioned wrapper for the dot.
   Adjust top/right (or left/bottom) to anchor it on your trigger. */
.t-badge {
  position: absolute;
  top: -6px;
  right: -8px;
  pointer-events: none;
  will-change: transform;
}
.t-badge[data-open="true"] {
  animation: t-badge-slide-in var(--badge-slide-dur) var(--badge-slide-ease);
}

.t-badge-dot {
  display: block;
  transform-origin: center;
  transform: scale(1);
  opacity: 1;
  filter: blur(0);
  transition:
    transform var(--badge-pop-dur) var(--badge-pop-ease),
    opacity var(--badge-fade-dur) var(--badge-pop-ease),
    filter var(--badge-pop-dur) var(--badge-pop-ease);
  will-change: transform, opacity, filter;
}
.t-badge[data-open="false"] .t-badge-dot {
  transform: scale(0);
  opacity: 0;
  filter: blur(var(--badge-blur));
  transition:
    transform var(--badge-pop-close-dur) var(--badge-close-ease),
    opacity var(--badge-fade-close-dur) var(--badge-close-ease),
    filter var(--badge-pop-close-dur) var(--badge-close-ease);
}

@media (prefers-reduced-motion: reduce) {
  .t-badge,
  .t-badge-dot {
    animation: none !important;
    transition: none !important;
  }
}

React

import { useState } from "react";
import { Bell } from "lucide-react";
import "./notification-badge.css"; // paste the CSS above

export function NotificationBadge() {
  const [open, setOpen] = useState(true);
  return (
    <button style={{ position: "relative" }}>
      <Bell />
      <span className="t-badge" data-open={open ? "true" : "false"}>
        <span className="t-badge-dot">3</span>
      </span>
    </button>
  );
}

Variables

VariableDefaultNotes
--badge-slide-dur260mssourced from --p1-pos-open-dur
--badge-pop-dur500mssourced from --p1-scale-open-dur
--badge-pop-close-dur180mssourced from --p1-scale-close-dur
--badge-fade-dur400mssourced from --p1-opacity-open-dur
--badge-fade-close-dur180mssourced from --p1-opacity-close-dur
--badge-blur2pxsourced from --p1-blur
--badge-offset-x-8.2pxsourced from --p1-distance-x
--badge-offset-y12.4pxsourced from --p1-distance-y
--badge-slide-easecubic-bezier(0.22, 1, 0.36, 1)sourced from --p1-ease-pos-open
--badge-pop-easecubic-bezier(0.34, 1.36, 0.64, 1)sourced from --p1-ease-scale-open
--badge-close-easecubic-bezier(0.4, 0, 0.2, 1)sourced from --p1-ease-close

Credit

Adapted from Notification Badge on transitions.dev by Jakub Antalik. Original source: github.com/Jakubantalik/transitions.dev.