Reduced Motion Support
Anicon icons automatically respect the user’s motion preferences using Framer Motion’s useReducedMotion() hook. When a user has enabled “reduce motion” in their system settings, animations are completely disabled.
How It Works
Every icon checks the user’s motion preference:
import { motion , useReducedMotion } from "framer-motion" ;
export function IconHeart ({ size = 24 , strokeWidth = 2 , ... props }) {
const prefersReducedMotion = useReducedMotion ();
return (
< motion.svg
// Disable animation variants when reduced motion is preferred
initial = { prefersReducedMotion ? false : "rest" }
animate = { prefersReducedMotion ? false : "rest" }
whileHover = { prefersReducedMotion ? undefined : "hover" }
whileTap = { prefersReducedMotion ? undefined : "tap" }
variants = { beatVariants }
{ ... props }
>
{ /* Icon paths */ }
</ motion.svg >
);
}
Testing Reduced Motion
To test reduced motion support:
macOS
Windows
Developer Tools
Open System Settings
Go to Accessibility → Display
Enable Reduce motion
Refresh your browser
Open Settings
Go to Accessibility → Visual effects
Turn off Animation effects
Refresh your browser
You can also emulate reduced motion in Chrome DevTools:
Open DevTools (F12)
Open Command Palette (Cmd/Ctrl + Shift + P)
Type “Rendering”
Select Show Rendering
Find Emulate CSS media feature prefers-reduced-motion
Select prefers-reduced-motion: reduce
Implementation Details
Icon-Level Reduced Motion
Here’s how different icons handle reduced motion:
Heart Icon
Bell Icon
Loader Icon
Arrow Icon
const prefersReducedMotion = useReducedMotion ();
return (
< motion.svg
// When reduced motion is enabled:
// - initial: false (no initial animation)
// - animate: false (no animate state)
// - whileHover: undefined (no hover animation)
// - whileTap: undefined (no tap animation)
initial = { prefersReducedMotion ? false : "rest" }
animate = { prefersReducedMotion ? false : "rest" }
whileHover = { prefersReducedMotion ? undefined : "hover" }
whileTap = { prefersReducedMotion ? undefined : "tap" }
variants = { beatVariants }
>
{ /* Icon content */ }
</ motion.svg >
);
const prefersReducedMotion = useReducedMotion ();
return (
< motion.svg
variants = { bellVariants }
initial = { prefersReducedMotion ? false : "rest" }
whileHover = { prefersReducedMotion ? undefined : "hover" }
whileTap = { prefersReducedMotion ? undefined : "tap" }
style = { { originX: "50%" , originY: "0%" } }
>
{ /* Icon content */ }
</ motion.svg >
);
const prefersReducedMotion = useReducedMotion ();
return (
< motion.svg
// Loader uses empty object {} instead of specific variants
animate = { prefersReducedMotion ? {} : { rotate: 360 } }
transition = {
prefersReducedMotion
? {}
: {
repeat: Infinity ,
duration: 1 ,
ease: "linear" ,
}
}
>
{ /* Icon content */ }
</ motion.svg >
);
const prefersReducedMotion = useReducedMotion ();
return (
< motion.svg
initial = "rest"
whileHover = "hover"
>
{ /* Variants applied to inner group instead */ }
< motion.g variants = { prefersReducedMotion ? {} : arrowVariants } >
< path d = "m18 12-6-6-6 6" />
< path d = "M12 18V6" />
</ motion.g >
</ motion.svg >
);
Custom Icon with Reduced Motion
When creating custom icons, always include reduced motion support:
import { motion , useReducedMotion , type Variants } from "framer-motion" ;
import { animationConfig } from "@/lib/animation-config" ;
const customVariants : Variants = {
rest: { scale: 1 , rotate: 0 },
hover: {
scale: animationConfig . scales . grow ,
rotate: animationConfig . rotations . small ,
transition: animationConfig . transitions . spring ,
},
};
export function CustomIcon ({ size = 24 , strokeWidth = 2 , ... props }) {
const prefersReducedMotion = useReducedMotion ();
return (
< motion.svg
width = { size }
height = { size }
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = { strokeWidth }
strokeLinecap = "round"
strokeLinejoin = "round"
variants = { customVariants }
initial = { prefersReducedMotion ? false : "rest" }
whileHover = { prefersReducedMotion ? undefined : "hover" }
className = "select-none"
{ ... props }
>
{ /* Your icon paths */ }
</ motion.svg >
);
}
ARIA Attributes
Decorative Icons
If an icon is purely decorative (next to text), use aria-hidden:
import { IconHeart } from "@/components/icon-heart" ;
export default function DecorativeExample () {
return (
< button >
< IconHeart aria-hidden = "true" />
< span > Like </ span >
</ button >
);
}
Meaningful Icons
If an icon conveys meaning without accompanying text, use role and aria-label:
import { IconHeart } from "@/components/icon-heart" ;
export default function MeaningfulExample () {
return (
< button >
< IconHeart
role = "img"
aria-label = "Like this post"
/>
</ button >
);
}
Interactive Icons
For clickable icons, ensure proper button semantics:
import { IconHeart } from "@/components/icon-heart" ;
import { useState } from "react" ;
export default function InteractiveExample () {
const [ liked , setLiked ] = useState ( false );
return (
< button
onClick = { () => setLiked ( ! liked ) }
aria-pressed = { liked }
aria-label = { liked ? "Unlike" : "Like" }
>
< IconHeart
aria-hidden = "true"
className = { liked ? "text-red-500" : "text-gray-400" }
/>
</ button >
);
}
Focus States
All Anicon icons include focus management styles:
className = "outline-none focus:outline-none focus:ring-0 select-none"
For keyboard navigation, wrap icons in focusable elements:
import { IconBell } from "@/components/icon-bell" ;
export default function FocusExample () {
return (
< button
className = "focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-lg p-2"
>
< IconBell aria-label = "Notifications" />
</ button >
);
}
Color Contrast
Ensure sufficient color contrast for icons:
WCAG Guidelines : Icons must meet WCAG 2.1 Level AA contrast ratio of at least 3:1 against their background.
import { IconHeart } from "@/components/icon-heart" ;
export default function ContrastExample () {
return (
< div >
{ /* Good contrast */ }
< div className = "bg-white" >
< IconHeart className = "text-gray-900" /> { /* High contrast */ }
</ div >
{ /* Good contrast */ }
< div className = "bg-gray-900" >
< IconHeart className = "text-white" /> { /* High contrast */ }
</ div >
{ /* Poor contrast - avoid */ }
< div className = "bg-gray-100" >
< IconHeart className = "text-gray-200" /> { /* Low contrast ❌ */ }
</ div >
</ div >
);
}
Screen Reader Support
Provide accessible labels for icon-only buttons:
import { IconHeart , IconBell , IconStar } from "@/components/icons" ;
export default function IconButtons () {
return (
< nav aria-label = "Actions" >
< button aria-label = "Like this post" >
< IconHeart aria-hidden = "true" />
</ button >
< button aria-label = "View notifications" >
< IconBell aria-hidden = "true" />
</ button >
< button aria-label = "Add to favorites" >
< IconStar aria-hidden = "true" />
</ button >
</ nav >
);
}
Status Icons
For status indicators, provide both visual and text alternatives:
import { IconCheck , IconX } from "@/components/icons" ;
export default function StatusIcons () {
return (
< div >
< div className = "flex items-center gap-2" >
< IconCheck className = "text-green-500" aria-hidden = "true" />
< span > Task completed </ span >
</ div >
< div className = "flex items-center gap-2" >
< IconX className = "text-red-500" aria-hidden = "true" />
< span > Task failed </ span >
</ div >
</ div >
);
}
Hidden Status Text
For visual-only status indicators, use visually hidden text:
import { IconCheck } from "@/components/icon-check" ;
export default function HiddenStatusText () {
return (
< div className = "relative" >
< IconCheck className = "text-green-500" aria-hidden = "true" />
< span className = "sr-only" > Success </ span >
</ div >
);
}
/* Add this utility class */
.sr-only {
position : absolute ;
width : 1 px ;
height : 1 px ;
padding : 0 ;
margin : -1 px ;
overflow : hidden ;
clip : rect ( 0 , 0 , 0 , 0 );
white-space : nowrap ;
border-width : 0 ;
}
Keyboard Navigation
Ensure icons in interactive elements are keyboard accessible:
import { IconHeart } from "@/components/icon-heart" ;
import { useState } from "react" ;
export default function KeyboardExample () {
const [ liked , setLiked ] = useState ( false );
return (
< button
onClick = { () => setLiked ( ! liked ) }
onKeyDown = { ( e ) => {
if ( e . key === "Enter" || e . key === " " ) {
e . preventDefault ();
setLiked ( ! liked );
}
} }
aria-pressed = { liked }
aria-label = { liked ? "Unlike" : "Like" }
className = "focus:outline-none focus:ring-2 focus:ring-blue-500 rounded p-2"
>
< IconHeart
aria-hidden = "true"
className = { liked ? "text-red-500" : "text-gray-400" }
/>
</ button >
);
}
Loading States
For loading indicators, provide appropriate ARIA attributes:
import { IconLoader } from "@/components/icon-loader" ;
export default function LoadingExample () {
return (
< div
role = "status"
aria-live = "polite"
aria-label = "Loading"
>
< IconLoader aria-hidden = "true" />
< span className = "sr-only" > Loading content... </ span >
</ div >
);
}
Best Practices Checklist
Always use useReducedMotion() - Respect user motion preferences
Add aria-hidden="true" - For decorative icons next to text
Add aria-label - For meaningful icons without accompanying text
Ensure color contrast - Meet WCAG 2.1 Level AA (3:1 minimum)
Provide text alternatives - Screen readers need text descriptions
Test keyboard navigation - All interactive icons must be keyboard accessible
Use semantic HTML - Wrap icons in proper <button> elements
Test with screen readers - Verify with VoiceOver (Mac) or NVDA (Windows)
Testing Accessibility
Automated Testing
Use tools like axe DevTools or Lighthouse:
# Install axe-core for React
npm install --save-dev @axe-core/react
import React from "react" ;
if ( process . env . NODE_ENV !== "production" ) {
import ( "@axe-core/react" ). then (( axe ) => {
axe . default ( React , ReactDOM , 1000 );
});
}
Manual Testing
Keyboard
Screen Reader
Reduced Motion
Navigate using Tab key
Activate with Enter or Space
Ensure focus is visible
Test all interactive icons
macOS (VoiceOver):
Press Cmd + F5 to enable
Navigate with VO + Arrow keys
Verify labels are announced
Windows (NVDA):
Install NVDA (free)
Press Ctrl + Alt + N to start
Navigate with arrow keys
Verify labels are announced
Enable reduced motion in system settings
Refresh browser
Verify animations are disabled
Ensure icons remain functional
Resources
WCAG Guidelines Web Content Accessibility Guidelines
MDN Accessibility Mozilla Developer Network accessibility resources
Framer Motion A11y Framer Motion accessibility documentation
WebAIM Web accessibility resources and tools
Next Steps
Basic Usage Learn the basics of using Anicon
Customization Customize icon appearance