← Back to gallery
CSS

Shape-aware Halo via drop-shadow

Halo follows an SVG shape's alpha silhouette via stacked filter: drop-shadow chains (NOT box-shadow, which only follows the bounding rectangle). Three shapes — a moon, a snowflake, and a leaf — each with a per-shape glow recipe.

drop-shadowfilter-chainalpha-silhouettehalosvg-shapeno-box-shadowprefers-reduced-motion

Alpha-aware glow · stacked drop-shadow

Shape-Aware Halo via drop-shadow

box-shadow follows the rectangular bounding box; filter: drop-shadow() follows the alpha silhouette. Three production-realistic icons each demonstrate a different motion archetype + a different alpha-edge feature — concave inner curve, 6-fold symmetric rotate, serrated edges with sway. 3-layer drop-shadow chains stay GPU-friendly while still showing the shape-aware halo idea.

Concave curve · warm pulse

Crescent Moon

A crescent carved out by subtracting one disc from another — the inner concave curve is part of the alpha silhouette, so the warm 4 → 5 layer halo chain wraps that hollow side just as cleanly as the outer rim.

  • concave curve
  • warm chain
  • slow breath

6-fold symmetry · ice rotate

Snowflake

Six identical arms arranged at 60° intervals, each composed of stacked stroked lines + tip dots. The whole flake rotates very slowly (60s) while the cool ice halo breathes on its own 4s cycle.

  • 6-fold symmetric
  • slow rotate
  • ice halo

Serrated edge · falling sway

Maple Leaf

Serrated maple leaf with sharp lobe notches. Sways ±3deg around its stem pivot on an 8s cycle while the autumn halo breathes independently at 5s — sway and breath drift in and out of phase.

  • serrated edge
  • pivot sway
  • autumn chain

Halo inspector

Crescent Moon

  • concave curve
  • warm chain
  • slow breath

A crescent carved out by subtracting one disc from another — the inner concave curve is part of the alpha silhouette, so the warm 4 → 5 layer halo chain wraps that hollow side just as cleanly as the outer rim.

Helped you ship something? 🐟 Send my cat a churu

/* Crescent Moon — 3-layer warm chain (cream → amber → ember). */
.halo-moon {
  /* Multi-layer drop-shadow chain animates over a 5.0s cycle.
     Intensity 1.00× scales every blur radius at the 50% peak;
     rest-state radii stay constant so the cycle still passes through
     the same baseline regardless of intensity. */
  animation: halo-moon 5.0s ease-in-out infinite;
}

/* Full keyframe definitions for all 13 variants live in the stage
   CSS — the drop-shadow chain length and color stops differ per
   variant. Halo cycles 5s ease-in-out infinite. 3 constant layers (cream / amber / ember). At 50% peak each radius roughly doubles and the outer ember stop extends to 38px. */

How to make this

A shape-aware halo uses filter: drop-shadow() on an SVG or transparent PNG, so the glow follows the element alpha silhouette instead of the rectangular CSS box.

