边框灯光环绕动画特效实现指南

边框灯光环绕动画特效实现指南

那个让用户一眼就注意到的重要元素,到底是怎么用纯 CSS 做出来的?其实也不难,就是绕了个弯子罢了。这篇文章带你从零开始实现边框灯光环绕动画,也顺带聊聊我们在 HagiCode 项目里踩过的那些坑。

背景

做前端的同学应该都有过这样的经历:产品经理跑过来,脸上挂着那种"这需求很简单"的表情------"这个正在运行的任务,能不能加个特效让用户一眼就能看到?"

你说行啊,加个边框变色呗。结果对方摇摇头,眼神里透着一种"你不懂"的意味:"不够明显,要那种灯光绕着边框跑的效果,跟科幻电影里一样。"

这时候你可能就会犯嘀咕:这玩意儿怎么实现?用 Canvas?用 SVG?还是说 CSS 能搞?毕竟谁也不想承认自己不会嘛。

其实啊,边框灯光环绕动画在现代 Web 应用中特别常见,主要用在这么几个场景:

  • 状态指示:标记正在进行的任务或活跃的项目
  • 视觉焦点:突出显示重要的内容区域
  • 品牌增强:营造科技感和现代感的视觉体验
  • 节日主题:配合特殊节日创建庆祝氛围

我们做 HagiCode 的时候就遇到过这个需求------用户需要一眼看出哪些会话正在运行、哪些提案正在处理中。试了好几种方案,有些路好走一点,有些路稍微曲折一点罢了,最后沉淀出了一套还算成熟的实现思路。

关于 HagiCode

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个 AI 驱动的代码助手项目,在界面中大量使用边框灯光动画来指示各种运行状态。比如会话列表的运行状态、提案流程图的状态过渡、吞吐量指示器的强度显示等等。

其实这些效果说起来也不复杂,就是做的时候踩了不少坑。如果你想看看实际效果,可以访问我们的 GitHub 仓库 或者直接去 官网 了解一下,毕竟能用的才是最好的嘛。

核心实现思路

经过对 HagiCode 代码的分析,我们总结出了下面几种核心的实现模式,每种都有它适用的场景,或者说,每种都有它存在的意义罢了。

1. Conic Gradient 旋转光晕(最常用)

这是最经典的边框灯光环绕实现方式,核心思路是用 CSS 的 conic-gradient 创建一个圆锥渐变,然后让它转起来。就像夜晚的路灯,一直在那里转啊转的。

关键要素:

  • ::before 伪元素创建光晕层
  • conic-gradient 定义渐变色分布
  • ::after 伪元素遮罩中心区域(可选)
  • @keyframes 实现旋转动画

2. 侧边发光线条

这个适用于列表项的状态指示,在元素的一侧创建发光的细线条就行,不用整个边框都动。毕竟有时候,一点光就够了,不需要照亮整个世界。

关键要素:

  • 绝对定位的细线元素
  • box-shadow 创建发光效果
  • scaleopacity 实现呼吸动画

3. Box-Shadow 发光背景

如果不需要环绕效果,只是想要个柔和的背景光晕,那用多层 box-shadow 叠加就够了。有些东西,简单点反而更好。

4. 无障碍访问支持

这个容易被忽略,但特别重要。所有动画都应该考虑 prefers-reduced-motion 媒体查询,给不喜欢动画的用户提供一个静态替代方案。毕竟不是所有人都喜欢动来动去的,尊重每个人的选择才是对的。

实现方案

方案一:Conic Gradient 旋转边框(推荐)

这是最完整的环绕灯光效果实现,也是 HagiCode 里用得最多的方案。毕竟,如果一样东西好用,为什么还要换呢?

css 复制代码
/* 父容器 */
.glow-border-container {
  position: relative;
  overflow: hidden;
}

/* 旋转的光晕层 */
.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;
}

/* 遮罩层(可选,用于创建空心边框效果) */
.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);
  }
}

这个方案的原理挺简单的:创建一个比父容器大的伪元素,上面画一个圆锥渐变,然后让它不停旋转。父容器设置 overflow: hidden,所以只能看到边框那一部分的光在转。就像我们在窗子里看外面的路灯,只能看到它经过的那一小段罢了。

方案二:简化版旋转光边框

如果你不需要那么复杂的效果,HagiCode 里有个更轻量的工具类实现。毕竟简单点,有时候反而更好。

css 复制代码
/* 旋转光边框工具类 */
.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);
  }
}

