Loading...
Loading...
Build icon components where any icon morphs into any other through SVG line transformation. Use when asked to "create morphing icons", "build icon transitions", "animate between icons", or "transform icons".
npx skill4agent add raphaelsalaja/userinterface-wiki morphing-iconsinterface IconLine {
x1: number;
y1: number;
x2: number;
y2: number;
opacity?: number;
}const CENTER = 7; // Center of 14x14 viewbox
const collapsed: IconLine = {
x1: CENTER,
y1: CENTER,
x2: CENTER,
y2: CENTER,
opacity: 0,
};interface IconDefinition {
lines: [IconLine, IconLine, IconLine];
rotation?: number;
group?: string;
}group// 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-upmorphing-three-linesconst checkIcon = {
lines: [
{ x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
{ x1: 5.5, y1: 11, x2: 12, y2: 3 },
], // Only 2 lines
};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-collapsedconst 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// 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 }, ...] }; // 28x28const VIEWBOX_SIZE = 14;
const CENTER = 7;
// All coordinates within 0-14 rangemorphing-group-variants// 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!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-motionprefers-reduced-motionfunction 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// Always animating rotation regardless of group
useEffect(() => {
rotation.set(definition.rotation ?? 0);
}, [definition]);useEffect(() => {
if (shouldRotate) {
rotation.set(definition.rotation ?? 0); // Animate
} else {
rotation.jump(definition.rotation ?? 0); // Instant
}
}, [definition, shouldRotate]);morphing-strokelinecap-roundstrokeLinecap="round"<motion.line strokeLinecap="butt" /><motion.line strokeLinecap="round" />morphing-aria-hiddenaria-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
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| Rule | Count | Severity |
|---|---|---|
| 2 | HIGH |
| 1 | HIGH |
| 1 | MEDIUM |