CSS Drawer Animation with Custom Easing: A Complete Implementation Guide

Use a CSS custom property like --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1) combined with GPU-accelerated transform transitions to create smooth, iOS-style drawer animations in web applications.

The emilkowalski/skills repository provides a battle-tested pattern for implementing drawer animations with custom easing curves. This living styleguide codifies UI motion language through shared animation tokens, standardized recipes, and documentation-driven code reviews. Here's how to implement the drawer animation pattern using the actual source files and conventions from the repository.

Why Custom Easing Matters for Drawer Animations

Native-feeling drawer animations rely on cubic-bezier curves that accelerate quickly then settle softly. The repository's --ease-drawer token replicates the iOS-style motion found in Apple's own interfaces, providing that characteristic "snappy-then-smooth" feel that users associate with quality interactions.

Without a centralized easing vocabulary, teams inevitably drift toward inconsistent timing—some drawers feel sluggish, others too abrupt. The emilkowalski/skills architecture solves this by defining curves once and consuming them everywhere.

The --ease-drawer Token Definition

The custom easing curve lives in skills/review-animations/STANDARDS.md at line 34:

:root {
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer curve (Ionic) */
}

This cubic-bezier(0.32, 0.72, 0, 1) curve produces:

  • Fast initial acceleration (0.32, 0.72) — the drawer snaps into motion immediately
  • Long deceleration tail (0, 1) — the drawer settles gently into its final position

The repository centralizes this definition so that any component referencing --ease-drawer automatically receives design-system-approved motion.

The Drawer Recipe from RECIPES.md

The ready-to-use implementation appears in skills/animate/RECIPES.md (lines 110-120):

.drawer {
  /* slide in/out using transform */
  transition: transform 500ms var(--ease-drawer);
}

/* Closed state – move the drawer completely out of view */
.drawer[data-closed] {
  transform: translateY(100%); /* 100% = height of the drawer itself */
}

Three architectural decisions make this recipe performant and maintainable:

Decision Rationale
transform property GPU-accelerated, bypasses main-thread layout and paint
translateY(100%) Percentage is relative to the element's own height, making the recipe height-agnostic
500ms duration Falls within the "occasional" animation range (200-500ms) defined in the standards table

The skills/emil-design-eng/SKILL.md file provides additional context for why percentage-based translate beats fixed pixel values—your drawer works whether it's 200px or 600px tall without code changes.

Complete Working Example

Here's a full implementation combining the token, recipe, and interactive state management:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS Drawer Animation with Custom Easing</title>
  <style>
    :root {
      /* From skills/review-animations/STANDARDS.md */
      --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
    }

    body {
      margin: 0;
      font-family: system-ui, sans-serif;
    }

    .drawer {
      position: fixed;
      inset: 0 auto 0 0;          /* left-hand drawer */
      width: 300px;
      height: 100%;
      background: #fff;
      box-shadow: 2px 0 8px rgba(0,0,0,.15);
      
      /* RECIPES.md pattern: closed by default */
      transform: translateY(100%);
      transition: transform 500ms var(--ease-drawer);
    }

    /* Open state – match the [data-closed="false"] convention */
    .drawer[data-closed="false"] {
      transform: translateY(0);
    }
  </style>
</head>
<body>
  <button id="toggle">Toggle Drawer</button>

  <aside class="drawer" data-closed="true" id="myDrawer">
    <h2>Menu</h2>
    <nav>
      <a href="#">Home</a>
      <a href="#">Settings</a>
      <a href="#">Profile</a>
    </nav>
  </aside>

  <script>
    const btn = document.getElementById('toggle');
    const drawer = document.getElementById('myDrawer');
    
    btn.addEventListener('click', () => {
      const isClosed = drawer.getAttribute('data-closed') === 'true';
      drawer.setAttribute('data-closed', String(!isClosed));
    });
  </script>
</body>
</html>

This follows the RECIPES.md convention of using data-closed attributes—boolean strings rather than presence/absence—making the state inspectable and explicit.

Token Reference in SKILL.md

For a complete list of available easing tokens, skills/animate/SKILL.md at line 109 documents the full vocabulary:

:root {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.87, 0, 0.13, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  /* ...additional tokens */
}

Each token carries semantic meaning: --ease-drawer is reserved specifically for sheet/drawer presentations, while --ease-out handles generic exit animations. This naming convention prevents misuse—don't reach for --ease-drawer on a tooltip fade.

Ensuring Compliance Through Review Workflows

The emilkowalski/skills repository enforces drawer animation standards through:

  1. STANDARDS.md — the master table defining allowed durations and required tokens
  2. AUDIT.md — checklist for verifying existing implementations
  3. FIND-ANIMATION-OPPORTUNITIES.md — guidance for identifying mismatched curves

During code review, search for --ease-drawer usage with a simple grep:

grep -r "\--ease-drawer" src/

Any drawer implementation missing this token flags a standards violation.

Adapting for Horizontal Drawers

The recipe uses translateY(100%) for bottom-sheet style drawers. For left/right drawers, swap to translateX:

.drawer-left {
  transform: translateX(-100%);
  transition: transform 500ms var(--ease-drawer);
}

.drawer-left[data-closed="false"] {
  transform: translateX(0);
}

The same easing curve applies regardless of direction—the curve reflects the interaction pattern (sheet presentation), not the axis of motion.

Summary

  • Define custom easing once as a CSS custom property in your design tokens — the emilkowalski/skills repository uses --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1) for iOS-style sheet animations
  • Reference the token in component recipes using GPU-accelerated transform transitions with percentage-based translations
  • Keep duration at 500ms for "occasional" interactions per the standards table in STANDARDS.md
  • Use data-closed attributes for explicit state management and easier debugging
  • Verify implementations through repository grep tools during code review to enforce consistency

Frequently Asked Questions

What makes the --ease-drawer cubic-bezier curve feel "iOS-like"?

The cubic-bezier(0.32, 0.72, 0, 1) curve has a steep initial slope (fast acceleration) and a very long tail (extended deceleration). This mimics how physical sheets behave: they start moving quickly when pulled, then seem to "settle" into place with a soft landing. The values derive from Ionic Framework's drawer implementation, which itself studied Apple's native motion curves.

Why use translateY(100%) instead of translateY(-300px)?

Percentage values scale with the element's own dimensions, so your animation works regardless of drawer height. As documented in skills/emil-design-eng/SKILL.md, this future-proofs your CSS against design changes—a 200px drawer and a 600px drawer both slide completely out of view with identical code. Fixed pixel values break if the height changes.

How do I audit existing drawer implementations for standards compliance?

Run grep -rn "\--ease-drawer" across your codebase to find usages. Any drawer component missing this token—or using hardcoded cubic-bezier values—violates the emilkowalski/skills standards. Also verify that animations use transform only (no top, left, height, or margin transitions) and fall within the 200-500ms duration window defined in STANDARDS.md.

Can I reuse --ease-drawer for other animation patterns?

Avoid it. The repository explicitly scopes --ease-drawer to sheet/drawer presentations. For tooltips or modals, use --ease-out or --ease-in-out as defined in skills/animate/SKILL.md. Mismatching curves to interaction types creates subtle "uncanny valley" effects where animations feel technically correct but emotionally wrong.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →