← Back to gallery
CSS

Fixed Grid Cell Overlay Pulse

A fixed CSS grid keeps every cell in its original track while an absolutely positioned overlay pulse highlights one active cell. Three expression methods separate outline ring, inner glow, and shadow halo treatments before mapping them to product contexts.

fixed-gridoverlay-pulsecss-gridactive-cellno-layout-shiftz-indexprefers-reduced-motion

Feedback / Fixed Grid Cue

Fixed Grid Cell Overlay Pulse

A fixed grid highlights one active cell with a restrained overlay pulse. Three expression methods compare outline ring, inner glow, and shadow halo first, then apply distinct product contexts to make each method easier to read.

Contribution calendarCommits9 today

Contribution calendar

Outline Ring Pulse

Outer ring scales around today's fixed commit cell while the heatmap data stays unchanged.

  • outline pulse
  • commits
  • fixed grid
Habit streakPractice42 min

Habit streak

Inner Glow Pulse

Inset glow changes perceived completion intensity without adding an outside halo.

  • inner glow
  • habit streak
  • stable cell
Team uptimeChecks1 alert

Team uptime

Shadow Halo Pulse

Bounded shadow halo radiates around today's uptime cell for stronger scanning.

  • shadow halo
  • uptime
  • reduced-motion

Heatmap inspector

Outline Ring Pulse

Contribution calendarCommits9 today
  • outline pulse
  • commits
  • fixed grid

A contribution calendar uses an outer border pseudo-element that scales beyond today's cell. This keeps commit density stable while making the current date explicit.

Helped you ship something? 🐟 Send my cat a churu

