Sim 项目 UI 打磨指南:让界面「感觉更好」的 16 条设计工程原则
【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim
大型界面很少因为某一个亮点而显得精致,通常是一系列小细节叠加,最终复合出完整的高级体验。这份指南以仓库中.agents/skills/make-interfaces-feel-better/SKILL.md及其四份子文档(typography / surfaces / animations / performance)为骨架,结合 Sim 当前仓库的样式体系(.claude/rules/sim-styling.md、apps/sim/app/_styles/globals.css、packages/emcn)展开,面向所有在 Sim 中构建 UI 组件、review 前端代码、实现动画、悬浮态、阴影、边框、字号、微交互、进出场动画的开发者。读完你可以直接照单执行:每一条原则都配有可复制的 CSS / Tailwind / Motion 代码、仓库内落点以及 review 时的检查清单。
总览:什么时候该用哪套原则
SKILL.md 开头给出一张快速参考表,把细节打磨分成四大类,对应的深入文档分别是:
| 分类 | 覆盖内容 | 深入文档 |
|---|---|---|
| Typography | 文本换行、字体平滑、tabular 数字 | .agents/skills/make-interfaces-feel-better/typography.md |
| Surfaces | 圆角、光学对齐、阴影、图片描边、点击区域 | .agents/skills/make-interfaces-feel-better/surfaces.md |
| Animations | 可中断动画、进出场过渡、图标动画、按压缩放 | .agents/skills/make-interfaces-feel-better/animations.md |
| Performance | 过渡属性收敛、will-change用法 | .agents/skills/make-interfaces-feel-better/performance.md |
Sim 前端使用 Tailwind +framer-motion(apps/sim/package.json中为"framer-motion": "^12.5.0"),组件库集中在packages/emcn。因此文中 Motion 示例使用motion/react(Framer Motion 新包名)语法,同时给出无 Motion 依赖时的纯 CSS 替代方案。
一、Surfaces:圆角、对齐、阴影与图片描边
1. 同心圆角(Concentric Border Radius)
嵌套圆角元素时,外半径必须等于内半径加上两者之间的 padding:
outerRadius = innerRadius + padding内外半径不匹配是让界面「感觉不对劲」最常见的单一原因。这条规则在嵌套表面距离较近时最有效;如果 padding 超过24px,就把各层当作独立表面,各自选半径,不必强行套同心数学。
/* 好——同心半径 */ .card { border-radius: 20px; /* 12 + 8 */ padding: 8px; } .card-inner { border-radius: 12px; } /* 坏——内外半径相同 */ .card { border-radius: 12px; padding: 8px; } .card-inner { border-radius: 12px; }Tailwind 写法:
{/* 好——外层半径已计入 padding */} <div className="rounded-2xl p-2"> {/* 16px 半径,8px padding */} <div className="rounded-lg"> {/* 8px = 16 - 8 ✓ */} ... </div> </div> {/* 坏——同样半径 */} <div className="rounded-xl p-2"> <div className="rounded-xl"> {/* 同样半径,看起来不对 */} ... </div> </div>2. 光学对齐优先于几何对齐(Optical Alignment)
当几何居中看起来偏时,要按光学对齐。按钮带图标、播放三角、不对称图标都需要手工调整。
文字 + 图标按钮:图标一侧 padding 比文字一侧少 2px,经验法则是icon-side padding = text-side padding - 2px。
/* 好——图标侧更少的 padding */ .button-with-icon { padding-left: 16px; padding-right: 14px; /* 图标侧 = 文字侧 - 2px */ } /* 坏——等距 padding 让图标看起来被推得太靠右 */ .button-with-icon { padding: 0 16px; }<button className="pl-4 pr-3.5 flex items-center gap-2"> <span>Continue</span> <ArrowRightIcon /> </button>播放三角:三角形图标的几何中心不等于视觉中心,需向右微移:
.play-button svg { margin-left: 2px; /* 向右偏移以补偿三角形重心 */ }不对称图标(星、箭头、caret):最佳方案是直接改 SVG(调整 viewBox 或 path),让组件代码里不再需要额外 margin;兜底方案才用ml-px之类的微调类。
3. 阴影优先于边框(Shadows Over Borders)
注意:在 Sim 仓库中,不要把中性边框换成
0 0 0 1px的阴影环。Sim 有自己完整的 token 体系:抬升效果用shadow-subtle/shadow-medium/shadow-overlay/shadow-card阴影 token,中性边线与分隔线保持--border边框(见 .claude/rules/sim-styling.md 的 "Line weight" 一节)。下面的--shadow-border模式是给没有这套 token 系统的其他项目用的。
在 apps/sim/app/_styles/globals.css 中可以看到 Sim 实际的阴影 token 定义:
--shadow-subtle: 0 2px 4px 0 rgba(0, 0, 0, 0.08); --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.1); --shadow-overlay: 0 10px 30px rgba(0, 0, 0, 0.11); --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04);--border的行宽来自--border-width:默认1px,在@media (min-resolution: 2dppx)下降到0.5px,让高密度屏得到真正的 hairline。注意sim-styling.md特别强调:线条的重量要调--border的颜色,不要调--border-width——浏览器会把边框取整到整设备像素,(0, 1px)区间内的值在 2dppx 屏幕上都会塌缩成同一根单像素线;而且永远不要用shadow-[inset_0_-1px_0_…]手工画线,box-shadow 有自己的宽度、无法跟随 token,会以双倍重量渲染。
对没有 token 体系的项目,用阴影代替边框实现卡片/容器的深度感:
/* 浅色模式:三层阴影——1px 边框环 + 轻微抬升 + 环境深度 */ :root { --shadow-border: 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 2px -1px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(0, 0, 0, 0.04); --shadow-border-hover: 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 1px 2px -1px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.06); } /* 深色模式:简化为单层白色环,深色背景上看不到分层深度阴影 */ --shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.08); --shadow-border-hover: 0 0 0 1px rgba(255, 255, 255, 0.13);配合 hover 过渡平滑切换:
.card { box-shadow: var(--shadow-border); transition-property: box-shadow; transition-duration: 150ms; transition-timing-function: ease-out; } .card:hover { box-shadow: var(--shadow-border-hover); }什么时候用阴影、什么时候保留边框:
| 用阴影 | 用边框 |
|---|---|
| 卡片、有深度的容器 | 列表项之间的分隔线 |
| 带边框样式的按钮 | 表格单元格边界 |
| 抬升元素(下拉、弹窗) | 表单输入框轮廓(可访问性考虑) |
| 多变背景上的元素 | 密集 UI 中的 hairline 分隔 |
| hover/focus 抬升效果 |
不要把这条规则套到分隔线上(border-b、border-t、侧边线),凡是职责是「布局分隔」而非「元素深度」的边框都应保持边框。
4. 图片描边(Image Outlines)
给图片加一条低透明度的 1px 描边,在周围元素都用边框或阴影的设计系统里能统一深度感。
- 浅色模式:纯黑
rgba(0, 0, 0, 0.1) - 深色模式:纯白
rgba(255, 255, 255, 0.1) - 带灰度的中性色(slate-900、zinc-900、
#0a0a0a、#f5f5f7)以及 accent/ink 色会吸收周围表面颜色,在图片边缘看起来像脏污——描边是中性分隔符,不是主题元素。
img { outline: 1px solid rgba(0, 0, 0, 0.1); outline-offset: -1px; /* 内嵌,不增加布局尺寸 */ }Tailwind(含暗色模式):
<img className="outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10" src={src} alt={alt} />注意只用outline-black/10/outline-white/10,不要用outline-slate-*、outline-zinc-*、outline-neutral-*等任何灰度刻度。为什么用 outline 而不是 border?outline不参与布局(不增加宽高),outline-offset: -1px保持内嵌,图片尺寸不变。
5. 最小点击区域(Minimum Hit Area)
交互元素需要至少 40×40px(WCAG 建议 44×44px)的点击区域。如果可见元素更小(比如 20×20 的复选框),用伪元素扩展命中区:
.checkbox { position: relative; width: 20px; height: 20px; } .checkbox::after { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40px; height: 40px; }<button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2"> <CheckIcon /> </button>碰撞规则:扩展后的命中区如果与另一个交互元素重叠,就缩小伪元素——但要在不碰撞的前提下尽量大。两个交互元素的命中区永远不能重叠。
二、Typography:换行、平滑与数字
6. 文本换行:balance 与 pretty
text-wrap: balance让文本均匀分布到各行,避免标题和短文本出现孤词。注意它只在 6 行以内(Chromium)或 10 行以内(Firefox)生效——平衡算法计算代价高,浏览器限制其只作用于短文本。对长段落使用会被静默忽略,属于浪费意图。
/* 好——短文本均匀分行 */ h1, h2, h3 { text-wrap: balance; } /* 坏——长段落上用 balance(被静默忽略) */ .article-body p { text-wrap: balance; }Tailwind:text-balance
text-wrap: pretty防止段落最后一行悬挂孤词,它不追求行等宽,只保证末行不会短得尴尬。对任何长度文本都没有行数限制,是短到中等文本(段落、描述、说明文字、列表项、卡片文字)的默认选择。对 10 行以上的超长文本,两者都不建议用,浏览器默认换行即可,避免不必要的布局开销。
<p className="text-pretty"> 一个不会在最后一行留下孤词的短段落。 </p>Tailwind:text-pretty
| 场景 | 使用 |
|---|---|
| 标题、需要均匀分布的场景 | text-wrap: balance |
| 短到中等文本:段落、描述、说明、UI 文字 | text-wrap: pretty |
| 长文本(10 行以上)、代码块、预格式化文本 | 都不用,保持默认 |
7. 字体平滑(macOS)
macOS 上文本默认渲染偏重。在根布局上应用 antialiased 平滑,让所有文本更清晰、更细:
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }<html className="antialiased">在根上只做一次,不要逐个元素应用,否则会出现「标题平滑、正文不平滑」的不一致。该属性只影响 macOS 渲染,其他平台忽略它,可以安全地全局应用。
8. Tabular Numbers(等宽数字)
动态更新的数字(计数器、价格、计时器、表格列)用 tabular-nums 让所有数字等宽,防止数值变化时布局跳动:
.counter { font-variant-numeric: tabular-nums; }<span className="tabular-nums">{count}</span>| 用 tabular-nums | 不要用 |
|---|---|
| 计数器、计时器 | 静态展示数字 |
| 会更新的价格 | 装饰性大数字 |
| 含数字的表格列 | 电话号码、邮编 |
| 数字转场动画 | 版本号(v2.1.0) |
| 计分板、仪表盘 |
注意事项:某些字体(如 Inter)开启该属性后会改变数字外观——尤其是数字1会变宽并居中。这是预期行为且通常有利于对齐,但要在具体字体上确认效果。
三、Animations:可中断、错峰与进出场
9. 可中断动画(Interruptible Animations)
用户会在交互中途改变意图,动画不可中断会让界面「像坏了一样」。
| CSS Transitions | CSS Keyframe Animations | |
|---|---|---|
| 行为 | 向最新状态插值 | 按固定时间线运行 |
| 可中断 | 是——中途重新定向 | 否——从头重新开始 |
| 用途 | 交互状态变化(hover、toggle、开合) | 只运行一次的分段序列(入场、loading) |
| 时长 | 随剩余距离自适应 | 与状态无关的固定时长 |
/* 好——toggle 用可中断 transition */ .drawer { transform: translateX(-100%); transition: transform 200ms ease-out; } .drawer.open { transform: translateX(0); } /* 动画中途再次点击会平滑反向——无卡顿 */ /* 坏——交互元素用 keyframe 动画 */ .drawer.open { animation: slideIn 200ms ease-out forwards; } /* 中途关闭会跳变或重播——感觉坏了 */规则:交互元素永远优先用 CSS transition;keyframes 只留给一次性序列。
10. 入场动画:拆分 + 错峰(Split and Stagger)
不要动画一个巨大的容器。把内容拆成语义块,每块单独动画,块间约100ms延迟。
步骤:1)拆分为逻辑组(标题、描述、按钮);2)错峰约 100ms;3) 标题可拆到单词级、约 80ms 错峰;4) 组合opacity+blur+translateY完成入场效果。
// Motion(Framer Motion)——错峰入场 function PageHeader() { return ( <motion.div initial="hidden" animate="visible" variants={{ visible: { transition: { staggerChildren: 0.1 } }, }} > <motion.h1 variants={{ hidden: { opacity: 0, y: 12, filter: "blur(4px)" }, visible: { opacity: 1, y: 0, filter: "blur(0px)" }, }} > Welcome </motion.h1> <motion.p variants={{ hidden: { opacity: 0, y: 12, filter: "blur(4px)" }, visible: { opacity: 1, y: 0, filter: "blur(0px)" }, }} > A description of the page. </motion.p> <motion.div variants={{ hidden: { opacity: 0, y: 12, filter: "blur(4px)" }, visible: { opacity: 1, y: 0, filter: "blur(0px)" }, }} > <Button>Get started</Button> </motion.div> </motion.div> ); }纯 CSS 错峰:
.stagger-item { opacity: 0; transform: translateY(12px); filter: blur(4px); animation: fadeInUp 400ms ease-out forwards; } .stagger-item:nth-child(1) { animation-delay: 0ms; } .stagger-item:nth-child(2) { animation-delay: 100ms; } .stagger-item:nth-child(3) { animation-delay: 200ms; } @keyframes fadeInUp { to { opacity: 1; transform: translateY(0); filter: blur(0); } }11. 离场动画要克制(Subtle Exit Animations)
离场应比入场更柔和、更不抢注意力——用户注意力正要移向下一件事。
推荐——小幅固定 translateY:
<motion.div exit={{ opacity: 0, y: -12, filter: "blur(4px)", transition: { duration: 0.15, ease: "easeIn" }, }} > {content} </motion.div>全尺寸离场(仅当空间上下文重要时)——例如卡片回到列表、抽屉关闭:
<motion.div exit={{ opacity: 0, x: "-100%", transition: { duration: 0.2, ease: "easeIn" }, }} > {content} </motion.div>/* 好——克制离场 */ .item-exit { opacity: 0; transform: translateY(-12px); transition: opacity 150ms ease-in, transform 150ms ease-in; } /* 坏——夸张离场抢走注意力 */ .item-exit { opacity: 0; transform: translateY(-100%) scale(0.5); transition: all 400ms ease-in; } /* 坏——完全没有离场动画(元素直接消失) */ .item-exit { display: none; }要点:用小而固定的translateY(如-12px)而非容器整高;保留一点方向性移动指示元素去向;离场时长应短于入场(150ms vs 300ms);不要完全去掉离场动画——克制的动效保留空间上下文。
12. 上下文图标动画(Contextual Icon Animations)
图标随上下文出现/消失(hover、状态切换)时,用opacity、scale、blur动画,而不是简单切换可见性。默认值:scale0.25→1、opacity0→1、blur4px→0、{ type: "spring", duration: 0.3, bounce: 0 }。
Motion 方案:
import { AnimatePresence, motion } from "motion/react"; function IconButton({ isActive, icon: Icon }) { return ( <button> <AnimatePresence mode="popLayout"> <motion.span key={isActive ? "active" : "inactive"} initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }} animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }} transition={{ type: "spring", duration: 0.3, bounce: 0 }} > <Icon /> </motion.span> </AnimatePresence> </button> ); }纯 CSS 交叉淡入(无 Motion 依赖):两个图标都留在 DOM 里,一个绝对定位叠在另一个上面。切换状态即交叉淡入——进入的图标从0.25放大、退出的图标缩到0.25,都带 opacity 和 blur。非绝对定位的图标决定布局尺寸,绝对定位的图标叠放其上、不参与文档流。
function IconButton({ isActive, ActiveIcon, InactiveIcon }) { return ( <button> <div className="relative"> <div className={cn( "absolute inset-0 flex items-center justify-center", "transition-[opacity,filter,scale] duration-300", "cubic-bezier(0.2, 0, 0, 1)", isActive ? "scale-100 opacity-100 blur-0" : "scale-[0.25] opacity-0 blur-[4px]" )} > <ActiveIcon /> </div> <div className={cn( "transition-[opacity,filter,scale] duration-300", "cubic-bezier(0.2, 0, 0, 1)", isActive ? "scale-[0.25] opacity-0 blur-[4px]" : "scale-100 opacity-100 blur-0" )} > <InactiveIcon /> </div> </div> </button> ); }怎么选:检查项目package.json里有没有motion或framer-motion。有就用 Motion 方案(Sim 有framer-motion ^12.5.0);没有就用 CSS 交叉淡入——不要只为图标过渡引入新依赖。
| Motion (Framer Motion) | CSS transitions(双图标常驻 DOM) | |
|---|---|---|
| 入场动画 | 有 | 有 |
| 离场动画 | 有(经AnimatePresence) | 有(交叉淡入——图标永不移除) |
| 弹簧物理 | 有 | 无——用cubic-bezier(0.2, 0, 0, 1)近似 |
| 适用场景 | 项目已用motion/react | 无 Motion 依赖,或想控制包体积 |
| 该动画 | 不该动画 |
|---|---|
| hover 才出现的图标(操作按钮) | 静态导航图标 |
| 状态切换图标(播放→暂停、喜欢→已喜欢) | 装饰性图标 |
| 上下文工具栏里的图标 | 始终可见的图标 |
| 加载/成功状态指示器 | 图标标签(图标旁的文字) |
13. 按压缩放(Scale on Press)
点击时轻微缩小(约 0.96–0.97)给按钮触觉反馈。用 CSS transition 保证可中断——用户在按压中途松手也能平滑复位。不是每个按钮都需要;给按钮组件加staticprop 在动效会分散注意力时禁用缩放。
.button { transition-property: scale; transition-duration: 150ms; transition-timing-function: ease-out; } .button:active { scale: 0.96; }<button className="transition-transform duration-150 ease-out active:scale-[0.96]"> Click me </button><motion.button whileTap={{ scale: 0.96 }}> Click me </motion.button>Static Prop 模式:
const tapScale = "active:not-disabled:scale-[0.96]"; function Button({ static: isStatic, className, children, ...props }) { return ( <button className={cn( "transition-transform duration-150 ease-out", !isStatic && tapScale, className, )} {...props} > {children} </button> ); } <Button>Click me</Button> {/* 按压缩放 */} <Button static>Submit</Button> {/* 无缩放 */}Sim 仓库落点:SKILL.md 明确指出,按压反馈应归属 emcn 的Button/Chipchrome(packages/emcn),而不是消费方类。查 packages/emcn/src/components/button/button.tsx 可以看到当前buttonVariants只有transition-colors、hover 态与 focus 处理,目前两个组件都没有实现按压缩放——正确的做法是去packages/emcn里提这个能力,而不是在每个调用点手写 transform 类。
14. 页面加载跳过入场动画
在AnimatePresence上用initial={false}防止首次渲染触发入场动画。已经处于默认状态的元素不应在页面加载时动起来,只在后续状态变化时动画。
{/* 好——图标挂载时不动画,只在状态切换时动画 */} <AnimatePresence initial={false} mode="popLayout"> <motion.span key={isActive ? "active" : "inactive"} initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }} animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }} > <Icon /> </motion.span> </AnimatePresence>适用于:图标切换、toggle、标签页、分段控件——任何页面加载时有默认状态的元素。
何时会坏:组件依赖initialprop 建立首次入场(如错峰 hero、加载态)时不要用initial={false}——那会跳过整个入场。应用前请做一次整页刷新验证。
四、Performance:过渡收敛与 GPU 提示
15. 永远不要用transition: all
transition: all迫使浏览器监听每一个属性的变化:会在你没打算动画的属性(颜色、padding、阴影)上产生意外过渡,并阻止浏览器优化。
/* 好——只过渡变化的部分 */ .button { transition-property: scale, background-color; transition-duration: 150ms; transition-timing-function: ease-out; } /* 坏——过渡一切 */ .button { transition: all 150ms ease-out; }// 好——显式属性 <button className="transition-[scale,background-color] duration-150 ease-out"> // 坏——过渡一切 <button className="transition duration-150 ease-out">Tailwindtransition-transform注意:它映射为transition-property: transform, translate, scale, rotate——覆盖所有 transform 相关属性,不只是transform。只动 transform 时用它;多个非 transform 属性用方括号语法transition-[scale,opacity,filter]。
16.will-change要克制
will-change提示浏览器把元素预提升到独立 GPU 合成层。没有它,浏览器只在动画开始时才提升——那一次性的层提升会在第一帧造成微卡顿。特别适合元素在改变scale、rotation或用transform移动的场景。其他属性帮助不大——浏览器本来就没法在 GPU 上合成它们。
/* 好——受益于 GPU 合成的具体属性 */ .animated-card { will-change: transform; } /* 坏——永远不要用 will-change: all */ .animated-card { will-change: all; } /* 坏——本来就不能 GPU 合成的属性 */ .animated-card { will-change: background-color, padding; }| 属性 | 可 GPU 合成 | 值得用will-change |
|---|---|---|
transform | 是 | 是 |
opacity | 是 | 是 |
filter(blur、brightness) | 是 | 是 |
clip-path | 是 | 是 |
top、left、width、height | 否 | 否 |
background、border、color | 否 | 否 |
何时跳过:现代浏览器自身优化已经很好。只在观察到首帧卡顿时才加will-change——Safari 尤其受益。不要给每个动画元素都预先加上;每多一个合成层都占内存。
常见错误速查
| 错误 | 修复 |
|---|---|
| 父子元素同样圆角 | 计算outerRadius = innerRadius + padding |
| 图标看起来不居中 | 光学微调(padding 或直接改 SVG) |
| 区块之间硬边框 | Sim 内用--borderhairline token;其他项目用带透明度的分层box-shadow |
| 生硬的进出场动画 | 拆分、错峰、离场保持克制 |
| 数字引起布局跳动 | 应用tabular-nums |
| macOS 上文字偏重 | 根元素应用antialiased |
| 页面加载就播动画 | AnimatePresence加initial={false} |
元素上有transition: all | 指定精确属性 |
| 首帧动画卡顿 | 加will-change: transform(克制) |
| 小控件点击区域太小 | 用伪元素扩展到 40×40px |
实践:UI Review 的输出格式与检查清单
Review 输出格式:以 Markdown 表格呈现改动,带Before / After两列;每条原则一个表格并加标题;每行一个 diff;当代码片段无法自解释时,引用文件和具体属性。如果某条原则 review 后无需改动,直接省略该表格——空表格只会增加噪音。
示例:
同心圆角
| Before | After |
|---|---|
卡片rounded-xl+ 内按钮rounded-xl(p-2) | 卡片rounded-2xl(12 + 8)、内按钮rounded-lg |
两个嵌套表面都是border-radius: 16px | 外层24px,内层16px配8pxpadding |
Tabular 数字
| Before | After |
|---|---|
动画计数器<span>{count}</span> | <span className="tabular-nums">{count}</span> |
| 计时器默认数字 | 根元素加font-variant-numeric: tabular-nums |
按压缩放
| Before | After |
|---|---|
<button className="..."> | 加active:scale-[0.96] transition-transform |
按压scale(0.9) | 提升到scale(0.96)——低于0.95感觉夸张 |
完整 Review 检查清单(逐项核对):
- 嵌套圆角元素使用同心圆角
- 图标是光学居中,不只是几何居中
- 合适场景用阴影而非边框
- 入场动画已拆分并错峰
- 离场动画克制
- 动态数字用 tabular-nums
- 已应用字体平滑
- 标题用 text-wrap: balance
- 图片有细微描边
- 按钮在合适场景使用按压缩放
- 默认态元素上的
AnimatePresence使用initial={false} - 无
transition: all——只有精确属性 will-change只在 transform/opacity/filter 上,绝不用于all- 交互元素至少有 40×40px 点击区域
结合 Sim 仓库落地的补充说明
上述原则在 Sim 代码库中有几处明确的「本地化」约定,落地时务必遵循:
阴影 token 优先:抬升用
shadow-subtle/shadow-medium/shadow-overlay/shadow-card,定义见 apps/sim/app/_styles/globals.css;中性边线保持--border,见 .claude/rules/sim-styling.md 的 "Line weight" 一节。sim-styling.md同时规定过渡类用transition-colors(交互状态)、禁止内联样式、精确值优于近似值(如h-[26px]而非h-6,但字号必须用具名 token)。按压缩放的归属:应放在
packages/emcn的Button/Chipchrome 中统一实现(当前 button.tsx 尚无该能力),不要在消费方逐个手写active:scale-*。图标渲染细节:
sim-styling.md规定图标默认size-[14px],等宽高用size-*而非h-N w-N;这与光学对齐原则直接呼应。Motion 依赖:
apps/sim/package.json已含framer-motion ^12.5.0,图标切换、进出场可直接使用motion/react的AnimatePresence+ 弹簧过渡;若目标代码路径不允许引入 Motion,则用上文「双图标常驻 DOM + CSS 交叉淡入」方案。
这 16 条原则是一套可以反复套用的「打磨清单」:构建新组件时逐条对照,review 代码时按表格输出 Before/After。细节不会单独成就一个界面,但一打正确的细节叠加,就是高级感本身。
【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考