十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Sim 项目 UI 打磨指南:让界面「感觉更好」的 16 条设计工程原则

Sim 项目 UI 打磨指南:让界面「感觉更好」的 16 条设计工程原则 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.mdSurfaces圆角、光学对齐、阴影、图片描边、点击区域.agents/skills/make-interfaces-feel-better/surfaces.mdAnimations可中断动画、进出场过渡、图标动画、按压缩放.agents/skills/make-interfaces-feel-better/animations.mdPerformance过渡属性收敛、will-change用法.agents/skills/make-interfaces-feel-better/performance.mdSim 前端使用 Tailwind framer-motionapps/sim/package.json中为framer-motion: ^12.5.0组件库集中在packages/emcn。因此文中 Motion 示例使用motion/reactFramer Motion 新包名语法同时给出无 Motion 依赖时的纯 CSS 替代方案。一、Surfaces圆角、对齐、阴影与图片描边1. 同心圆角Concentric Border Radius嵌套圆角元素时外半径必须等于内半径加上两者之间的 paddingouterRadius 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 classNamerounded-2xl p-2 {/* 16px 半径8px padding */} div classNamerounded-lg {/* 8px 16 - 8 ✓ */} ... /div /div {/* 坏——同样半径 */} div classNamerounded-xl p-2 div classNamerounded-xl {/* 同样半径看起来不对 */} ... /div /div2. 光学对齐优先于几何对齐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 classNamepl-4 pr-3.5 flex items-center gap-2 spanContinue/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 classNameoutline 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 而不是 borderoutline不参与布局不增加宽高outline-offset: -1px保持内嵌图片尺寸不变。5. 最小点击区域Minimum Hit Area交互元素需要至少 40×40pxWCAG 建议 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 classNamerelative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2 CheckIcon / /button碰撞规则扩展后的命中区如果与另一个交互元素重叠就缩小伪元素——但要在不碰撞的前提下尽量大。两个交互元素的命中区永远不能重叠。二、Typography换行、平滑与数字6. 文本换行balance 与 prettytext-wrap: balance让文本均匀分布到各行避免标题和短文本出现孤词。注意它只在 6 行以内Chromium或 10 行以内Firefox生效——平衡算法计算代价高浏览器限制其只作用于短文本。对长段落使用会被静默忽略属于浪费意图。/* 好——短文本均匀分行 */ h1, h2, h3 { text-wrap: balance; } /* 坏——长段落上用 balance被静默忽略 */ .article-body p { text-wrap: balance; }Tailwindtext-balancetext-wrap: pretty防止段落最后一行悬挂孤词它不追求行等宽只保证末行不会短得尴尬。对任何长度文本都没有行数限制是短到中等文本段落、描述、说明文字、列表项、卡片文字的默认选择。对 10 行以上的超长文本两者都不建议用浏览器默认换行即可避免不必要的布局开销。p classNametext-pretty 一个不会在最后一行留下孤词的短段落。 /pTailwindtext-pretty场景使用标题、需要均匀分布的场景text-wrap: balance短到中等文本段落、描述、说明、UI 文字text-wrap: pretty长文本10 行以上、代码块、预格式化文本都不用保持默认7. 字体平滑macOSmacOS 上文本默认渲染偏重。在根布局上应用 antialiased 平滑让所有文本更清晰、更细html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }html classNameantialiased在根上只做一次不要逐个元素应用否则会出现「标题平滑、正文不平滑」的不一致。该属性只影响 macOS 渲染其他平台忽略它可以安全地全局应用。8. Tabular Numbers等宽数字动态更新的数字计数器、价格、计时器、表格列用 tabular-nums 让所有数字等宽防止数值变化时布局跳动.counter { font-variant-numeric: tabular-nums; }span classNametabular-nums{count}/span用 tabular-nums不要用计数器、计时器静态展示数字会更新的价格装饰性大数字含数字的表格列电话号码、邮编数字转场动画版本号v2.1.0计分板、仪表盘注意事项某些字体如 Inter开启该属性后会改变数字外观——尤其是数字1会变宽并居中。这是预期行为且通常有利于对齐但要在具体字体上确认效果。三、Animations可中断、错峰与进出场9. 可中断动画Interruptible Animations用户会在交互中途改变意图动画不可中断会让界面「像坏了一样」。CSS TransitionsCSS 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 transitionkeyframes 只留给一次性序列。10. 入场动画拆分 错峰Split and Stagger不要动画一个巨大的容器。把内容拆成语义块每块单独动画块间约100ms延迟。步骤1)拆分为逻辑组标题、描述、按钮2)错峰约 100ms3) 标题可拆到单词级、约 80ms 错峰4) 组合opacityblurtranslateY完成入场效果。// MotionFramer Motion——错峰入场 function PageHeader() { return ( motion.div initialhidden animatevisible 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) }, }} ButtonGet 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离场应比入场更柔和、更不抢注意力——用户注意力正要移向下一件事。推荐——小幅固定 translateYmotion.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 modepopLayout 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 classNamerelative 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 classNametransition-transform duration-150 ease-out active:scale-[0.96] Click me /buttonmotion.button whileTap{{ scale: 0.96 }} Click me /motion.buttonStatic 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 ); } ButtonClick me/Button {/* 按压缩放 */} Button staticSubmit/Button {/* 无缩放 */}Sim 仓库落点SKILL.md 明确指出按压反馈应归属 emcn 的Button/Chipchromepackages/emcn而不是消费方类。查 packages/emcn/src/components/button/button.tsx 可以看到当前buttonVariants只有transition-colors、hover 态与 focus 处理目前两个组件都没有实现按压缩放——正确的做法是去packages/emcn里提这个能力而不是在每个调用点手写 transform 类。14. 页面加载跳过入场动画在AnimatePresence上用initial{false}防止首次渲染触发入场动画。已经处于默认状态的元素不应在页面加载时动起来只在后续状态变化时动画。{/* 好——图标挂载时不动画只在状态切换时动画 */} AnimatePresence initial{false} modepopLayout 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: alltransition: all迫使浏览器监听每一个属性的变化会在你没打算动画的属性颜色、padding、阴影上产生意外过渡并阻止浏览器优化。/* 好——只过渡变化的部分 */ .button { transition-property: scale, background-color; transition-duration: 150ms; transition-timing-function: ease-out; } /* 坏——过渡一切 */ .button { transition: all 150ms ease-out; }// 好——显式属性 button classNametransition-[scale,background-color] duration-150 ease-out // 坏——过渡一切 button classNametransition duration-150 ease-outTailwindtransition-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-changetransform是是opacity是是filterblur、brightness是是clip-path是是top、left、width、height否否background、border、color否否何时跳过现代浏览器自身优化已经很好。只在观察到首帧卡顿时才加will-change——Safari 尤其受益。不要给每个动画元素都预先加上每多一个合成层都占内存。常见错误速查错误修复父子元素同样圆角计算outerRadius innerRadius padding图标看起来不居中光学微调padding 或直接改 SVG区块之间硬边框Sim 内用--borderhairline token其他项目用带透明度的分层box-shadow生硬的进出场动画拆分、错峰、离场保持克制数字引起布局跳动应用tabular-numsmacOS 上文字偏重根元素应用antialiased页面加载就播动画AnimatePresence加initial{false}元素上有transition: all指定精确属性首帧动画卡顿加will-change: transform克制小控件点击区域太小用伪元素扩展到 40×40px实践UI Review 的输出格式与检查清单Review 输出格式以 Markdown 表格呈现改动带Before / After两列每条原则一个表格并加标题每行一个 diff当代码片段无法自解释时引用文件和具体属性。如果某条原则 review 后无需改动直接省略该表格——空表格只会增加噪音。示例同心圆角BeforeAfter卡片rounded-xl 内按钮rounded-xlp-2卡片rounded-2xl12 8、内按钮rounded-lg两个嵌套表面都是border-radius: 16px外层24px内层16px配8pxpaddingTabular 数字BeforeAfter动画计数器span{count}/spanspan classNametabular-nums{count}/span计时器默认数字根元素加font-variant-numeric: tabular-nums按压缩放BeforeAfterbutton 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),仅供参考
返回列表