design-with-taste by cristicretu/family-taste-skill
npx skills add https://github.com/cristicretu/family-taste-skill --skill design-with-taste这项技能编码了 Family 背后的设计哲学——这是一款因其感觉生动、热情和有意为之而广受好评的产品。最初由 Benji Taylor 在 benji.org/family-values 记录。
在编写任何 UI 代码之前,请先阅读本文。每次都要读。
用户想要构建某个东西。你的工作是让它感觉像是由一个真正在乎的人设计的。
按优先级排序。没有流畅性,就没有愉悦感;没有简洁性,就没有流畅性。
"用户的每个操作都会使界面展开和演变,就像走过一系列相互连接的房间。"
问题所在:大多数 UI 一次性倾倒所有内容——每个功能、每个选项、每个边缘情况,全部可见,始终如此。这会将认知负担从设计师转移到用户身上。
原则:只展示当前重要的内容。界面应该感觉像走过一个个房间——在到达之前,你就能瞥见接下来是什么。
规则:
每个视图只有一个主要操作。 两个同等权重的 CTA = 失败。使其他所有内容都成为次要的。
渐进式披露优于功能堆砌。 分层托盘、分步流程、可展开部分。当 3 个步骤,每步 4 个字段可行时,绝不要展示一个 12 个字段的表单。
上下文保留的覆盖层优于全页面导航。 覆盖当前上下文的表单/托盘/模态框能使用户保持方向感。全屏过渡会让他们迷失方向。
改变堆叠层的高度。 每个后续的表单/托盘必须具有明显不同的高度,以便进展清晰无误。切勿堆叠两个相同高度的层。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
每个表单/托盘/模态框都需要一个标题和关闭操作。 用户必须始终知道他们在看什么以及如何返回。
托盘适应上下文。 出现在深色主题流程中的托盘应采用较深的配色方案。视觉环境跟随用户。
托盘可以启动全屏流程。 一个紧凑的托盘是多步骤全屏体验的有效入口点——不要强制在"托盘"和"页面"之间做二元选择。
使用托盘处理临时操作;使用全屏处理持久性目的地。 确认、警告和上下文信息 = 托盘。设置、核心内容 = 全屏。
// 良好示例:渐进式托盘 —— 紧凑、专注、上下文感知 <Sheet> <SheetTrigger>确认发送</SheetTrigger> <SheetContent className="h-[45vh]"> {/* 高度与父级不同 /} <SheetHeader> <SheetTitle>审核交易</SheetTitle> <DismissButton /> </SheetHeader> {/ 仅核心信息 —— 无额外内容 */} <Button>发送 $42.00</Button> </SheetContent> </Sheet>
自检:用户能否在 1 秒内知道下一步该做什么?如果不能,请简化。
"我们飞行,而不是传送。"
问题所在:静态过渡使产品感觉死气沉沉。死气沉沉的产品感觉不受重视。瞬间切换会破坏空间方向感——那个东西从哪里来?它去了哪里?
原则:将你的应用视为一个具有不可打破的物理规则的空间。在添加过渡之前,要知道它为什么在架构上是有意义的。每个元素都从某个地方移动到另一个地方。
规则:
没有瞬间显示/隐藏。 每个出现或消失的元素都必须有动画。选择一个在空间上有意义的过渡效果——淡入淡出、滑动、缩放、变形。
共享元素过渡。 如果一个元素同时存在于状态 A 和状态 B(例如,展开的卡片、变成托盘的按钮),它必须在视觉上在两者之间移动。切勿卸载并重新挂载——要变形。
方向一致性。 向右导航(下一步,下一个标签页)→ 内容从右侧进入。返回 → 内容从左侧进入。当前标签页左侧的标签页向左滑动。这有助于建立空间记忆。
文本变形优于瞬间替换。 当按钮标签改变时(例如,"继续" → "确认"),动画化过渡过程。识别共享的字母序列("Con")——保持它们固定,同时其余部分变形。使用 torph (npm i torph) —— 无依赖,适用于 React/Vue/Svelte。交叉淡入淡出是最低限度的后备方案;共享字母变形是理想方案。
部分文本更改:仅动画化更改的部分。 如果一个句子增加或减少一个单词,保持未更改的部分静止。动画化未更改的文本会产生令人不适的冗余感。
持久元素保持原位。 如果页眉、卡片或组件在过渡过程中持续存在,它不得动画化消失再出现。只有变化的部分移动。
加载状态移动到其目的地。 加载指示器不只是停留在触发它的地方——它移动到用户将寻找结果的地方(例如,提交交易后,加载指示器移动到活动标签页图标处)。
微方向提示。 V 形符号、箭头和插入符应动画化以反映所采取的操作。返回导航时,→ 变成 ←。手风琴的 V 形符号在展开时旋转。
统一插值。 所有由相同数据驱动的视觉元素应共享相同的 lerp/缓动函数。这使界面感觉像一个整体在呼吸,而不是一堆独立更新的部件。当值改变时,线条、标签、轴和徽章都应作为一个整体移动。
// 文本变形 —— 使用 torph import { TextMorph } from 'torph/react'; <TextMorph>{label}</TextMorph> // 自动处理共享字母动画
// 方向性标签页过渡 const direction = newIndex > currentIndex ? 1 : -1; <motion.div key={currentTab} initial={{ x: direction * 20, opacity: 0 }} animate={{ x: 0, opacity: 1 }} exit={{ x: -direction * 20, opacity: 0 }} transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }} />
// 共享元素:卡片 → 详情视图
<motion.div
layoutId={card-${id}}
className={isExpanded ? "fixed inset-0 rounded-none" : "rounded-xl"}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
/>
黄金缓动曲线:cubic-bezier(0.16, 1, 0.3, 1) —— 快速启动,平缓停止。所有进入和变形动画的默认值。仅对退出动画使用 ease-in (cubic-bezier(0.4, 0, 1, 1))。切勿使用线性。
自检:录制你的屏幕并以 0.5 倍速播放。你能跟踪每个元素的旅程吗?任何传送的元素都需要过渡。
"掌握愉悦感就是掌握选择性强调。"
问题所在:要么毫无个性(企业式敷衍),要么所有东西都在跳动和闪烁(令人厌烦)。两者都不得要领。
原则:愉悦感-影响曲线 —— 功能使用频率越低,其愉悦感应越强。日常操作需要效率,并辅以细微的触动。罕见的时刻值得戏剧性的触动。
愉悦感 ↑
| * (罕见功能:戏剧性)
| *
| *
| * * (中等:令人难忘)
| * *
|* * * (频繁:细微)
+------------------→ 功能使用频率
规则:
npm i liveline) —— 一个画布,除了 React 18 外无依赖,60fps 插值。对于 60fps 的数值叠加,直接更新 DOM 而不是通过 React 状态,以避免重新渲染的开销。愉悦感模式库 —— 经过验证有效的具体瞬间:
| 功能 | 使用频率 | 愉悦感等级 | 模式 |
|---|---|---|---|
| 数字输入 | 每日 | 细微 | 输入数字时逗号移动位置 |
| 标签页/图表导航 | 每日 | 细微 | 箭头图标随数值变化翻转方向 |
| 空状态 | 首次访问 | 中等 | 动画箭头 + 漂浮插图 |
| 项目重新排序 | 偶尔 | 中等 | 堆叠动画 + 平滑放置 |
| 删除/垃圾桶 | 偶尔 | 中等 | 项目翻滚进拟物化垃圾桶 + 音效 |
| 首次使用功能 | 一次 | 高 | 空状态中的动画引导箭头 |
| 关键完成(备份、新手指引) | 一次 | 戏剧性 | 彩带爆炸 + 庆祝音效 |
| 彩蛋(二维码、隐藏手势) | 罕见 | 戏剧性 | 点击时涟漪 → 滑动时亮片效果 |
// 带有平滑逗号移动的动画数字
function AnimatedNumber({ value }) {
const spring = useSpring(value, { stiffness: 80, damping: 20 });
return <motion.span>{useTransform(spring, v => Math.round(v).toLocaleString())}</motion.span>;
}
// 实时图表 —— liveline 处理插值、动量箭头、擦洗、主题
import { Liveline } from 'liveline';
<div style={{ height: 200 }}>
<Liveline
data={history} // [{ time, value }]
value={latestValue} // 当前数字
momentum // 方向箭头(绿色/红色/灰色)
showValue // 60fps DOM 叠加,无重新渲染
color="#3b82f6" // 从一种颜色衍生出完整调色板
/>
</div>
// 令人满意的空状态
function EmptyState() {
return (
<div className="flex flex-col items-center gap-4 py-16">
<motion.div animate={{ y: [0, -8, 0] }} transition={{ repeat: Infinity, duration: 2, ease: "easeInOut" }}>
<IllustrationIcon />
</motion.div>
<p className="text-muted">这里还没有内容</p>
<motion.div animate={{ x: [0, 5, 0] }} transition={{ repeat: Infinity, duration: 1.5 }}>
<ArrowRight className="inline mr-1" /> 创建你的第一个项目
</motion.div>
</div>
);
}
// 重要完成时的彩带
function CompletionScreen() {
useEffect(() => { playSound('success'); }, []);
return (
<motion.div initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", damping: 15, stiffness: 200 }}>
<ConfettiExplosion />
<h2>全部设置完成!</h2>
</motion.div>
);
}
自检:将你的 UI 展示给某人看 30 秒。他们微笑了吗?如果没有,添加愉悦感。他们看起来厌烦吗?你对一个高频交互过度愉悦化了。
在认为任何 UI"完成"之前运行:
cubic-bezier(0.16, 1, 0.3, 1)opacity: 0 → 1。| 使用场景 | 缓动 | 持续时间 |
|---|---|---|
| 元素进入 | cubic-bezier(0.16, 1, 0.3, 1) | 300–400ms |
| 元素退出 | cubic-bezier(0.4, 0, 1, 1) | 200–250ms |
| 共享元素变形 | cubic-bezier(0.16, 1, 0.3, 1) | 350–500ms |
| 微交互(悬停、按下) | cubic-bezier(0.2, 0, 0, 1) | 100–150ms |
| 弹簧(有弹性) | damping: 20, stiffness: 300 | 自动 |
| 弹簧(平滑) | damping: 30, stiffness: 200 | 自动 |
| 数字计数 | ease-out cubic | 400–800ms |
| 页面过渡 | cubic-bezier(0.16, 1, 0.3, 1) | 300ms |
| 项目间错开 | — | 每个项目 30–60ms |
这些库由 Family 背后的同一批人构建,并体现了相同的理念:
当构建任何涉及文本更改或实时数字/图表数据的内容时,在自行开发之前先考虑使用这些工具。
frontend-design 技能配对 用于视觉美学(排版、颜色、布局)。此技能处理感觉和交互质量。目标不是制造一个"能用"的东西。目标是制造一个有人使用后会想:"制作这个东西的人真的在乎。"
这就是品味。
每周安装次数
140
代码仓库
GitHub 星标数
6
首次出现
2026年2月18日
安全审计
安装于
opencode131
codex130
gemini-cli129
github-copilot118
amp115
kimi-cli114
This skill encodes the design philosophy behind Family — a product widely praised for feeling alive , welcoming , and intentional. Originally documented by Benji Taylor at benji.org/family-values.
Read this before writing any UI code. Every time.
The user wants something built. Your job is to make it feel like a human who gives a shit designed it.
Ordered by priority. You cannot have Delight without Fluidity, and you cannot have Fluidity without Simplicity.
"Each action by the user makes the interface unfold and evolve, much like walking through a series of interconnected rooms."
The problem : Most UIs dump everything at once — every feature, every option, every edge case, all visible, all the time. This transfers cognitive burden from the designer to the user.
The principle : Show only what matters right now. The interface should feel like walking through rooms — you glimpse what's next before you arrive.
Rules :
One primary action per view. Two equally weighted CTAs = failure. Make everything else secondary.
Progressive disclosure over feature dumps. Layered trays, step-by-step flows, expandable sections. Never show a 12-field form when 3 steps of 4 fields works.
Context-preserving overlays over full-page navigations. Sheets/trays/modals that overlay the current context keep users oriented. Full-screen transitions displace them.
Vary heights of stacked layers. Each subsequent sheet/tray must be a visibly different height so the progression is unmistakably clear. Never stack two identical-height layers.
Every sheet/tray/modal needs a title and dismiss action. Users must always know what they're looking at and how to get back.
Trays adapt to context. A tray appearing within a dark-themed flow should adopt a darker color scheme. The visual environment follows the user.
Trays can launch full-screen flows. A compact tray is a valid entry point for a multi-step full-screen experience — don't force a binary choice between "tray" and "page."
Use trays for transient actions; full screens for persistent destinations. Confirmations, warnings, and contextual info = tray. Settings, core content = full screen.
// GOOD: Progressive tray — compact, focused, context-aware <Sheet> <SheetTrigger>Confirm Send</SheetTrigger> <SheetContent className="h-[45vh]"> {/* height varies from parent /} <SheetHeader> <SheetTitle>Review Transaction</SheetTitle> <DismissButton /> </SheetHeader> {/ Core info only — no extras */} <Button>Send $42.00</Button> </SheetContent> </Sheet>
Self-check : Can the user tell within 1 second what to do next? If not, simplify.
"We fly instead of teleport."
The problem : Static transitions make products feel dead. A dead product feels uncared for. Instant cuts destroy spatial orientation — where did that come from? Where did it go?
The principle : Treat your app as a space with unbreakable physical rules. Know why a transition makes sense architecturally before adding it. Every element moves from somewhere to somewhere.
Rules :
No instant show/hide. Every element that appears or disappears must animate. Pick a transition that makes spatial sense — fade, slide, scale, morph.
Shared element transitions. If an element exists in both State A and State B (a card that expands, a button that becomes a sheet), it must visually travel between them. Never unmount and remount — morph.
Directional consistency. Navigate right (next step, next tab) → content enters from right. Go back → content enters from left. Tabs to the left of current slide left. This builds spatial memory.
Text morphing over instant replacement. When button labels change (e.g., "Continue" → "Confirm"), animate the transition. Identify shared letter sequences ("Con") — keep them fixed while the rest morphs. Use torph (npm i torph) — dependency-free, works with React/Vue/Svelte. Crossfade is the minimum fallback; shared-letter morphing is the ideal.
Partial text changes: only animate what changes. If a sentence gains or loses a word, keep the unchanged portion static. Animating unchanged text creates jarring redundancy.
Persistent elements stay put. If a header, card, or component persists across a transition, it must NOT animate out and back in. Only the changing parts move.
Loading states travel to their destination. A spinner doesn't just sit where triggered — it moves to where the user will look for results (e.g., after submitting a transaction, the spinner migrates to the activity tab icon).
Micro-directional cues. Chevrons, arrows, and carets should animate to reflect the action taken. A becomes a on back-navigation. An accordion chevron rotates on expand.
The golden easing curve : cubic-bezier(0.16, 1, 0.3, 1) — fast start, gentle settle. Default for all entrances and morphs. Use ease-in (cubic-bezier(0.4, 0, 1, 1)) for exits only. Never use linear.
Self-check : Record your screen and play back at 0.5x speed. Can you follow every element's journey? Anything that teleports needs a transition.
"Mastering delight is mastering selective emphasis."
The problem : Either zero personality (corporate slop) or everything bounces and sparkles (annoying). Both miss the point.
The principle : The Delight-Impact Curve — the less frequently a feature is used, the more delightful it should be. Daily actions need efficiency with subtle touches. Rare moments deserve theatrical ones.
Delight ↑
| * (rare features: theatrical)
| *
| *
| * * (medium: memorable)
| * *
|* * * (frequent: subtle)
+------------------→ Feature frequency
Rules :
npm i liveline) — one canvas, no dependencies beyond React 18, 60fps interpolation. For 60fps value overlays, update the DOM directly rather than through React state to avoid re-render overhead.Delight pattern library — concrete moments proven to work:
| Feature | Frequency | Delight Level | Pattern |
|---|---|---|---|
| Number input | Daily | Subtle | Commas shift position as digits are typed |
| Tab/chart navigation | Daily | Subtle | Arrow icon flips direction with value change |
| Empty state | First visit | Medium | Animated arrow + floating illustration |
| Item reorder | Occasional | Medium | Stacking animation + smooth drop |
| Delete/trash | Occasional | Medium | Item tumbles into skeuomorphic trash + sound |
| First feature use | Once | High | Animated guide arrow in empty state |
| Critical completion (backup, onboarding) | Once |
// Animated number with smooth comma shifting
function AnimatedNumber({ value }) {
const spring = useSpring(value, { stiffness: 80, damping: 20 });
return <motion.span>{useTransform(spring, v => Math.round(v).toLocaleString())}</motion.span>;
}
// Real-time chart — liveline handles interpolation, momentum arrows, scrub, theming
import { Liveline } from 'liveline';
<div style={{ height: 200 }}>
<Liveline
data={history} // [{ time, value }]
value={latestValue} // current number
momentum // directional arrows (green/red/grey)
showValue // 60fps DOM overlay, no re-renders
color="#3b82f6" // derives full palette from one color
/>
</div>
// Satisfying empty state
function EmptyState() {
return (
<div className="flex flex-col items-center gap-4 py-16">
<motion.div animate={{ y: [0, -8, 0] }} transition={{ repeat: Infinity, duration: 2, ease: "easeInOut" }}>
<IllustrationIcon />
</motion.div>
<p className="text-muted">Nothing here yet</p>
<motion.div animate={{ x: [0, 5, 0] }} transition={{ repeat: Infinity, duration: 1.5 }}>
<ArrowRight className="inline mr-1" /> Create your first item
</motion.div>
</div>
);
}
// Confetti on significant completion
function CompletionScreen() {
useEffect(() => { playSound('success'); }, []);
return (
<motion.div initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", damping: 15, stiffness: 200 }}>
<ConfettiExplosion />
<h2>You're all set!</h2>
</motion.div>
);
}
Self-check : Show your UI to someone for 30 seconds. Do they smile? If not, add delight. Do they look annoyed? You over-delighted a high-frequency interaction.
Run before considering any UI "done":
cubic-bezier(0.16, 1, 0.3, 1)opacity: 0 → 1 centered.| Use Case | Easing | Duration |
|---|---|---|
| Element entering | cubic-bezier(0.16, 1, 0.3, 1) | 300–400ms |
| Element exiting | cubic-bezier(0.4, 0, 1, 1) | 200–250ms |
| Shared element morph | cubic-bezier(0.16, 1, 0.3, 1) | 350–500ms |
| Micro-interaction (hover, press) | cubic-bezier(0.2, 0, 0, 1) | 100–150ms |
| Spring (bouncy) | damping: 20, stiffness: 300 |
These libraries are built by the same people behind Family and embody the same philosophy:
| Library | Purpose | Install |
|---|---|---|
| torph | Dependency-free text morphing. Handles shared-letter transitions automatically. React, Vue, Svelte. | npm i torph |
| liveline | Real-time animated line charts. One canvas, 60fps lerp, momentum arrows, no dependencies beyond React 18. | npm i liveline |
When building anything with text that changes or live numeric/chart data, reach for these before rolling your own.
frontend-design skill for visual aesthetics (typography, color, layout). This skill handles feel and interaction quality.The goal is not to make something that "works." The goal is to make something that someone uses and thinks: "Whoever made this actually gives a shit."
That's taste.
Weekly Installs
140
Repository
GitHub Stars
6
First Seen
Feb 18, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode131
codex130
gemini-cli129
github-copilot118
amp115
kimi-cli114
前端设计技能指南:避免AI垃圾美学,打造独特生产级界面
41,300 周安装
Docker安全指南:全面容器安全最佳实践、漏洞扫描与合规性要求
177 周安装
iOS开发专家技能:精通Swift 6、SwiftUI与原生应用开发,涵盖架构、性能与App Store合规
177 周安装
describe技能:AI驱动结构化测试用例生成,提升代码质量与评审效率
2 周安装
专业 README 生成器 | 支持 Rust/TypeScript/Python 项目,自动应用最佳实践
2 周安装
Django 6 升级指南:从 Django 5 迁移的完整步骤与重大变更解析
1 周安装
GitLab DAG与并行处理指南:needs与parallel优化CI/CD流水线速度
2 周安装
→←Unified interpolation. All visual elements driven by the same data should share the same lerp/easing. This makes the interface feel like one thing breathing rather than a bunch of parts updating independently. When the value changes, the line, the label, the axis, and the badge should all move as one.
// Text morphing — use torph import { TextMorph } from 'torph/react'; <TextMorph>{label}</TextMorph> // handles shared-letter animation automatically
// Directional tab transitions const direction = newIndex > currentIndex ? 1 : -1; <motion.div key={currentTab} initial={{ x: direction * 20, opacity: 0 }} animate={{ x: 0, opacity: 1 }} exit={{ x: -direction * 20, opacity: 0 }} transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }} />
// Shared element: card → detail view
<motion.div
layoutId={card-${id}}
className={isExpanded ? "fixed inset-0 rounded-none" : "rounded-xl"}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
/>
| Theatrical |
| Confetti explosion + celebratory sound |
| Easter egg (QR, hidden gesture) | Rare | Theatrical | Ripple on tap → sequin effect on swipe |
| auto |
| Spring (smooth) | damping: 30, stiffness: 200 | auto |
| Number counting | ease-out cubic | 400–800ms |
| Page transition | cubic-bezier(0.16, 1, 0.3, 1) | 300ms |
| Stagger between items | — | 30–60ms per item |