html
1<figure class="halo-demo" aria-label="Crescent moon halo">  <svg class="halo-demo__art" viewBox="0 0 120 120" aria-hidden="true">    <path class="halo-demo__shape"4      d="M82 18A42 42 0 1 0 82 102A34 34 0 0 1 82 18Z" />  </svg>  <figcaption>drop-shadow follows the alpha edge</figcaption></figure> <style>.halo-demo {  display: grid;  justify-items: center;  gap: .75rem;  width: 220px;  padding: 1.5rem;  color: #fde68a;  background: radial-gradient(circle, #241638, #05030a);  border-radius: 20px;}.halo-demo__art {  width: 128px;  aspect-ratio: 1;23  overflow: visible;}.halo-demo__shape {  fill: #fff7cc;27  filter:28    drop-shadow(0 0 4px #fff7cc)    drop-shadow(0 0 14px #fbbf24)    drop-shadow(0 0 28px #fb923c);  animation: halo-drop-moon 4.8s ease-in-out infinite;}@keyframes halo-drop-moon {  0%, 100% {    filter:      drop-shadow(0 0 4px #fff7cc)      drop-shadow(0 0 14px #fbbf24)      drop-shadow(0 0 28px #fb923c);  }  50% {    filter:      drop-shadow(0 0 6px #fff7cc)      drop-shadow(0 0 22px #fbbf24)      drop-shadow(0 0 40px #fb923c);  }}.halo-demo figcaption { font: 600 .85rem/1.3 sans-serif; }48@media (prefers-reduced-motion: reduce) {  .halo-demo__shape { animation: none; }}</style>

Annotated snippet

  1. Line 1The figure gives the decorative SVG a visible text explanation. If the shape is only ornament, keep the SVG aria-hidden and let nearby text describe the effect.
  2. Line 4The crescent path has a concave cutout. That inner curve is part of the alpha silhouette, which is exactly the edge drop-shadow can follow and box-shadow cannot.
    PitfallWhy does box-shadow not follow my SVG icon shape?

    box-shadow belongs to the element box, so it uses the rectangular layout bounds. Use filter: drop-shadow() on the SVG, image, or transparent element when you want the glow to follow the visible alpha silhouette, including holes, cutouts, and irregular edges.

  3. Line 23overflow: visible prevents the outer glow from being clipped by the SVG viewport. Without it, the halo can flatten at the top or sides even though the filter is correct.

    Same drop-shadow filter on both. overflow: hidden on the SVG clips the glow at the viewBox edge — the halo flattens against an invisible wall. overflow: visible lets the same filter extend past the SVG box.

    PitfallHow do I stop a drop-shadow halo from being clipped?

    Check every clipping boundary: the SVG viewport, parent overflow, masks, and the preview card. Set overflow: visible on the SVG when possible, and leave enough padding around the shape for the largest blur radius. A correct filter can still look broken if the glow has no room.

  4. Line 27filter: drop-shadow reads the rendered alpha channel, not the layout rectangle. This is the line that makes the halo wrap the moon shape instead of the SVG box.

    box-shadow glows around the rectangular box; drop-shadow follows the crescent silhouette.

    PitfallWhy does box-shadow not follow my SVG icon shape?

    box-shadow belongs to the element box, so it uses the rectangular layout bounds. Use filter: drop-shadow() on the SVG, image, or transparent element when you want the glow to follow the visible alpha silhouette, including holes, cutouts, and irregular edges.

  5. Line 28Stacking several drop-shadow calls creates a near, mid, and outer halo. Keep the number of layers consistent between keyframes so interpolation stays smooth.
    PitfallIs filter: drop-shadow expensive to animate?

    It can be more expensive than transform or opacity because changing filter values affects pixels around the shape. Use a small number of layers, cap the largest blur radius, and avoid running many animated halos at once. For lists or grids, prefer static drop-shadows.

  6. Line 48Reduced motion freezes the breathing halo but leaves the static glow in place. The effect remains shape-aware without forcing a constant pulse.
    PitfallHow should a halo effect respect prefers-reduced-motion?

    Stop the pulse animation and keep a static halo. Removing the glow entirely can change the visual hierarchy, while leaving the animation running can be distracting. The reduced-motion branch should preserve the final composition without repeated brightness changes.

Other pitfalls

Which browsers support filter: drop-shadow on SVG?
Modern Chromium, Firefox, and Safari support CSS filters on SVG content, but clipping and filter-region behavior can vary. Test Safari when the SVG has a tight viewBox or sits inside overflow-hidden containers. A static unfiltered fill should still communicate the icon if the halo fails.

Notes

Overview

Halo glows that follow an SVG shape’s actual alpha silhouette — not the bounding rectangle — via stacked filter: drop-shadow() chains. box-shadow is rectangular by definition and cannot wrap a non-rectangular shape; drop-shadow runs on the alpha channel and gets the silhouette right.

When to use it

Reach for shape-aware halos on logo glow, icon focus affordances, and any decorative SVG that needs a contoured glow. Skip it on rectangular surfaces — box-shadow is cheaper there. Stack at most three drop-shadow layers; each layer is a separate rasterization pass and the budget compounds quickly on Retina.

How it works

Apply a chained filter: filter: drop-shadow(0 0 8px var(--accent)) drop-shadow(0 0 24px var(--accent)). Each drop-shadow() reads the element’s alpha channel and renders a blurred copy offset by the x/y values with the given blur radius and color — unlike box-shadow, which draws a rectangular shadow regardless of the visual silhouette. For irregular SVG shapes the difference is dramatic: drop-shadow wraps the actual outline; box-shadow draws around the bounding box. Stack two or three layers with progressively wider blur radii to build up a soft halo while keeping the tight inner ring crisp.

Production gotchas

Each drop-shadow layer in the chain is a separate rasterization pass — budget compounds quickly. Cap at three layers and keep the widest blur under ~30px on retina. Drop-shadow does not promote to its own compositor layer like box-shadow does, so animating the element underneath the filter triggers full repaints. If you need to animate the halo color, animate a CSS custom property the filter reads — do not animate the filter string itself, which forces re-parsing every frame. Watch for clipping by overflow: hidden ancestors that strip the halo edges.

Accessibility

The halo is purely decorative — do not rely on its presence to communicate state. Pair with an explicit text label or icon for any meaning. Under prefers-reduced-motion: reduce if the halo breathes or pulses, pin it at rest opacity. Verify the halo color does not reduce contrast of nearby content when the glow extends past the element bounds.

References

Implementation depth

drop-shadow follows the alpha silhouette, which is why it works for moons, leaves, and icons where box-shadow would only draw a rectangle. Stack smaller shadows before larger halos to keep the edge readable.

Every extra drop-shadow is another rasterization pass. Use the fewest layers that communicate focus or glow, and check high-contrast modes where decorative halo color may disappear or need a clearer outline.