DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

Guide to Implementing Border Light Surround Animation Effects

Guide to Implementing Border Light Surround Animation Effects

That important element that catches users' attention at first glance—how is it actually made with pure CSS? It's not that difficult, just takes a bit of a roundabout approach. This article walks you through implementing border light surround animation from scratch, and also shares some of the pitfalls we encountered in the HagiCode project.

Background

Frontend developers have likely all had this experience: a product manager comes over with that "this requirement is simple" look on their face—"Can you add a special effect so users can see at a glance which tasks are running?"

You say sure, let's add a border color change. But they shake their head, with a look that says "you don't get it"—"Not obvious enough. It needs that light-circling-the-border effect, like in sci-fi movies."

At this point you might be wondering: How do you implement this? Canvas? SVG? Or can CSS handle it? After all, nobody wants to admit they don't know how.

Actually, border light surround animation is quite common in modern web applications, mainly used in these scenarios:

  • Status indication: Marking running tasks or active items
  • Visual focus: Highlighting important content areas
  • Brand enhancement: Creating a tech-savvy and modern visual experience
  • Thematic events: Creating celebratory atmospheres for special occasions

When we were building HagiCode, we encountered this requirement—users needed to see at a glance which sessions were running and which proposals were being processed. We tried several approaches; some paths were smoother, others a bit more winding, but we eventually settled on a fairly mature implementation approach.

About HagiCode

The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is an AI-driven code assistant project that makes extensive use of border light animations in the interface to indicate various running states. For example, the running status of the session list, state transitions in the proposal flow diagram, intensity display of the throughput indicator, and so on.

Actually, these effects aren't that complex to explain, but we definitely hit quite a few bumps along the way. If you want to see the actual效果, you can visit our GitHub repository or go directly to the official website to learn more—after all, what works is best.

Core Implementation Approach

Through analysis of the HagiCode codebase, we've summarized several core implementation patterns below, each with its applicable scenarios—or rather, each has its reason for existing.

1. Conic Gradient Rotating Glow (Most Common)

This is the most classic border light surround implementation approach. The core idea is to use CSS's conic-gradient to create a conic gradient, then make it spin. Like a streetlamp at night, just keeps spinning and spinning.

Key elements:

  • Use ::before pseudo-element to create the glow layer
  • Use conic-gradient to define the gradient color distribution
  • Use ::after pseudo-element to mask the center area (optional)
  • Use @keyframes to implement rotation animation

2. Side Glow Line

This applies to list item status indication—just create a glowing thin line on one side of the element, no need to animate the entire border. After all, sometimes a little light is enough, no need to illuminate the whole world.

Key elements:

  • Absolute-positioned thin line element
  • Use box-shadow to create the glow effect
  • Use scale and opacity to implement breathing animation

3. Box-Shadow Glow Background

If you don't need the surround effect and just want a soft background glow, layering multiple box-shadow values is sufficient. Some things are actually better when kept simple.

4. Accessibility Support

This is easily overlooked but particularly important. All animations should consider the prefers-reduced-motion media query, providing a static alternative for users who don't like animations. After all, not everyone likes things moving around—respecting everyone's choices is the right thing to do.

Implementation Solutions

Solution 1: Conic Gradient Rotating Border (Recommended)

This is the most complete surround light effect implementation and also the most used approach in HagiCode. After all, if something works well, why change it?

/* Parent container */
.glow-border-container {
  position: relative;
  overflow: hidden;
}

/* Rotating glow layer */
.glow-border-container::before {
  content: '';
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  background: conic-gradient(
    transparent 0deg,
    rgba(59, 130, 246, 0.6) 60deg,
    rgba(59, 130, 246, 0.3) 120deg,
    rgba(59, 130, 246, 0.6) 180deg,
    transparent 240deg
  );
  animation: border-rotate 3s linear infinite;
  z-index: -1;
}

/* Mask layer (optional, for creating hollow border effect) */
.glow-border-container::after {
  content: '';
  position: absolute;
  inset: 2px;
  background: inherit;
  border-radius: inherit;
  z-index: -1;
}

@keyframes border-rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

The principle of this solution is quite simple: create a pseudo-element larger than the parent container, draw a conic gradient on it, then make it spin continuously. The parent container sets overflow: hidden, so you only see the portion of light rotating at the border. It's like watching a streetlamp outside through a window—you can only see that small section as it passes by.