/* 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
  .running-light-border {
    animation: none;
  }
}

注意这里的 will-change: transform,这是告诉浏览器"这个元素要一直变",浏览器就会提前做些优化,动画会更流畅。毕竟提前准备,总比临时抱佛脚强嘛。

方案三:侧边发光线条

列表项的状态指示用这个特别合适,HagiCode 的会话列表就是用的这个方案。一条细线,却能在众多项目中脱颖而出,这不也是一种生活哲学吗?

css 复制代码
.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);
  }
}

这里用了 isolation: isolate 创建一个新的层叠上下文,然后用 z-index 控制各层的显示顺序。pointer-events: none 也很关键,不然那个伪元素会挡住用户的点击操作。就像有些东西,好看是好看,但是不能碍事才行。

方案四:React 组件封装

如果你项目里用 React,可以封装一个组件来处理这些逻辑,特别是无障碍访问的部分。毕竟代码写一次,用很多次,这才是我们想要的嘛。

tsx 复制代码
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>
    );
  }
);

对应的 CSS 模块:

css 复制代码
/* GlowBorder.module.css */

/* 动画版本 */
.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;
}

/* 静态版本(无障碍) */
.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);
  }
}

framer-motionuseReducedMotion hook 会自动检测用户的系统偏好,如果用户开启了"减弱动态效果",就会返回 true,这时候就显示静态版本。毕竟,尊重用户的选择比强行展示更重要。

实践经验分享

下面这些是我们在做 HagiCode 时踩过坑、总结出来的经验。其实也就是些碎碎念罢了,希望能帮到后来的你。

1. 主题变量系统

用 CSS 变量实现多主题支持特别方便。毕竟谁也不想每次切换主题都要改一堆代码呢?

css 复制代码
: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);
}

/* 使用 */
.glow-effect {
  background: var(--theme-glow-color);
  box-shadow: 0 0 20px var(--theme-glow-color);
}

这样切换主题的时候只需要改一下 html 标签的 class,所有动画颜色都会自动更新。一套代码,两种风格,这不就是我们追求的吗?

2. 性能优化

使用 will-change 提示浏览器优化:

css 复制代码
.animated-glow {
  will-change: transform, opacity;
}

提前告诉浏览器,它就会帮你做些优化。就像生活中的很多事情,提前准备总是好的。

避免在大面积元素上使用复杂的 box-shadow:

css 复制代码
/* 不好 - 大面积元素上使用模糊阴影 */
.large-card {
  box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
}

/* 更好 - 使用伪元素限制发光区域 */
.large-card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 20px var(--glow-color);
  pointer-events: none;
}

我们在 HagiCode 里测试过,在大卡片上直接加模糊阴影会让滚动帧率掉到 30fps 以下,改用伪元素后就稳稳 60fps 了。这种体验上的差异,用户是能感觉到的。

3. 无障碍访问

这个真的不能省,有些用户会觉得动画很晕或者很吵,尊重他们的选择是做产品的基本素养。毕竟美的事物不必强加于人嘛。

CSS 媒体查询:

css 复制代码
@media (prefers-reduced-motion: reduce) {
  .glow-animation {
    animation: none;
  }

  .glow-animation::before {
    /* 提供静态替代方案 */
    opacity: 1;
  }
}

React 中检测用户偏好:

tsx 复制代码
import { useReducedMotion } from 'framer-motion';

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

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

4. 强度级别控制

HagiCode 里的 Token 吞吐量指示器会根据实时吞吐量显示不同颜色的灯光,这个是动态实现的。毕竟不同的状态,应该有不一样的表达方式。

tsx 复制代码
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),
      }}
    />
  );
};

5. 注意事项

有些细节还是要注意的,不然踩了坑才知道就晚了。

注意事项 说明
z-index 管理 光晕层应设置合适的 z-index,避免影响内容交互
pointer-events 光晕伪元素应设置 pointer-events: none
边界溢出 父容器需要设置 overflow: hidden 或调整伪元素尺寸
性能影响 复杂动画在移动设备上可能影响性能,需要测试
深色模式 确保发光颜色在深色背景下清晰可见
主题切换 使用 CSS 变量确保主题切换时动画颜色正确更新

6. 调试技巧

伪元素在开发者工具里有时候不太好找,可以临时加个边框来看看位置。

css 复制代码
/* 临时显示伪元素边界用于调试 */
.glow-effect::before {
  /* debug: border: 1px solid red; */
}

调好位置之后记得把这行注释掉或者删掉,不然生产环境会很尴尬。有些东西,还是留在开发环境比较好。

总结

边框灯光环绕动画说难不难,说简单也不简单。核心就是 conic-gradient 加旋转,但要做到性能好、可维护、无障碍友好,还是有不少细节要注意的。

HagiCode 在这个上面踩了不少坑,也总结出了一些最佳实践。其实做项目就是这样,一遍遍试错,一遍遍改进。如果你在做类似的需求,希望这篇文章能帮你少走点弯路。

毕竟,有些东西,还是要亲自实践才知道深浅。

参考资料

原文与版权说明

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。

本内容采用人工智能辅助协作,最终内容由作者审核并确认。