morphing-icons by raphaelsalaja/userinterface-wiki
npx skills add https://github.com/raphaelsalaja/userinterface-wiki --skill morphing-icons构建通过实际形状变换而非交叉淡入淡出实现变形的图标。任何图标都可以变形为任何其他图标,因为它们共享相同的底层三线结构。
每个图标完全由三条 SVG 线段构成。需要较少线条的图标会将多余的线条折叠到不可见的中心点。这一约束使得任意两个图标之间能够实现无缝变形。
每条线段都有坐标和可选的不透明度:
interface IconLine {
x1: number;
y1: number;
x2: number;
y2: number;
opacity?: number;
}
需要少于 3 条线的图标使用折叠线段——位于中心点的零长度线段:
const CENTER = 7; // 14x14 视口的中心点
const collapsed: IconLine = {
x1: CENTER,
y1: CENTER,
x2: CENTER,
y2: CENTER,
opacity: 0,
};
每个图标恰好有 3 条线段,可选旋转角度,以及可选分组:
interface IconDefinition {
lines: [IconLine, IconLine, IconLine];
rotation?: number;
group?: string;
}
共享同一个 group 的图标在它们之间切换时会带动画旋转。没有匹配分组的图标会立即跳转到新的旋转角度:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// 这些图标在彼此之间平滑旋转
{ lines: plusLines, rotation: 0, group: "plus-cross" } // 加号
{ lines: plusLines, rotation: 45, group: "plus-cross" } // 叉号
// 这些图标在彼此之间平滑旋转
{ lines: arrowLines, rotation: 0, group: "arrow" } // 右箭头
{ lines: arrowLines, rotation: 90, group: "arrow" } // 下箭头
{ lines: arrowLines, rotation: 180, group: "arrow" } // 左箭头
{ lines: arrowLines, rotation: -90, group: "arrow" } // 上箭头
morphing-three-lines每个图标必须恰好使用 3 条线段。不能多,也不能少。
错误示例:
const checkIcon = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
], // 只有 2 条线段
};
正确示例:
const checkIcon = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
collapsed, // 第三条线段折叠
],
};
morphing-use-collapsed未使用的线段必须使用折叠常量,而不是省略或设为 null。
错误示例:
const minusIcon = {
lines: [
{ x1: 2, y1: 7, x2: 12, y2: 7 },
null,
null,
],
};
正确示例:
const minusIcon = {
lines: [
{ x1: 2, y1: 7, x2: 12, y2: 7 },
collapsed,
collapsed,
],
};
morphing-consistent-viewbox所有图标必须使用相同的 viewBox(推荐 14x14)。
错误示例:
// 混合使用不同比例的视口
const icon1 = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] }; // 14x14
const icon2 = { lines: [{ x1: 4, y1: 14, x2: 24, y2: 14 }, ...] }; // 28x28
正确示例:
const VIEWBOX_SIZE = 14;
const CENTER = 7;
// 所有坐标都在 0-14 范围内
morphing-group-variants属于旋转变体的图标必须共享相同的分组和基础线段定义。
错误示例:
// 箭头使用了不同的线段定义
const arrowRight = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] };
const arrowDown = { lines: [{ x1: 7, y1: 2, x2: 7, y2: 12 }, ...] }; // 不同!
正确示例:
const arrowLines: [IconLine, IconLine, IconLine] = [
{ x1: 2, y1: 7, x2: 12, y2: 7 },
{ x1: 7.5, y1: 2.5, x2: 12, y2: 7 },
{ x1: 7.5, y1: 11.5, x2: 12, y2: 7 },
];
const icons = {
"arrow-right": { lines: arrowLines, rotation: 0, group: "arrow" },
"arrow-down": { lines: arrowLines, rotation: 90, group: "arrow" },
"arrow-left": { lines: arrowLines, rotation: 180, group: "arrow" },
"arrow-up": { lines: arrowLines, rotation: -90, group: "arrow" },
};
morphing-spring-rotation分组图标之间的旋转应使用弹簧物理效果以实现自然运动。
错误示例:
<motion.g animate={{ rotate: rotation }} transition={{ duration: 0.3 }} />
正确示例:
const rotation = useSpring(definition.rotation ?? 0, activeTransition);
<motion.g style={{ rotate: rotation, transformOrigin: "center" }} />
morphing-reduced-motion通过禁用动画来尊重 prefers-reduced-motion 设置。
错误示例:
function MorphingIcon({ icon }: Props) {
return <motion.line animate={...} transition={{ duration: 0.4 }} />;
}
正确示例:
function MorphingIcon({ icon }: Props) {
const reducedMotion = useReducedMotion() ?? false;
const activeTransition = reducedMotion ? { duration: 0 } : transition;
return <motion.line animate={...} transition={activeTransition} />;
}
morphing-jump-non-grouped当在不属于同一分组的图标之间切换时,旋转应立即跳转。
错误示例:
// 无论分组如何,总是动画旋转
useEffect(() => {
rotation.set(definition.rotation ?? 0);
}, [definition]);
正确示例:
useEffect(() => {
if (shouldRotate) {
rotation.set(definition.rotation ?? 0); // 动画
} else {
rotation.jump(definition.rotation ?? 0); // 立即
}
}, [definition, shouldRotate]);
morphing-strokelinecap-round线段应使用 strokeLinecap="round" 以获得圆滑的端点。
错误示例:
<motion.line strokeLinecap="butt" />
正确示例:
<motion.line strokeLinecap="round" />
morphing-aria-hidden图标 SVG 应为 aria-hidden,因为它们是装饰性的。
错误示例:
<svg width={size} height={size}>...</svg>
正确示例:
<svg width={size} height={size} aria-hidden="true">...</svg>
使用一条或两条折叠线段:
const check = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
collapsed,
],
};
使用全部三条线段:
const menu = {
lines: [
{ x1: 2, y1: 3.5, x2: 12, y2: 3.5 },
{ x1: 2, y1: 7, x2: 12, y2: 7 },
{ x1: 2, y1: 10.5, x2: 12, y2: 10.5 },
],
};
使用零长度线段作为点:
const more = {
lines: [
{ x1: 3, y1: 7, x2: 3, y2: 7 },
{ x1: 7, y1: 7, x2: 7, y2: 7 },
{ x1: 11, y1: 7, x2: 11, y2: 7 },
],
};
使用指数缓出函数实现平滑变形:
const defaultTransition: Transition = {
ease: [0.19, 1, 0.22, 1],
duration: 0.4,
};
在审核变形图标实现时,按以下格式输出发现的问题:
file:line - [rule-id] description of issue
示例:
components/icon/index.tsx:45 - [morphing-three-lines] 图标 "check" 只有 2 条线段,需要折叠第三条
components/icon/index.tsx:78 - [morphing-group-variants] arrow-down 使用了与 arrow-right 不同的线段定义
在发现问题后,输出一个汇总:
| 规则 | 数量 | 严重程度 |
|---|---|---|
morphing-three-lines | 2 | 高 |
morphing-group-variants | 1 | 高 |
morphing-reduced-motion | 1 | 中 |
每周安装量
120
代码仓库
GitHub 星标数
633
首次出现
2026年2月1日
安全审计
安装于
opencode107
codex98
cursor98
gemini-cli94
claude-code91
github-copilot84
Build icons that transform through actual shape transformation, not crossfades. Any icon can morph into any other because they share the same underlying 3-line structure.
Every icon is composed of exactly three SVG lines. Icons that need fewer lines collapse the extras to invisible center points. This constraint enables seamless morphing between any two icons.
Each line has coordinates and optional opacity:
interface IconLine {
x1: number;
y1: number;
x2: number;
y2: number;
opacity?: number;
}
Icons needing fewer than 3 lines use collapsed lines—zero-length lines at the center:
const CENTER = 7; // Center of 14x14 viewbox
const collapsed: IconLine = {
x1: CENTER,
y1: CENTER,
x2: CENTER,
y2: CENTER,
opacity: 0,
};
Each icon has exactly 3 lines, optional rotation, and optional group:
interface IconDefinition {
lines: [IconLine, IconLine, IconLine];
rotation?: number;
group?: string;
}
Icons sharing a group animate rotation when transitioning between them. Icons without matching groups jump to the new rotation instantly:
// These rotate smoothly between each other
{ lines: plusLines, rotation: 0, group: "plus-cross" } // plus
{ lines: plusLines, rotation: 45, group: "plus-cross" } // cross
// These rotate smoothly between each other
{ lines: arrowLines, rotation: 0, group: "arrow" } // arrow-right
{ lines: arrowLines, rotation: 90, group: "arrow" } // arrow-down
{ lines: arrowLines, rotation: 180, group: "arrow" } // arrow-left
{ lines: arrowLines, rotation: -90, group: "arrow" } // arrow-up
morphing-three-linesEvery icon MUST use exactly 3 lines. No more, no fewer.
Fail:
const checkIcon = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
], // Only 2 lines
};
Pass:
const checkIcon = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
collapsed, // Third line collapsed
],
};
morphing-use-collapsedUnused lines must use the collapsed constant, not omission or null.
Fail:
const minusIcon = {
lines: [
{ x1: 2, y1: 7, x2: 12, y2: 7 },
null,
null,
],
};
Pass:
const minusIcon = {
lines: [
{ x1: 2, y1: 7, x2: 12, y2: 7 },
collapsed,
collapsed,
],
};
morphing-consistent-viewboxAll icons must use the same viewBox (14x14 recommended).
Fail:
// Mixing viewbox scales
const icon1 = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] }; // 14x14
const icon2 = { lines: [{ x1: 4, y1: 14, x2: 24, y2: 14 }, ...] }; // 28x28
Pass:
const VIEWBOX_SIZE = 14;
const CENTER = 7;
// All coordinates within 0-14 range
morphing-group-variantsIcons that are rotational variants MUST share the same group and base lines.
Fail:
// Different line definitions for arrows
const arrowRight = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] };
const arrowDown = { lines: [{ x1: 7, y1: 2, x2: 7, y2: 12 }, ...] }; // Different!
Pass:
const arrowLines: [IconLine, IconLine, IconLine] = [
{ x1: 2, y1: 7, x2: 12, y2: 7 },
{ x1: 7.5, y1: 2.5, x2: 12, y2: 7 },
{ x1: 7.5, y1: 11.5, x2: 12, y2: 7 },
];
const icons = {
"arrow-right": { lines: arrowLines, rotation: 0, group: "arrow" },
"arrow-down": { lines: arrowLines, rotation: 90, group: "arrow" },
"arrow-left": { lines: arrowLines, rotation: 180, group: "arrow" },
"arrow-up": { lines: arrowLines, rotation: -90, group: "arrow" },
};
morphing-spring-rotationRotation between grouped icons should use spring physics for natural motion.
Fail:
<motion.g animate={{ rotate: rotation }} transition={{ duration: 0.3 }} />
Pass:
const rotation = useSpring(definition.rotation ?? 0, activeTransition);
<motion.g style={{ rotate: rotation, transformOrigin: "center" }} />
morphing-reduced-motionRespect prefers-reduced-motion by disabling animations.
Fail:
function MorphingIcon({ icon }: Props) {
return <motion.line animate={...} transition={{ duration: 0.4 }} />;
}
Pass:
function MorphingIcon({ icon }: Props) {
const reducedMotion = useReducedMotion() ?? false;
const activeTransition = reducedMotion ? { duration: 0 } : transition;
return <motion.line animate={...} transition={activeTransition} />;
}
morphing-jump-non-groupedWhen transitioning between icons NOT in the same group, rotation should jump instantly.
Fail:
// Always animating rotation regardless of group
useEffect(() => {
rotation.set(definition.rotation ?? 0);
}, [definition]);
Pass:
useEffect(() => {
if (shouldRotate) {
rotation.set(definition.rotation ?? 0); // Animate
} else {
rotation.jump(definition.rotation ?? 0); // Instant
}
}, [definition, shouldRotate]);
morphing-strokelinecap-roundLines should use strokeLinecap="round" for polished endpoints.
Fail:
<motion.line strokeLinecap="butt" />
Pass:
<motion.line strokeLinecap="round" />
morphing-aria-hiddenIcon SVGs should be aria-hidden since they're decorative.
Fail:
<svg width={size} height={size}>...</svg>
Pass:
<svg width={size} height={size} aria-hidden="true">...</svg>
Use one or two collapsed lines:
const check = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
collapsed,
],
};
Use all three lines:
const menu = {
lines: [
{ x1: 2, y1: 3.5, x2: 12, y2: 3.5 },
{ x1: 2, y1: 7, x2: 12, y2: 7 },
{ x1: 2, y1: 10.5, x2: 12, y2: 10.5 },
],
};
Use zero-length lines as dots:
const more = {
lines: [
{ x1: 3, y1: 7, x2: 3, y2: 7 },
{ x1: 7, y1: 7, x2: 7, y2: 7 },
{ x1: 11, y1: 7, x2: 11, y2: 7 },
],
};
Use exponential ease-out for smooth morphing:
const defaultTransition: Transition = {
ease: [0.19, 1, 0.22, 1],
duration: 0.4,
};
When auditing morphing icon implementations, output findings as:
file:line - [rule-id] description of issue
Example:
components/icon/index.tsx:45 - [morphing-three-lines] Icon "check" has only 2 lines, needs collapsed third
components/icon/index.tsx:78 - [morphing-group-variants] arrow-down uses different line definitions than arrow-right
After findings, output a summary:
| Rule | Count | Severity |
|---|---|---|
morphing-three-lines | 2 | HIGH |
morphing-group-variants | 1 | HIGH |
morphing-reduced-motion | 1 | MEDIUM |
Weekly Installs
120
Repository
GitHub Stars
633
First Seen
Feb 1, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode107
codex98
cursor98
gemini-cli94
claude-code91
github-copilot84
HeroUI v2 到 v3 迁移指南:破坏性变更、复合组件、Tailwind v4 升级
490 周安装