Solution 2: Simplified Rotating Light Border

If you don't need such a complex effect, HagiCode has a lighter utility class implementation. After all, simpler is sometimes better.

/* Rotating light border utility class */
.running-light-border {
  position: absolute;
  inset: -2px;
  background: conic-gradient(
    from 0deg,
    transparent 0deg 270deg,
    var(--theme-running-color) 270deg 360deg
  );
  border-radius: inherit;
  animation: lightRayRotate 3s linear infinite;
  will-change: transform;
  z-index: 0;
}

@keyframes lightRayRotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

/* Accessibility support */
@media (prefers-reduced-motion: reduce) {
  .running-light-border {
    animation: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note the will-change: transform here—this tells the browser "this element will keep changing," and the browser will do some optimizations in advance, making the animation smoother. After all, being prepared is better than scrambling at the last minute.

Solution 3: Side Glow Line

This is particularly suitable for list item status indication. HagiCode's session list uses this approach. A thin line that stands out among many items—isn't that also a life philosophy?

.side-glow {
  position: relative;
  isolation: isolate;
}

.side-glow::before {
  content: '';
  position: absolute;
  left: 0;
  top: 14px;
  bottom: 14px;
  width: 1px;
  border-radius: 999px;
  background: var(--theme-running-color);
  box-shadow:
    0 0 16px var(--theme-running-color),
    0 0 28px var(--theme-running-color);
  z-index: 1;
  pointer-events: none;
  animation: sidePulse 2.6s ease-in-out infinite;
}

.side-glow > * {
  position: relative;
  z-index: 2;
}

@keyframes sidePulse {
  0%, 100% {
    opacity: 0.55;
    transform: scaleY(0.96);
  }
  50% {
    opacity: 0.95;
    transform: scaleY(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we use isolation: isolate to create a new stacking context, then use z-index to control the display order of each layer. pointer-events: none is also crucial, otherwise the pseudo-element would block user click interactions. Like some things—nice to look at, but they shouldn't get in the way.

Solution 4: React Component Wrapper

If you use React in your project, you can wrap a component to handle this logic, especially the accessibility parts. After all, writing code once and using it many times—that's what we want.

import React from 'react';
import { useReducedMotion } from 'framer-motion';
import styles from './GlowBorder.module.css';

interface GlowBorderProps {
  isActive: boolean;
  children: React.ReactNode;
  className?: string;
}

export const GlowBorder = React.memo<GlowBorderProps>(
  ({ isActive, children, className = '' }) => {
    const prefersReducedMotion = useReducedMotion();

    if (!isActive) {
      return <div className={className}>{children}</div>;
    }

    if (prefersReducedMotion) {
      return (
        <div className={`${styles.glowStatic} ${className}`}>
          {children}
        </div>
      );
    }

    return (
      <div className={`${styles.glowAnimated} ${className}`}>
        {children}
      </div>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode

Corresponding CSS module:

/* GlowBorder.module.css */

/* Animated version */
.glowAnimated {
  position: relative;
  overflow: hidden;
}

.glowAnimated::before {
  content: '';
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  background: conic-gradient(
    from 0deg,
    transparent,
    rgba(59, 130, 246, 0.6),
    transparent,
    rgba(59, 130, 246, 0.6),
    transparent
  );
  animation: rotateGlow 3s linear infinite;
  z-index: -1;
}

.glowAnimated::after {
  content: '';
  position: absolute;
  inset: 2px;
  background: inherit;
  border-radius: inherit;
  z-index: -1;
}

/* Static version (accessibility) */
.glowStatic {
  position: relative;
  border: 1px solid rgba(59, 130, 246, 0.5);
  box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
}

@keyframes rotateGlow {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

framer-motion's useReducedMotion hook automatically detects the user's system preferences. If the user has enabled "reduce motion," it returns true, at which point the static version is displayed. After all, respecting user choices is more important than forcing an animation on them.

Practical Experience Sharing

Below are some experiences we summed up from the pitfalls we encountered while building HagiCode. Just some rambling, really, but hope it helps you who come after.

1. Theme Variable System

Using CSS variables for multi-theme support is particularly convenient. After all, nobody wants to modify a bunch of code every time they switch themes.

:root {
  --glow-color-light: rgb(16, 185, 129);
  --glow-color-dark: rgb(16, 185, 129);
  --theme-glow-color: var(--glow-color-light);
}

html.dark {
  --theme-glow-color: var(--glow-color-dark);
}

/* Usage */
.glow-effect {
  background: var(--theme-glow-color);
  box-shadow: 0 0 20px var(--theme-glow-color);
}
Enter fullscreen mode Exit fullscreen mode

This way, when switching themes you only need to modify the class on the html tag, and all animation colors will automatically update. One codebase, two styles—isn't that what we're after?

2. Performance Optimization

Use will-change to hint browser optimization:

.animated-glow {
  will-change: transform, opacity;
}
Enter fullscreen mode Exit fullscreen mode

Tell the browser in advance, and it will help with some optimizations. Like many things in life—being prepared is always good.

Avoid complex box-shadow on large area elements:

/* Bad - using blurred shadow on large area elements */
.large-card {
  box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
}

/* Better - use pseudo-element to limit glow area */
.large-card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 20px var(--glow-color);
  pointer-events: none;
}
Enter fullscreen mode Exit fullscreen mode

We tested this in HagiCode—adding blurred shadows directly on large cards dropped scroll frame rates below 30fps, but after switching to pseudo-elements it stayed at a steady 60fps. Users can feel this kind of experience difference.

3. Accessibility

This really can't be skipped. Some users find animations dizzying or noisy, and respecting their choices is basic product etiquette. After all, beautiful things shouldn't be forced on people.

CSS media query:

@media (prefers-reduced-motion: reduce) {
  .glow-animation {
    animation: none;
  }

  .glow-animation::before {
    /* Provide static alternative */
    opacity: 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

Detecting user preferences in React:

import { useReducedMotion } from 'framer-motion';

const Component = () => {
  const prefersReducedMotion = useReducedMotion();

  return (
    <div className={prefersReducedMotion ? 'static-glow' : 'animated-glow'}>
      Content
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

4. Intensity Level Control

The Token throughput indicator in HagiCode displays different colored lights based on real-time throughput. This is implemented dynamically. After all, different states should have different expressions.

const colors = [
  null,       // Level 0 - no color
  '#3b82f6',  // Level 1 - Blue
  '#34d399',  // Level 2 - Emerald
  '#facc15',  // Level 3 - Yellow
  '#fbbf24',  // Level 4 - Amber
  '#f97316',  // Level 5 - Orange
  '#22d3ee',  // Level 6 - Cyan
  '#d946ef',  // Level 7 - Fuchsia
  '#f43f5e',  // Level 8 - Rose
];

const IntensityGlow = ({ intensity }) => {
  const glowColor = colors[Math.min(intensity, colors.length - 1)];

  return (
    <div
      className="glow-effect"
      style={{
        '--glow-color': glowColor,
        opacity: 0.6 + (intensity * 0.08),
      }}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

5. Considerations

Some details still need attention, otherwise it'll be too late when you fall into a pit.

Consideration Description
z-index management Glow layer should have appropriate z-index to avoid affecting content interaction
pointer-events Glow pseudo-elements should have pointer-events: none
Boundary overflow Parent container needs overflow: hidden or adjust pseudo-element size
Performance impact Complex animations may affect performance on mobile devices, needs testing
Dark mode Ensure glow colors are clearly visible on dark backgrounds
Theme switching Use CSS variables to ensure animation colors update correctly on theme switch

6. Debugging Tips

Pseudo-elements are sometimes hard to find in DevTools. You can temporarily add a border to see the position.

/* Temporarily show pseudo-element boundary for debugging */
.glow-effect::before {
  /* debug: border: 1px solid red; */
}
Enter fullscreen mode Exit fullscreen mode

After adjusting the position, remember to comment out or delete this line, otherwise production will be awkward. Some things are better left in the development environment.

Summary

Border light surround animation isn't hard, but it isn't simple either. The core is conic-gradient plus rotation, but to achieve good performance, maintainability, and accessibility, there are quite a few details to pay attention to.

HagiCode hit quite a few bumps with this and also summarized some best practices. Actually, doing projects is like this—trial and error, iteration after iteration. If you're working on similar requirements, hope this article helps you take fewer detours.

After all, some things you have to experience yourself to know their depth.

References

Original Article & License

Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.

Top comments (0)