“魅力我的标记:冬至”这类前端挑战,重点往往不是把页面塞满节气素材,而是用干净的标记结构、少量样式和一组可控动画,做出一个能承载冬至意象的小型组件。本文实现的是一个圆形可点击标记:夜空背景里雪花缓慢旋转,碗中的汤圆被热气轻轻托起,鼠标或键盘点击后出现一句节气提示。整套组件只依赖原生HTML、CSS和少量JavaScript,不引入第三方库,适合作为活动页视觉锚点,也可以当作12月前端作品的加分细节。

一、用语义化结构搭出标记骨架
先把组件当成一个按钮来设计。按钮天然支持键盘聚焦和点击事件,比在普通容器上手动补 role、tabindex 更可靠。内部需要三个层次:负责背景渐变的装饰层、承载雪花和汤圆的 SVG 层、以及默认隐藏的提示文字。SVG 层单独放置,方便后续用 transform 做动画而不影响整体布局。
<button class="dongzhi-mark" aria-label="查看冬至提示" data-mark>
<span class="dongzhi-mark__sky"></span>
<svg class="dongzhi-mark__art" viewBox="0 0 200 200" aria-hidden="true">
<g class="snow-layer">
<path class="snowflake" d="M100 20 L104 37 L121 41 L104 45 L100 62 L96 45 L79 41 L96 37 Z" />
<path class="snowflake small" d="M62 62 L65 73 L76 76 L65 79 L62 90 L59 79 L48 76 L59 73 Z" />
<path class="snowflake small" d="M142 70 L145 80 L155 83 L145 86 L142 96 L139 86 L129 83 L139 80 Z" />
</g>
<g class="food-layer">
<path class="bowl" d="M58 126 Q100 146 142 126 L137 160 Q100 170 63 160 Z" />
<circle class="tangyuan t1" cx="84" cy="136" r="13" />
<circle class="tangyuan t2" cx="116" cy="136" r="13" />
<circle class="tangyuan t3" cx="100" cy="127" r="13" />
</g>
<g class="steam-layer">
<path class="steam s1" d="M86 116 C76 104 92 96 84 82" />
<path class="steam s2" d="M112 114 C102 102 120 94 110 78" />
</g>
</svg>
<span class="dongzhi-mark__hint">冬至快乐</span>
</button>
结构里所有装饰性 SVG 都加上 aria-hidden=true,避免屏幕阅读器逐个朗读路径。按钮本身用 aria-label 提供冬至提示的语义,data-mark 作为 JavaScript 选择器。提示文字 .dongzhi-mark__hint 默认放在视觉边界之外,展开时再淡入,这样不会影响圆形构图的完整感。
二、用CSS变量控制雪夜氛围
基础样式从圆形容器开始。width 和 border-radius 各占一半形成圆形,overflow:hidden 把所有超出边缘的装饰裁掉,isolation:isolate 创建独立堆叠上下文,防止后续动画的层叠顺序影响页面其他模块。背景使用 radial-gradient,而不是纯色,能让容器边缘更暗、中间微微透出夜色,看起来更有雪的反射感。
:root {
--dz-night-1: #0b1020;
--dz-night-2: #1c2d4a;
--dz-snow: #eef5ff;
--dz-hot: rgba(255, 190, 110, 0.8);
--dz-ring: 4px;
}
.dongzhi-mark {
position: relative;
width: 220px;
height: 220px;
border: none;
border-radius: 50%;
background: radial-gradient(circle at 30% 25%, var(--dz-night-2), var(--dz-night-1) 65%);
box-shadow: 0 20px 45px rgba(4, 8, 18, 0.45), inset 0 0 0 4px rgba(255,255,255,0.08);
cursor: pointer;
overflow: hidden;
isolation: isolate;
}
.dongzhi-mark:focus-visible {
outline: 3px solid #ffd9a0;
outline-offset: 6px;
}
自定义属性放在 :root 中,后续如果要把组件改成其他节气或主题,只需要替换几个颜色值。focus-visible 样式不能省,键盘用户按下 Tab 聚焦时需要清晰的轮廓。暗色背景下的标记轮廓可以用暖橙色,和冬至的汤圆、热气形成呼应,而不会显得突兀。
三、SVG绘制冬至角色:雪花、碗和热气
SVG 部分是组件的核心。手写路径听起来复杂,但可以把雪花简化为菱形和四条短线的组合,汤圆用三个普通圆形,热气用两条二次贝塞尔曲线。路径坐标不需要追求绝对精细,只要保持 viewBox 从 0 0 200 200 出发,几何中心就是 100,100,元素位置容易推算。比如雪花顶部尖角从 100 20 开始,两侧展开到 79 41 和 121 41,就可以得到对称的形状。
.dongzhi-mark__art {
position: absolute;
inset: 18px;
width: calc(100% - 36px);
height: calc(100% - 36px);
overflow: visible;
}
.snowflake {
fill: var(--dz-snow);
opacity: 0.86;
}
.bowl {
fill: #6e3b2f;
stroke: #d9a066;
stroke-width: 3;
}
.tangyuan {
fill: #f5e6d3;
stroke: #c9946c;
stroke-width: 2;
filter: drop-shadow(0 2px 2px rgba(0,0,0,0.25));
}
.steam {
fill: none;
stroke: var(--dz-hot);
stroke-width: 5;
stroke-linecap: round;
opacity: 0.7;
}
绘制完成后,样式阶段用 fill 和 stroke 区分材质。雪花填充接近白色的蓝色,汤圆使用浅米色并加上 2px 描边,碗用深棕色填充、暖褐色描边,热气不填充,只保留圆头线条。为 SVG 子元素使用 transform-box:fill-box,可以保证后续旋转动画围绕每片雪花自身中心,而不是围绕整个 SVG 画布原点,这是最容易踩的坐标坑。
四、动画与交互:保持克制才有节气感
动画不必太复杂。雪花旋转用 transform 即可,浏览器可以在合成线程处理,不触发重排;热气上升使用 stroke-dashoffset 和 opacity,虽然会重绘,但热气线条很短,消耗很小。两个动画一个慢一个快,形成空间层次。需要注意,雪花的 transform-origin 必须配合 transform-box:fill-box,否则旋转会偏移到容器角落。
.snowflake {
animation: snow-spin 16s linear infinite;
transform-origin: center;
transform-box: fill-box;
}
.snowflake.small {
animation-duration: 22s;
animation-direction: reverse;
}
.steam {
stroke-dasharray: 60;
stroke-dashoffset: 60;
animation: steam-rise 2.6s ease-in-out infinite;
}
.steam.s2 {
animation-delay: 1.3s;
}
@keyframes snow-spin {
to { transform: rotate(360deg); }
}
@keyframes steam-rise {
0% { stroke-dashoffset: 60; opacity: 0.7; }
60% { opacity: 0.9; }
100% { stroke-dashoffset: -60; opacity: 0; }
}
交互层面,点击按钮时切换 data-open 状态,同时给提示文字加上 is-visible 类,并用 aria-expanded 告诉屏幕阅读器当前是展开还是收起。提示文字可以用 opacity 和 translateY 做淡入下移,再配合一个短的过渡,让状态切换不至于太生硬。不要用 setTimeout 自动关闭,用户心理上更倾向于手动收放,尤其是移动端。
const mark = document.querySelector('[data-mark]');
const hint = mark?.querySelector('.dongzhi-mark__hint');
if (mark && hint) {
mark.addEventListener('click', () => {
const isOpen = mark.dataset.open === 'true';
mark.dataset.open = String(!isOpen);
hint.classList.toggle('is-visible', !isOpen);
mark.setAttribute('aria-expanded', String(!isOpen));
});
}
.dongzhi-mark__hint {
position: absolute;
bottom: 22px;
left: 50%;
transform: translateX(-50%) translateY(8px);
opacity: 0;
pointer-events: none;
color: #ffe6c1;
font-size: 14px;
letter-spacing: 2px;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.dongzhi-mark__hint.is-visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
五、移动端与减少动效偏好
移动端下,标记会以更小的直径呈现,但触控区域仍建议保留至少 172px,这已经超过 44px 的推荐最小点击尺寸。媒体查询只调整宽高和描边粗细,不要单独缩小 SVG,因为视口缩放可能让雪花路径变得模糊。若页面空间紧张,可以把它放在卡片右上角,不参与文档流的主内容重排。
@media (max-width: 480px) {
.dongzhi-mark {
width: 172px;
height: 172px;
}
.steam {
stroke-width: 4;
}
}
@media (prefers-reduced-motion: reduce) {
.snowflake,
.steam {
animation: none;
}
.steam {
opacity: 0.55;
}
}
prefers-reduced-motion 是一项容易忽略但很加分的细节。对眩晕敏感的用户而言,持续旋转的雪花和上升热气可能造成不适。开启减少动效后,将所有 animation 置为 none,热气保留半透明静态状态,视觉氛围仍然成立。还可以用 prefers-color-scheme 为浅色背景提供一版日间配色,让组件不只在深色页面中可用。
.dongzhi-mark {
color-scheme: dark;
}
@media (prefers-color-scheme: light) {
.dongzhi-mark {
background: radial-gradient(circle at 30% 25%, #b7c9e2, #7f9bc4 70%);
box-shadow: 0 20px 45px rgba(26, 42, 68, 0.28);
}
.snowflake {
fill: #ffffff;
}
}
整套组件完成度足够直接用于作品集:原生结构、零依赖、可交互、具备可访问性降级。冬至主题只是外壳,替换 SVG 图案和配色后,同样能改成春节或其他节气标记。对12月前端挑战来说,重点不是堆叠多少效果,而是用清晰的结构和克制动画,让一个圆形标记也能讲清楚节气氛围。