.activity-heatmap {
  --today-color: #34d399;
  --today-soft: rgba(52, 211, 153, 0.22);
  --pulse-duration: 2.2s;
  --ring-scale: 1.29;
  --core-scale: 1.14;
  --shadow-size: 25px;
  display: inline-grid;
  gap: 0.75rem;
  padding: 1rem;
  border-radius: 0.75rem;
  background: #0f172a;
  color: #e5edf8;
  font: 600 0.9rem/1.35 system-ui, sans-serif;
}
.activity-heatmap__grid {
  display: grid;
  grid-template-columns: repeat(7, 1rem);
  gap: 0.3rem;
}
.activity-heatmap__cell {
  position: relative;
  width: 1rem;
  aspect-ratio: 1;
  border-radius: 0.25rem;
  background: #1f2937;
}
.activity-heatmap__cell[data-level="1"] { background: #064e3b; }
.activity-heatmap__cell[data-level="2"] { background: #047857; }
.activity-heatmap__cell[data-level="3"] { background: #10b981; }
.activity-heatmap__cell[data-level="4"] { background: #86efac; }
.activity-heatmap__cell--today::before,
.activity-heatmap__cell--today::after {
  content: "";
  position: absolute;
  border-radius: inherit;
  pointer-events: none;
}
.activity-heatmap--outline .activity-heatmap__cell--today::before {
  inset: -0.25rem;
  border: 2px solid var(--today-color);
  animation: activity-heatmap-outline-ring var(--pulse-duration) ease-in-out infinite;
}
.activity-heatmap--outline .activity-heatmap__cell--today::after {
  content: none;
}
@keyframes activity-heatmap-outline-ring {
  0%, 100% { opacity: .45; transform: scale(.94); box-shadow: none; }
  50% { opacity: 1; transform: scale(var(--ring-scale)); box-shadow: 0 0 var(--shadow-size) var(--today-soft); }
}
@media (prefers-reduced-motion: reduce) {
  .activity-heatmap__cell--today::before,
  .activity-heatmap__cell--today::after {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

How to make this

A fixed grid cell overlay pulse keeps grid tracks fixed and animates only absolutely positioned pseudo-elements on the active square, so the cue stands out without layout shift.

html
<figure class="activity-heatmap" aria-labelledby="activity-title">  <figcaption id="activity-title">    Grid status  </figcaption>5  <div class="activity-heatmap__grid" role="img" aria-label="Fixed grid with one active cell highlighted.">    <span class="activity-heatmap__cell" data-level="1" aria-hidden="true"></span>    <span class="activity-heatmap__cell" data-level="3" aria-hidden="true"></span>    <span class="activity-heatmap__cell" data-level="0" aria-hidden="true"></span>    <span class="activity-heatmap__cell" data-level="2" aria-hidden="true"></span>10    <time class="activity-heatmap__cell activity-heatmap__cell--today" datetime="2026-05-24" data-level="4" aria-current="date" aria-label="Active cell"></time>  </div></figure> <style>.activity-heatmap {  --today-color: #34d399;  --today-soft: rgba(52, 211, 153, 0.24);  --pulse-duration: 1.8s;  display: inline-grid;  gap: 0.75rem;  padding: 1rem;  border-radius: 0.75rem;  background: #0f172a;  color: #e5edf8;  font: 600 14px/1.4 system-ui, sans-serif;}.activity-heatmap__grid {  display: grid;29  grid-template-columns: repeat(5, 18px);  gap: 5px;}.activity-heatmap__cell {  position: relative;  width: 18px;  aspect-ratio: 1;  border-radius: 4px;  background: #1f2937;}.activity-heatmap__cell[data-level="1"] { background: #064e3b; }.activity-heatmap__cell[data-level="2"] { background: #047857; }.activity-heatmap__cell[data-level="3"] { background: #10b981; }.activity-heatmap__cell[data-level="4"] { background: #86efac; }.activity-heatmap__cell--today { z-index: 1; }44.activity-heatmap__cell--today::before,.activity-heatmap__cell--today::after {  content: "";  position: absolute;  inset: -4px;  border-radius: inherit;  pointer-events: none;}.activity-heatmap__cell--today::before {  border: 2px solid var(--today-color);  animation: active-cell-ring var(--pulse-duration) ease-in-out infinite;}.activity-heatmap__cell--today::after {  inset: 2px;  background: var(--today-soft);  animation: active-cell-core var(--pulse-duration) ease-in-out infinite;}@keyframes active-cell-ring {  0%, 100% { opacity: .45; transform: scale(.94); box-shadow: none; }  50% { opacity: 1; transform: scale(1.3); box-shadow: 0 0 24px var(--today-soft); }}@keyframes active-cell-core {  0%, 100% { opacity: .35; transform: scale(1); }  50% { opacity: .78; transform: scale(1.14); }}69@media (prefers-reduced-motion: reduce) {  .activity-heatmap__cell--today::before,  .activity-heatmap__cell--today::after {    animation: none;    opacity: 1;    transform: none;  }}</style>

Annotated snippet

  1. Line 5The grid is exposed as one labeled image. Individual historical cells are not useful speech output, so the text label carries the daily summary.
    PitfallShould every heatmap cell be announced individually?

    Usually no. Dense heatmaps become noisy when every square is exposed as separate speech output. Use a concise role="img" label for the visual grid, then expose important state such as the active cell with real text or a focused detail elsewhere.

  2. Line 10The current date uses a real time element with aria-current="date". This separates "today" semantics from the visual pulse.
    PitfallWhy use aria-current="date" on the active cell?

    aria-current="date" communicates that this item represents the current date, while the pulse is only visual emphasis. It also keeps the HTML meaningful if the animation is disabled or stripped from a copied snippet.

  3. Line 29Fixed grid tracks keep every square in the same box while the marker animates. Do not animate grid-template-columns, gap, width, or height for this cue.

    Height changes push the UI below the heatmap; animating an overlay keeps the neighboring UI locked.

    PitfallWhy not pulse the actual heatmap cell size?

    Changing width, height, gap, or grid tracks makes nearby cells move and creates layout shift. Keep the cell box fixed and animate only transform or opacity on an overlay layer.

  4. Line 44The active cell motion lives in pseudo-elements. The underlying cell color stays stable, and the pulse layer can scale beyond the square without changing layout.
    PitfallAre pseudo-elements better than an extra wrapper for this pulse?

    Pseudo-elements keep the markup small and make it clear that the pulse is decorative. An extra element can work too, but it should still be positioned absolutely and excluded from layout.

  5. Line 69Reduced motion freezes the pulse into a static outline and fill. The active cell remains findable without an infinite loop.
    PitfallWhat should prefers-reduced-motion do for a active-cell pulse?

    Stop the loop and keep a static outline or fill on the current cell. Reduced motion should remove repeated movement, not remove the information that today is special.

Other pitfalls

How do I keep the pulse above neighboring cells?
Raise only the active cell inside a local stacking context, and keep the overlay on that cell or its pseudo-elements. A small local z-index is enough when the grid owns the context; avoid global values such as 9999, and check that parent overflow does not clip the ring.
Can I pulse many grid cells at once?
Use restraint. One active, current, or focused cell is usually the point of the pattern. Dozens of simultaneous outline, glow, or shadow pulses can create visual noise and extra paint work, so large grids should reserve animation for the most important state and leave the rest static.

Notes

Overview

A fixed-grid cell overlay pulse is an emphasis pattern for a dense matrix where the data layout must stay calm. The grid tracks define the information structure; the pulse is only an emphasis layer. That distinction matters. If the highlighted square grows, the row grows with it. If a pseudo-element grows above the square, the grid keeps its rhythm and the user sees exactly one active state.

When to use it

Use this for heatmaps, availability grids, calendar cells, badge matrices, seat maps, and compact status boards where one cell needs attention without re-sorting the user's mental map. It works best when the highlighted item means current, selected, focused, recently changed, or needs review. Avoid it when many cells need equal emphasis; a constantly pulsing grid turns into noise and makes comparison harder.

How it works

The grid keeps fixed columns, fixed gaps, and stable square cells. The active cell gets position: relative and a local z-index, then ::before and ::after draw the visible effect. The outline ring preset scales a border layer, the inner glow preset breathes inside the square, and the shadow halo preset expands light outside the cell. In every case, the base cell remains the same size; only overlay paint, opacity, and transform change.

Production gotchas

Do not animate grid-template-columns, gap, width, or height on the grid item unless you want neighboring UI to move. The compare demo intentionally shows the bad version by changing the target cell's height; the corrected version keeps the target box fixed and moves the pulse layer instead. Keep the active cell inside a local stacking context so the ring sits above adjacent cells without resorting to global values such as z-index: 9999.

Accessibility

Dense grids should not announce every decorative cell. Give the whole visual a concise accessible name, then expose the meaningful active state through text, aria-currentwhere appropriate, or a focused detail panel. Under prefers-reduced-motion: reduce, stop the loop and keep a static outline or fill so the important cell remains visible without repeated brightness or scale changes.

References

Implementation depth

A fixed-grid pulse should never resize the grid. The tracks, gap, and cell dimensions are the layout contract, so the emphasis belongs on absolutely positioned pseudo-elements that can scale and fade without moving neighboring cells.

The showcase presets compare expression methods first: outline ring, inner glow, and shadow halo. Once those methods are distinct, each one can carry a different domain context, so long as color and labels support the method instead of replacing it.

Semantics depend on the domain that uses the grid. Dense cells are often decorative as individual nodes, but the active cell still needs a stable text or state source. Reduced motion should keep the marker visible as a static cue instead of removing the information.