#CSS Anchor 交互注释
使用 CSS Anchor Positioning 创建产品功能标注和代码注释,让内容更具交互性。
预览
annotations.demo.tsx
产品功能标注
超视网膜 XDR 显示屏
6.9 英寸 OLED 全面屏,支持 ProMotion 自适应刷新率技术,峰值亮度 2000 尼特
专业级相机系统
4800 万像素主摄 + 超广角 + 5 倍长焦,支持 ProRAW 和 ProRes 视频拍摄
A18 Pro 芯片
最新一代仿生芯片,6 核 CPU + 6 核 GPU + 16 核神经网络引擎
全天候续航
最长可达 29 小时视频播放,支持 MagSafe 15W 无线快充
点击标注点或下方按钮查看功能详情
代码行注释
greeting.js
1const greeting = (name) => {
箭头函数定义,接收 name 参数
2 const time = new Date().getHours();
获取当前小时数(0-23)
3 let message = '';
初始化问候语变量
4
5 if (time < 12) {
判断是否为上午
6 message = 'Good morning';
7 } else if (time < 18) {
判断是否为下午
8 message = 'Good afternoon';
9 } else {
10 message = 'Good evening';
11 }
12
13 return `${message}, ${name}!`;
模板字符串拼接问候语
14};
点击高亮行查看代码注释。注释框使用 CSS Anchor 精确定位到对应代码行。
import React, { useState } from 'react';
interface Annotation {
id: string;
x: number;
y: number;
title: string;
description: string;
color: 'blue' | 'emerald' | 'amber' | 'rose';
}
const productAnnotations: Annotation[] = [
{
id: 'screen',
x: 50,
y: 20,
title: '超视网膜 XDR 显示屏',
description: '6.9 英寸 OLED 全面屏,支持 ProMotion 自适应刷新率技术,峰值亮度 2000 尼特',
color: 'blue',
},
{
id: 'camera',
x: 25,
y: 15,
title: '专业级相机系统',
description: '4800 万像素主摄 + 超广角 + 5 倍长焦,支持 ProRAW 和 ProRes 视频拍摄',
color: 'emerald',
},
{
id: 'chip',
x: 50,
y: 55,
title: 'A18 Pro 芯片',
description: '最新一代仿生芯片,6 核 CPU + 6 核 GPU + 16 核神经网络引擎',
color: 'amber',
},
{
id: 'battery',
x: 50,
y: 75,
title: '全天候续航',
description: '最长可达 29 小时视频播放,支持 MagSafe 15W 无线快充',
color: 'rose',
},
];
const colorClasses = {
blue: { bg: 'bg-brand-5', ring: 'ring-brand-2', text: 'text-brand-6', cardBg: 'bg-brand-1', border: 'border-brand-3' },
emerald: { bg: 'bg-success', ring: 'ring-success-1', text: 'text-success', cardBg: 'bg-success-1', border: 'border-success-2' },
amber: { bg: 'bg-warning', ring: 'ring-warning-1', text: 'text-warning', cardBg: 'bg-warning-1', border: 'border-warning-2' },
rose: { bg: 'bg-error', ring: 'ring-error-1', text: 'text-error', cardBg: 'bg-error-1', border: 'border-error-2' },
};
function AnnotationPoint({ annotation, isActive, onClick }: { annotation: Annotation; isActive: boolean; onClick: () => void }) {
const colors = colorClasses[annotation.color];
const anchorName = `--anchor-point-${annotation.id}`;
const isOnLeft = annotation.x < 50;
return (
<>
<button
type="button"
onClick={onClick}
className={`rd-full absolute z-10 h-5 w-5 ${colors.bg} ring-1 ring-white/50 transition-all duration-300 hover:scale-125 ${isActive ? 'scale-125' : ''}`}
style={
{
left: `${annotation.x}%`,
top: `${annotation.y}%`,
transform: 'translate(-50%, -50%)',
anchorName: anchorName,
} as React.CSSProperties
}
>
<span className={`rd-full absolute inset-0 ${colors.bg} animate-ping opacity-50`} />
</button>
<div
className={`rd-xl fixed z-50 w-64 border p-4 shadow-xl transition-all duration-300 ${colors.cardBg} ${colors.border} ${
isActive ? 'scale-100 opacity-100' : 'pointer-events-none scale-90 opacity-0'
}`}
style={
{
positionAnchor: anchorName,
...(isOnLeft
? { left: 'anchor(right)', translate: '16px -50%' }
: { right: 'anchor(left)', translate: '-16px -50%' }),
top: 'anchor(center)',
positionTryFallbacks: 'flip-inline, flip-block',
} as React.CSSProperties
}
>
<div className={`absolute top-1/2 h-0.5 w-4 ${colors.bg} ${isOnLeft ? '-left-4' : '-right-4'} -translate-y-1/2`} />
<div className={`mb-1 font-semibold text-sm ${colors.text}`}>{annotation.title}</div>
<div className="text-gray-6! text-xs leading-relaxed">{annotation.description}</div>
</div>
</>
);
}
function ProductShowcase() {
const [activeAnnotation, setActiveAnnotation] = useState<string | null>('screen');
return (
<div className="rd-2xl relative overflow-hidden bg-gradient-to-b from-gray-1/20 to-transparent p-8 dark:from-gray-7/20">
<div className="relative mx-auto aspect-[3/5] max-w-70">
<div className="rd-10 absolute inset-0 bg-gradient-to-b from-gray-8 to-gray-9 shadow-2xl">
<div className="rd-8 absolute inset-3 overflow-hidden bg-gradient-to-br from-brand-6 via-purple-600 to-pink-600">
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<div className="rd-b-2xl absolute top-0 left-1/2 h-7 w-28 -translate-x-1/2 bg-gray-9" />
</div>
<div className="rd-l absolute top-24 right-[-2px] h-12 w-1 bg-gray-7" />
<div className="rd-r absolute top-20 left-[-2px] h-6 w-1 bg-gray-7" />
<div className="rd-r absolute top-28 left-[-2px] h-10 w-1 bg-gray-7" />
<div className="rd-r absolute top-40 left-[-2px] h-10 w-1 bg-gray-7" />
</div>
{productAnnotations.map((annotation) => (
<AnnotationPoint
key={annotation.id}
annotation={annotation}
isActive={activeAnnotation === annotation.id}
onClick={() => setActiveAnnotation(activeAnnotation === annotation.id ? null : annotation.id)}
/>
))}
</div>
<div className="mt-8 flex flex-wrap justify-center gap-4">
{productAnnotations.map((annotation) => {
const colors = colorClasses[annotation.color];
return (
<button
key={annotation.id}
type="button"
onClick={() => setActiveAnnotation(activeAnnotation === annotation.id ? null : annotation.id)}
className={`rd-full flex items-center gap-2 px-3 py-1.5 text-#242424! text-xs transition-all ${
activeAnnotation === annotation.id ? `${colors.cardBg} ${colors.border} border` : 'hover:bg-gray-1'
}`}
>
<span className={`rd-full h-2 w-2 ${colors.bg}`} />
{annotation.title.split(' ')[0]}
</button>
);
})}
</div>
</div>
);
}
function CodeAnnotation() {
const [activeLines, setActiveLines] = useState<number[]>([2]);
const codeLines = [
{ num: 1, code: 'const greeting = (name) => {', annotation: '箭头函数定义,接收 name 参数' },
{ num: 2, code: ' const time = new Date().getHours();', annotation: '获取当前小时数(0-23)' },
{ num: 3, code: " let message = '';", annotation: '初始化问候语变量' },
{ num: 4, code: ' ', annotation: null },
{ num: 5, code: ' if (time < 12) {', annotation: '判断是否为上午' },
{ num: 6, code: " message = 'Good morning';", annotation: null },
{ num: 7, code: ' } else if (time < 18) {', annotation: '判断是否为下午' },
{ num: 8, code: " message = 'Good afternoon';", annotation: null },
{ num: 9, code: ' } else {', annotation: null },
{ num: 10, code: " message = 'Good evening';", annotation: null },
{ num: 11, code: ' }', annotation: null },
{ num: 12, code: ' ', annotation: null },
{ num: 13, code: ' return `${message}, ${name}!`;', annotation: '模板字符串拼接问候语' },
{ num: 14, code: '};', annotation: null },
];
const toggleLine = (num: number) => {
setActiveLines((prev) => (prev.includes(num) ? prev.filter((n) => n !== num) : [...prev, num]));
};
return (
<div className="rd-2xl max-w-80% overflow-hidden bg-gray-9">
<div className="flex items-center gap-2 border-gray-7 border-b bg-gray-8/50 px-4 py-3">
<div className="flex gap-1.5">
<div className="rd-full h-3 w-3 bg-error" />
<div className="rd-full h-3 w-3 bg-warning" />
<div className="rd-full h-3 w-3 bg-success" />
</div>
<span className="ml-2 text-gray-4 text-xs">greeting.js</span>
</div>
<div className="p-4 font-mono text-sm">
{codeLines.map((line) => {
const hasAnnotation = line.annotation !== null;
const isActive = activeLines.includes(line.num);
const anchorName = `--anchor-code-${line.num}`;
return (
<div key={line.num} className="relative flex">
<span className="w-8 select-none pr-4 text-right text-white-6">{line.num}</span>
<span
className={`flex-1 ${hasAnnotation ? 'rd -mx-1 cursor-pointer px-1 hover:bg-brand-5/20' : ''} ${isActive ? 'bg-brand-5/30' : ''}`}
style={hasAnnotation ? ({ anchorName: anchorName } as React.CSSProperties) : undefined}
onClick={() => hasAnnotation && toggleLine(line.num)}
>
<span className="whitespace-pre text-white-8">{line.code}</span>
</span>
{hasAnnotation && (
<div
className={`rd-lg fixed z-50 max-w-50 bg-brand-6 px-3 py-2 text-white text-xs shadow-lg transition-all duration-200 ${
isActive ? 'scale-100 opacity-100' : 'pointer-events-none scale-90 opacity-0'
}`}
style={
{
positionAnchor: anchorName,
left: 'anchor(right)',
top: 'anchor(center)',
translate: '18px -50%',
positionTryFallbacks: 'flip-inline',
} as React.CSSProperties
}
>
{line.annotation}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
const Demo: React.FC = () => {
return (
<div className="p-8">
<div className="space-y-20">
<section>
<div className="mb-6 font-semibold text-lg">产品功能标注</div>
<ProductShowcase />
<div className="text-center text-gray-6 text-sm">点击标注点或下方按钮查看功能详情</div>
</section>
<section>
<div className="mb-6 font-semibold text-lg">代码行注释</div>
<CodeAnnotation />
<div className="mt-4 text-gray-6 text-sm">点击高亮行查看代码注释。注释框使用 CSS Anchor 精确定位到对应代码行。</div>
</section>
</div>
</div>
);
};
export default Demo;
#核心特性
#1. 产品功能标注
在产品图片上添加交互式标注点,点击后显示详细说明:
- 标注点使用
ring和animate-ping实现呼吸效果 - 根据标注点位置(左/右)自动调整说明框方向
- 连接线精确指向标注点位置
style={{
positionAnchor: `--anchor-point-${id}`,
...(isOnLeft
? { left: 'anchor(right)', translate: '16px -50%' }
: { right: 'anchor(left)', translate: '-16px -50%' }
),
top: 'anchor(center)',
positionTryFallbacks: 'flip-inline, flip-block',
}}#2. 代码行注释
为代码编辑器添加交互式注释功能:
- 点击可注释的代码行显示说明
- 注释框定位到代码行右侧
- 三角形箭头指向对应代码行
- 支持多行同时激活
style={{
positionAnchor: `--anchor-code-${lineNum}`,
left: 'anchor(right)',
top: 'anchor(center)',
translate: '18px -50%',
positionTryFallbacks: 'flip-inline',
}}#3. 颜色系统
使用不同颜色区分不同类型的标注:
- 蓝色:显示屏相关
- 绿色:相机功能
- 黄色:性能芯片
- 红色:电池续航
#应用场景
- 产品展示页面:为产品图片添加功能标注
- 在线教程:为代码添加分步说明
- 技术文档:为示例代码添加详细注释
- 设计评审:为设计稿添加批注
