# Tailwind motion-safe/motion-reduce Variants

**MUST** · **ID:** `animations-tailwind-motion-variants` · **Category:** animations
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-tailwind-motion-variants

> MUST: Use Tailwind `motion-safe:` and `motion-reduce:` variants to conditionally apply animations. Default pattern: `motion-safe:animate-*` ensures animations only run when user allows motion.

Use Tailwind motion-safe: and motion-reduce: variants for accessible animations

These variants respect the user's prefers-reduced-motion setting. motion-safe: applies styles only when motion is allowed. motion-reduce: applies alternative styles when user prefers reduced motion.

## Bad — do not do this

`animations-tailwind-motion-variants-bad`

```tsx
import { useState } from 'react';
import { ReducedMotionSwitch } from '@/components/demo-kit/ReducedMotionSwitch';

export function TailwindMotionVariantsBad() {
  const [replay, setReplay] = useState(0);

  return (
    <div className="space-y-4">
      <ReducedMotionSwitch />
      <button
        onClick={() => setReplay((v) => v + 1)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Replay
      </button>
      <div key={replay} className="rounded-lg bg-muted p-3 text-sm" style={{ animation: 'tmSlide 400ms ease-out' }}>
        Always slides in (no variant)
      </div>
      <style>{`@keyframes tmSlide { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }`}</style>
      <p className="text-xs text-destructive">
        A raw animation with no <code>motion-reduce:</code> variant — it slides in even when the user asked for less motion
      </p>
    </div>
  );
}
```

## Good — do this

`animations-tailwind-motion-variants-good`

```tsx
import { useState } from 'react';
import { ReducedMotionSwitch } from '@/components/demo-kit/ReducedMotionSwitch';
import { useSimulatedReducedMotion } from '@/hooks/useSimulatedReducedMotion';

export function TailwindMotionVariantsGood() {
  const reduced = useSimulatedReducedMotion();
  const [replay, setReplay] = useState(0);

  return (
    <div className="space-y-4">
      <ReducedMotionSwitch />
      <button
        onClick={() => setReplay((v) => v + 1)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Replay
      </button>
      <div
        key={replay}
        className="rounded-lg bg-muted p-3 text-sm"
        style={{ animation: reduced ? 'tmFade 200ms ease-out' : 'tmSlide 400ms ease-out' }}
      >
        {reduced ? 'Fades in (motion-reduce)' : 'Slides in (motion-safe)'}
      </div>
      <style>{`
        @keyframes tmSlide { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
        @keyframes tmFade { from { opacity: 0; } to { opacity: 1; } }
      `}</style>
      <p className="text-xs text-success">
        <code>motion-safe:</code> slides, <code>motion-reduce:</code> just fades — toggle to see it swap (this simulates the OS setting)
      </p>
    </div>
  );
}
```

## References

- [Tailwind Animation](https://tailwindcss.com/docs/animation)
- [Motion-Safe Animations Guide](https://dev.to/hexshift/building-fluid-motion-safe-animations-in-tailwind-css-that-respect-user-preferences-3i6e)
