# Correct Transform Origin

**MUST** · **ID:** `animations-correct-transform-origin` · **Category:** animations
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-correct-transform-origin

> MUST: Correct `transform-origin` (motion starts where it "physically" should)

Anchor motion to where it physically starts

When elements scale or rotate, the transform-origin should match where the motion naturally originates. For example, a dropdown opening from a button should scale from the button's position, not from the center of the dropdown.

## Bad — do not do this

`animations-correct-transform-origin-bad`

```tsx
import { useState } from 'react';

export function CorrectTransformOriginBad() {
  const [open, setOpen] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setOpen((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        {open ? 'Close menu' : 'Open menu'}
      </button>
      <div className="relative h-44">
        {open && (
          <div
            className="absolute left-0 top-0 w-44 rounded-lg bg-card border border-border shadow-lg p-2 space-y-1"
            style={{ transformOrigin: 'center', animation: 'originGrow 500ms cubic-bezier(0.16, 1, 0.3, 1)' }}
          >
            {['Edit', 'Duplicate', 'Delete'].map((x) => (
              <div key={x} className="px-3 py-2 text-sm rounded hover:bg-muted">
                {x}
              </div>
            ))}
          </div>
        )}
      </div>
      <style>{`
        @keyframes originGrow {
          from { opacity: 0; transform: scale(0.4); }
          to { opacity: 1; transform: scale(1); }
        }
      `}</style>
      <p className="text-xs text-destructive">
        <code>transform-origin: center</code> — the menu balloons out of its own middle, floating free of the button
      </p>
    </div>
  );
}
```

## Good — do this

`animations-correct-transform-origin-good`

```tsx
import { useState } from 'react';

export function CorrectTransformOriginGood() {
  const [open, setOpen] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setOpen((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        {open ? 'Close menu' : 'Open menu'}
      </button>
      <div className="relative h-44">
        {open && (
          <div
            className="absolute left-0 top-0 w-44 rounded-lg bg-card border border-border shadow-lg p-2 space-y-1"
            style={{ transformOrigin: 'top left', animation: 'originGrow 500ms cubic-bezier(0.16, 1, 0.3, 1)' }}
          >
            <span className="absolute -left-1 -top-1 size-2 rounded-full bg-primary" aria-hidden="true" />
            {['Edit', 'Duplicate', 'Delete'].map((x) => (
              <div key={x} className="px-3 py-2 text-sm rounded hover:bg-muted">
                {x}
              </div>
            ))}
          </div>
        )}
      </div>
      <style>{`
        @keyframes originGrow {
          from { opacity: 0; transform: scale(0.4); }
          to { opacity: 1; transform: scale(1); }
        }
      `}</style>
      <p className="text-xs text-success">
        <code>transform-origin: top left</code> (the dot) — the menu grows out of the button it belongs to
      </p>
    </div>
  );
}
```

## References

- [transform-origin](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transform-origin)
