我用72小时、18700行代码复刻了Claude的设计系统,然后发现真正值钱的不是代码
我用72小时、18700行代码复刻了Claude的设计系统,然后发现真正值钱的不是代码
你有没有注意到,Claude用起来就是比其他AI聊天工具"顺"——但你说不清楚哪里顺。
不是因为它更聪明。不是因为界面更花哨。是那种说不清道不明的"丝滑感"——消息出来的节奏、错误提示的措辞、输入框的弹性、连"思考中"的动画都透着一股克制的专业气息。
我在某个深夜盯着Claude的对话框想:这种"顺"是可以被拆解的。被拆解了,就能被复制。
于是我开始了一个有点疯狂的实验:用72小时,尽可能完整地复刻Claude的前端设计系统。最终产出了18700行代码,踩了一堆坑,也提炼出了5个任何AI产品都可以直接抄的设计细节。
这篇文章是全程实录。
---
第一章:为什么要复刻Claude的设计系统?
先说清楚动机,避免误解。
这不是一篇"抄袭界面"的文章。复刻的目的是逆向工程设计决策——搞清楚Anthropic的产品团队在每一个交互细节上为什么这样选择,而不是那样选择。
市面上大多数套壳AI产品,用起来都有一种微妙的廉价感。不是功能不行,而是"感觉不对"。具体来说,通常是这四个维度出了问题:
排版:消息气泡间距混乱,Markdown渲染不一致,代码块没有语法高亮或者高亮样式突兀。 交互反馈:发送消息后没有即时反馈,用户不知道系统在处理还是卡死了。 状态管理:加载中、流式输出中、错误状态——这三种状态的视觉区分模糊,用户体验焦虑。 错误处理:出错了只弹一个"请求失败",完全不告诉用户下一步该怎么办。Claude在这四个维度上都做得异常克制而精准。复刻它,本质上是给自己做一次强制性的设计教育。
---
第二章:72小时实验全记录
0-24小时:逆向分析与组件拆解
第一天没有写一行业务代码。全部时间用在观察和记录上。
打开Claude,用开发者工具扒CSS变量,记录颜色系统(比你想象的简单,核心颜色不超过12个);用慢速网络模拟流式输出,逐帧分析打字机效果的节奏;故意触发各种错误,截图记录每一种错误状态的提示文案和视觉样式。
最终梳理出需要实现的组件清单:消息气泡、输入框、工具栏、侧边栏对话历史、加载动画、错误提示、代码块、Markdown渲染器。
18700行代码的模块分布大致如下: | 模块 | 代码量占比 | | UI组件层 | ~42% | | 状态管理层 | ~28% | | 工具函数与Hooks | ~18% | | 类型定义与接口 | ~12% |UI组件占比最高,但状态管理才是最难写的部分——后面会说为什么。
24-48小时:踩坑记录
这是整个实验最痛苦的阶段,也是信息密度最高的阶段。
坑一:流式输出的光标闪烁问题。打字机效果听起来简单——不就是逐字追加文本吗?但实际上,SSE推送的数据块大小不均匀:有时一次推送半个句子,有时只推送一个字符。如果直接追加渲染,会出现光标位置跳动、Markdown中间状态渲染错误(比如加粗在**还没闭合时会直接显示星号)等问题。
解决思路是引入一个渲染缓冲区,对流式数据做防抖处理,同时将"原始文本积累"和"渲染触发"解耦。具体实现见第三章。
坑二:Markdown渲染的边界case。Claude的回答里经常出现嵌套列表、表格、代码块混排。大多数Markdown渲染库在处理这种复杂嵌套时会出现样式崩塌。踩坑之后的结论是:不要用全功能Markdown库处理流式内容,而是在流式阶段只渲染内联样式(加粗、斜体、行内代码),等输出完成后再整体渲染块级元素(表格、代码块、列表)。
坑三:移动端适配的三个死亡陷阱。- iOS Safari的
100vh包含地址栏高度,导致输入框被软键盘遮挡 - 移动端的
resize事件在软键盘弹出时不稳定,输入框自适应高度失效 - 触摸滚动在流式输出时会被强制打断,用户体验极差
这三个问题前后花了将近6小时,是整个72小时里性价比最低的时间投入。
48-72小时:收尾与打磨
最后24小时做的事情听起来不性感,但价值极高:统一设计token、清理魔法数字、补全TypeScript类型定义、写组件文档。
这些工作让代码从"能跑"变成"可维护"。也是很多独立开发者最容易跳过的环节——然后在三个月后为此付出双倍的时间成本。
---
第三章:5个值得直接抄的设计细节
① "思考中"动画——纯CSS实现,零依赖
Claude的三点跳动动画看起来简单,但细节在于:三个点的动画不是同频的,而是有错开的延迟,形成"波浪感"。
.thinking-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--color-text-secondary);
animation: thinking-bounce 1.2s ease-in-out infinite;
}
.thinking-dot:nth-child(1) { animation-delay: 0s; }
.thinking-dot:nth-child(2) { animation-delay: 0.2s; }
.thinking-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes thinking-bounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-6px); }
}
为什么Claude这样做: 同频动画会让用户感到焦虑(像警报),错开延迟的波浪感传达的是"正在处理,不慌"。
② 流式输出的打字机效果防抖处理
核心逻辑是维护一个文本缓冲区,以固定帧率(而非SSE推送频率)触发渲染更新:
class StreamRenderer {
private buffer: string = '';
private rafId: number | null = null;
private onUpdate: (text: string) => void;
constructor(onUpdate: (text: string) => void) {
this.onUpdate = onUpdate;
}
push(chunk: string) {
this.buffer += chunk;
if (!this.rafId) {
this.rafId = requestAnimationFrame(() => {
this.onUpdate(this.buffer);
this.rafId = null;
});
}
}
flush() {
if (this.rafId) cancelAnimationFrame(this.rafId);
this.onUpdate(this.buffer);
}
}
用requestAnimationFrame而非setTimeout的原因:与浏览器渲染周期对齐,避免掉帧。
③ 错误状态的分级提示设计
这是最容易被忽视、但对用户体验影响最大的细节。Claude的错误提示从不只说"出错了",而是告诉用户是什么错、为什么错、能做什么。
type ErrorLevel = 'network' | 'api' | 'content' | 'rate_limit';
interface AppError {
level: ErrorLevel;
title: string;
description: string;
action: {
label: string;
handler: () => void;
} | null;
}
const ERROR_MAP: Record> = {
network: {
level: 'network',
title: '网络连接中断',
description: '请检查你的网络连接后重试',
},
api: {
level: 'api',
title: '服务暂时不可用',
description: '后端服务遇到问题,通常几分钟内会恢复',
},
content: {
level: 'content',
title: '内容无法生成',
description: '这条消息触发了内容限制,请调整后重试',
},
rate_limit: {
level: 'rate_limit',
title: '请求过于频繁',
description: '稍等片刻后再试',
},
};
收益: 用户看到错误不再茫然,流失率显著降低。这是用户体验中ROI最高的改动之一。
④ 对话历史的懒加载策略
侧边栏历史列表不要一次性渲染所有条目。Claude用的是"按时间分组+虚拟滚动"的组合:今天、昨天、过去7天、更早——分组本身就是一种信息降噪。
实现上,优先使用IntersectionObserver做懒加载,而非监听scroll事件——性能差距在历史条目超过100条时非常明显。
⑤ 输入框自适应高度与快捷键绑定
function useAutoResize(ref: RefObject) {
const resize = useCallback(() => {
const el = ref.current;
if (!el) return;
el.style.height = 'auto';
el.style.height = ${Math.min(el.scrollHeight, 200)}px;
}, [ref]);
useEffect(() => {
const el = ref.current;
if (!el) return;
el.addEventListener('input', resize);
return () => el.removeEventListener('input', resize);
}, [resize]);
}
关键点:先设height: auto再读scrollHeight,否则高度只会增长不会收缩。上限设200px,超过后转为内部滚动。
---
第四章:哪里不值得复刻?
诚实说:Claude有些设计决策是为它自己的产品形态服务的,生搬硬套会适得其反。
复刻价值评分矩阵: | 模块 | 实现成本 | 迁移收益 | 建议 | | 消息气泡排版 | 低 | 高 | ✅ 直接抄 | | 打字机效果 | 低 | 高 | ✅ 直接抄 | | 错误分级提示 | 中 | 高 | ✅ 直接抄 | | 思考中动画 | 低 | 中 | ✅ 直接抄 | | Projects信息架构 | 高 | 低(依赖产品形态) | ❌ 按需重设计 | | 侧边栏分组逻辑 | 中 | 中 | ⚠️ 参考但不照搬 | | 多文件上传交互 | 高 | 低(除非你有此需求) | ❌ 跳过 | | 语音输入模块 | 高 | 低 | ❌ 跳过 |核心结论:设计系统的价值在于"一致性",而不是"像谁"。 你复刻Claude,不是为了让用户觉得你在用Claude,而是为了让你的产品在每一个交互细节上都有同等水准的思考密度。
---
第五章:接上API,让设计系统跑起来
再好的界面,没有稳定的API支撑都是空壳。
将这套设计系统接入真实API,有三个关键配置节点:
1. 流式输出的SSE处理不要用fetch+手动解析,推荐用EventSource或成熟的SSE客户端库。关键是要处理好[DONE]信号,在流结束时调用StreamRenderer.flush()触发最终渲染。
在输入框上方实时显示当前对话的Token消耗,是一个低成本、高信任感的设计细节。大多数API都会在响应头或响应体中返回usage字段,直接读取即可。
切换模型时,必须清空当前的流式渲染缓冲区,并重置错误状态。否则会出现"上一个模型的错误提示在新模型回答时还在显示"的诡异状态。
复刻实验中,API层我用的是 api.884819.xyz 作为接入节点——原因很实际:流式输出的延迟表现稳定,支持直接切换底层模型(Claude全系列、GPT系列、Deepseek等),方便在同一套UI下对比不同模型的体验差异。接入方式和OpenAI官方API完全兼容,改一行baseURL就能跑,国产模型还完全免费,没有月租。
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.884819.xyz/v1',
apiKey: 'your-api-key',
});
就这一行改动,你的整套设计系统就有了真实的灵魂。
---
写在最后
72小时、18700行代码,最终让我真正理解的不是Claude的界面,而是一件更重要的事:
优秀的AI产品体验,从来不是偶然的。 它是无数个"为什么这样而不是那样"的设计决策的积累。每一个动画延迟、每一条错误文案、每一个像素间距背后,都有一个被反复推敲过的理由。复刻不是目的,理解设计决策才是。理解了,你才能在自己的产品里做出同样有温度的选择。
---
📦 本文配套资源
- 5个可复用组件的独立代码片段(见评论区置顶)
- API快速接入配置模板
→ 推荐使用 [api.884819.xyz](https://api.884819.xyz)
兼容OpenAI格式 / 支持Claude全系列 / 按量计费 / 新用户注册即送体验Token / 国产模型免费
---
💬 下篇预告
这套设计系统现在跑在一个真实项目上——一个我用来替代Notion做技术笔记的AI工具。
下一篇我想聊一个更有意思的问题:当你把Claude的设计语言移植到垂直场景,哪些地方会"水土不服"?哪些地方反而比原版更好用?
比如在技术笔记场景里,我把对话历史改成了"知识图谱"形式的导航——这个改动让我自己的使用频率提升了三倍,但也带来了一个完全没预料到的问题。
如果你也在做类似的事,欢迎评论区告诉我你的场景。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI开发 #Claude #前端工程 #设计系统 #8848AI #流式输出 #AI产品 #独立开发