# Never Animate Layout Properties

**NEVER** · **ID:** `animations-ibelick-no-layout` · **Category:** animations
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-ibelick-no-layout

> NEVER: Animate layout properties (width, height, top, left, margin, padding). Use transform: scale() and translate() instead. Layout triggers are expensive.

Never animate width, height, top, left, or other layout-triggering properties

A layout property does not just move the element — it invalidates the geometry of everything the element participates in, so the browser reflows potentially hundreds of siblings on every frame, then repaints them, then composites. Use transform: scale() for size and translate() for position: they produce the same visual result with none of that work. Where the effect genuinely needs a layout-like change (a panel actually growing), measure once and fake it with a transform — FLIP — rather than animating the property; ibelick's fixing-motion-performance skill puts it as "measure once, then animate via transform or opacity".

## Bad — do not do this

`animations-ibelick-no-layout-bad`

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

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

  return (
    <div className="space-y-4">
      <button
        onClick={() => setOpen((v) => !v)}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-lg transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        {open ? 'Collapse' : 'Expand'}
      </button>
      <div
        className="overflow-hidden rounded-lg bg-muted"
        style={{ height: open ? 120 : 0, transition: 'height 300ms ease' }}
      >
        <div className="p-3">
          <p className="font-medium">Panel Content</p>
          <p className="text-sm text-muted-foreground mt-2">The browser recalculates layout on every frame.</p>
        </div>
      </div>
      <p className="text-xs text-destructive">
        Animating <code>height</code> triggers a layout pass every frame — janky on complex pages
      </p>
    </div>
  );
}
```

## Good — do this

`animations-ibelick-no-layout-good`

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

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

  return (
    <div className="space-y-4">
      <button
        onClick={() => setOpen((v) => !v)}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-lg transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        {open ? 'Collapse' : 'Expand'}
      </button>
      {/* Space is reserved so revealing the panel never animates layout. */}
      <div className="min-h-[120px]">
        <div
          className="rounded-lg bg-muted"
          style={{
            transform: open ? 'translateY(0)' : 'translateY(-8px)',
            opacity: open ? 1 : 0,
            transition: 'transform 300ms cubic-bezier(0.16, 1, 0.3, 1), opacity 250ms ease-out',
            pointerEvents: open ? 'auto' : 'none',
          }}
        >
          <div className="p-3">
            <p className="font-medium">Panel Content</p>
            <p className="text-sm text-muted-foreground mt-2">Only transform and opacity animate, off the main thread.</p>
          </div>
        </div>
      </div>
      <p className="text-xs text-success">
        Reveal with <code>transform</code> + <code>opacity</code> — no layout recalculation, no distorted text
      </p>
    </div>
  );
}
```

## References

- [ibelick — baseline-ui SKILL.md](https://github.com/ibelick/ui-skills/blob/main/skills/baseline-ui/SKILL.md)
- [Avoid Layout Thrashing](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing)
