This document describes how Lucide-based animated icons are wired in NEO so they work on web and native without breaking react-native-svg on the web.
Use animated icons for nav items, tabs, or buttons where Icon is called with animated={true} and optional scene classes. Static icons keep using packages/app/ui/atoms/icon.tsx (with iconset.tsx / iconset.web.tsx) as usual.
Icon receives animated, className, and interaction props (active, selected, hovered, pressed). When animated is true and the icon resolves to a string Lucide name, it looks up a component from the registry (see below).className are parsed by parseIconSceneClasses in packages/app/ui/atoms/icon.tsx. Tokens like icon-scene-fill and icon-scene-draw are turned into a scenes object and stripped from the class string passed to the animated component.scenes.draw is set and hovered is not controlled from the parent, Icon attaches onMouseEnter / onMouseLeave on web so hover-driven animations run without a parent tracking hover.scenes, active (often from route/selection), and merged hovered.packages/app/default/animated-icons.js — exports animatedIcons keyed by Lucide component names as returned after findIconFromRemote (e.g. House, Compass). Add new entries here for upstream NEO.Forks / custom apps: extend via packages/app/customization/animated-icons.js by spreading the default animatedIcons and adding or overriding keys. Upstream NEO keeps that file a plain re-export (see Forks and the customization layer).
import { animatedIcons as animatedIconsDefault } from 'app/default/animated-icons';
import { AnimatedMyIcon } from 'app/ui/atoms/animated-icons/icons/my-icon';
export const animatedIcons = {
...animatedIconsDefault,
MyIcon: AnimatedMyIcon,
};
If a name is missing from the registry, Icon falls back to the normal (non-animated) Lucide path. An icon of the same name in customization/icons-svg.js wins over the animated registry, so a fork's custom icon is never animated.
Default registry keys (upstream NEO): House, Compass, TvMinimalPlay, Store, Shapes, Calendar, Bell, Info, Mail, Menu, MessageCircleMore, UsersRound (see packages/app/default/animated-icons.js).
animated: true on the item (e.g. in packages/app/settings/menus.js or tab config) where the row passes props into Icon. That enables fill (active/selected duotone) and draw (hover path / motion on web) by default.className / addClassName tokens such as icon-scene-draw (draw only) or icon-scene-fill. Any explicit icon-scene-* token replaces the default set. Do not prefix these with web: / group-hover: — they are not CSS utilities; hover is driven by the hovered prop / mouse handlers in Icon.Scene names supported by the parser: fill, draw, morph, smoke, custom1–custom6 (see parseIconSceneClasses in packages/app/ui/atoms/icon.tsx).
Animated* componentReference implementations:
packages/app/ui/atoms/animated-icons/icons/house.tsx — fill opacity, scale, and draw (stroke dash) on hover.packages/app/ui/atoms/animated-icons/icons/compass.tsx — fill opacity, rotation, paths aligned with Lucide.Guidelines:
'use client' — animated icons use React state / Animated from react-native.lucide-react-native on native, lucide-static on web) so geometry matches the static icon.currentColor / theme-driven stroke and fill; avoid hardcoded palette colors.fill / fillOpacity from getAnimatedFillProps (animated-icons/fill-props.ts), not ad hoc SVG elements that render differently on web vs native. The fill fades on web only: react-native-svg ignores an animated fillOpacity on native, so native switches between fill="none" and a fixed 0.2 opacity.AnimatedPath, AnimatedG, AnimatedRect and AnimatedCircle from animated-icons/shared.tsx. They strip collapsable / onLayout before the props reach a web DOM <svg> element.originX / originY on svg <G> for pivot rotation. On web, react-native-svg can emit DOM properties that conflict with React 19. Use a nested group: translate to the pivot, apply rotation, translate back (see Compass).duration, easing, or spring friction / tension inside the icon file; keep durations in a sensible range (roughly hundreds of ms for UI feedback unless intentionally slow).House, Compass, …).animated (fill + draw are default; override with icon-scene-* only when needed).Platform.OS).measureLayout / layout thrash introduced from the icon itself (icons should stay self-contained).For general icon and styling rules, see AGENTS.md.