0Pricing
Tailwind CSS Academy · レッスン

Headless UI によるトランジション

Transition コンポーネントと Tailwind の transition ユーティリティを使い、ダイアログ、ドロップダウン、ドロワーの表示・非表示状態をアニメーションさせます。

「Headless UI によるトランジション」はCoddyKit上の無料Tailwind CSS Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはTailwind CSS Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Tailwind CSS Academyコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

Why Animate With Headless UI's Transition?

Tailwind's transition-* utilities handle smooth state changes on persistent elements, but they cannot animate elements that mount and unmount from the DOM. When a dialog or dropdown appears, it goes from non-existent to visible in one frame — there is no transition. Headless UI's Transition component solves this by keeping the element mounted briefly during the exit animation, allowing Tailwind transition utilities to complete before the element is removed.

The Transition Component API

The Transition component from Headless UI accepts a show boolean and a set of class props: enter, enterFrom, enterTo for the entering animation, and leave, leaveFrom, leaveTo for the exit. Classes in enter and leave are active during the entire transition. Classes in enterFrom/leaveFrom define the start state, and enterTo/leaveTo define the end state.

import { Transition } from '@headlessui/react';

<Transition
  show={isVisible}
  enter='transition-opacity duration-200 ease-out'
  enterFrom='opacity-0'
  enterTo='opacity-100'
  leave='transition-opacity duration-150 ease-in'
  leaveFrom='opacity-100'
  leaveTo='opacity-0'
>
  <div className='bg-white rounded-xl p-6 shadow-lg'>
    Content that fades in and out
  </div>
</Transition>

Animating a Dialog Backdrop and Panel

A polished dialog entrance uses two separate animations: the backdrop fades in while the panel scales and fades in from a slightly smaller size. Wrap each element in its own Transition.Child and set different durations so the backdrop appears slightly before the panel. Use Transition as the outer wrapper with the show prop, and Transition.Child for coordinated child animations.

import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';

function AnimatedDialog({ open, onClose, children }) {
  return (
    <Transition show={open} as={Fragment}>
      <Dialog onClose={onClose} className='relative z-50'>

        {/* Backdrop fade */}
        <Transition.Child
          as={Fragment}
          enter='ease-out duration-300'
          enterFrom='opacity-0'
          enterTo='opacity-100'
          leave='ease-in duration-200'
          leaveFrom='opacity-100'
          leaveTo='opacity-0'
        >
          <div className='fixed inset-0 bg-black/50' aria-hidden='true' />
        </Transition.Child>

        {/* Panel scale-in */}
        <div className='fixed inset-0 flex items-center justify-center p-4'>
          <Transition.Child
            as={Fragment}
            enter='ease-out duration-300'
            enterFrom='opacity-0 scale-95'
            enterTo='opacity-100 scale-100'
            leave='ease-in duration-200'
            leaveFrom='opacity-100 scale-100'
            leaveTo='opacity-0 scale-95'
          >
            <Dialog.Panel className='bg-white rounded-2xl shadow-2xl max-w-md w-full p-6'>
              {children}
            </Dialog.Panel>
          </Transition.Child>
        </div>

      </Dialog>
    </Transition>
  );
}

Animating Dropdowns and Popovers

Dropdown menus benefit from a subtle slide-and-fade entrance. Wrap the Menu.Items in a Transition component that slides down from the trigger and fades in. The exit animation should be faster than the entrance — a 100-150ms leave feels crisp and responsive. Use transform origin-top so the scale animation originates from the top where the button is.

import { Menu, Transition } from '@headlessui/react';
import { Fragment } from 'react';

<Menu as='div' className='relative'>
  <Menu.Button>Options</Menu.Button>

  <Transition
    as={Fragment}
    enter='transition ease-out duration-150'
    enterFrom='transform opacity-0 scale-95 -translate-y-2'
    enterTo='transform opacity-100 scale-100 translate-y-0'
    leave='transition ease-in duration-100'
    leaveFrom='transform opacity-100 scale-100 translate-y-0'
    leaveTo='transform opacity-0 scale-95 -translate-y-2'
  >
    <Menu.Items className='absolute right-0 mt-1 w-48 rounded-xl bg-white shadow-lg ring-1 ring-black/5 p-1 focus:outline-none'>
      {/* items */}
    </Menu.Items>
  </Transition>
</Menu>

Slide-In Sidebar Drawer

A mobile sidebar drawer slides in from the left or right edge. Use a Transition with translate-x utilities for the slide direction. Start off-screen (-translate-x-full for left, translate-x-full for right) and translate to translate-x-0 to bring it into view. Add a backdrop transition simultaneously using Transition.Child.

function MobileSidebar({ open, onClose }) {
  return (
    <Transition show={open} as={Fragment}>
      <Dialog onClose={onClose}>
        {/* Backdrop */}
        <Transition.Child
          as={Fragment}
          enter='ease-out duration-300'
          enterFrom='opacity-0'
          enterTo='opacity-100'
          leave='ease-in duration-200'
          leaveFrom='opacity-100'
          leaveTo='opacity-0'
        >
          <div className='fixed inset-0 bg-black/40 z-40' aria-hidden='true' />
        </Transition.Child>

        {/* Sidebar panel */}
        <Transition.Child
          as={Fragment}
          enter='transition ease-out duration-300'
          enterFrom='-translate-x-full'
          enterTo='translate-x-0'
          leave='transition ease-in duration-200'
          leaveFrom='translate-x-0'
          leaveTo='-translate-x-full'
        >
          <Dialog.Panel className='fixed left-0 top-0 h-full w-72 bg-white shadow-xl z-50 overflow-y-auto'>
            {/* sidebar content */}
          </Dialog.Panel>
        </Transition.Child>
      </Dialog>
    </Transition>
  );
}

Notification Toast Animation

Notification toasts typically slide in from the bottom or top corner and fade out after a timeout. Use a Transition with translate-y for the slide direction. Combine show with a boolean that automatically toggles to false after a timeout using setTimeout in a useEffect. The leave animation completes before the component unmounts.

function Toast({ message, show, onHide }) {
  return (
    <Transition
      show={show}
      as={Fragment}
      enter='transition ease-out duration-300'
      enterFrom='translate-y-4 opacity-0'
      enterTo='translate-y-0 opacity-100'
      leave='transition ease-in duration-200'
      leaveFrom='translate-y-0 opacity-100'
      leaveTo='translate-y-4 opacity-0'
    >
      <div className='
        fixed bottom-4 right-4 z-50
        bg-gray-900 text-white
        px-4 py-3 rounded-xl shadow-lg
        flex items-center gap-3
        max-w-sm
      '>
        <CheckCircleIcon className='h-5 w-5 text-green-400 flex-shrink-0' />
        <span className='text-sm'>{message}</span>
        <button onClick={onHide} className='ml-auto text-gray-400 hover:text-white'>
          <XMarkIcon className='h-4 w-4' />
        </button>
      </div>
    </Transition>
  );
}

Coordinating Multiple Transitions

The Transition.Child component coordinates timing with a parent Transition. Children observe the parent's show state and can add their own timing offsets. This enables staggered animations where different parts of a UI appear in sequence. Headless UI does not natively support arbitrary stagger delays, but you can simulate it with incrementally longer enter durations or CSS animation delays.

// Staggered items using animation-delay workaround
<Transition show={open}>
  <div className='bg-white rounded-xl p-6 shadow-lg'>
    {items.map((item, i) => (
      <Transition.Child
        key={item.id}
        as={Fragment}
        enter='transition ease-out duration-200'
        enterFrom='opacity-0 translate-y-2'
        enterTo='opacity-100 translate-y-0'
        style={{ transitionDelay: i * 50 + 'ms' }}
        leave='transition ease-in duration-150'
        leaveFrom='opacity-100'
        leaveTo='opacity-0'
      >
        <div className='py-2 border-b border-gray-100'>{item.label}</div>
      </Transition.Child>
    ))}
  </div>
</Transition>

Transition With appear Prop

By default, the Transition component does not animate on first render if show is already true when it mounts. The appear prop overrides this, causing the enter transition to play even on initial mount. This is useful for page-load animations or when a component is conditionally rendered with show immediately set to true.

// Without 'appear': no animation on initial mount when show=true
<Transition show={true}>
  <div>This appears without animation on first load</div>
</Transition>

// With 'appear': animates in even on first mount
<Transition
  show={true}
  appear  // enables enter transition on initial mount
  enter='transition-all ease-out duration-500'
  enterFrom='opacity-0 scale-95'
  enterTo='opacity-100 scale-100'
>
  <div className='bg-white rounded-xl p-6 shadow'>
    This animates in on first load
  </div>
</Transition>

Handling afterLeave Callbacks

The afterLeave prop fires a callback after the leave transition completes. This is useful for cleanup actions that should happen only after the element has fully disappeared — like resetting form state, clearing error messages, or unmounting expensive child components. Running cleanup during the transition (when the element is still visible) would cause a jarring visual change.

function SearchDialog({ open, onClose }) {
  const [query, setQuery] = useState('');

  return (
    <Transition
      show={open}
      afterLeave={() => {
        // Reset query AFTER dialog has fully faded out
        // Prevents seeing the input clear while dialog is still visible
        setQuery('');
      }}
      enter='ease-out duration-300'
      enterFrom='opacity-0 scale-95'
      enterTo='opacity-100 scale-100'
      leave='ease-in duration-200'
      leaveFrom='opacity-100 scale-100'
      leaveTo='opacity-0 scale-95'
    >
      <Dialog open={open} onClose={onClose}>
        <Dialog.Panel className='bg-white rounded-2xl p-6 shadow-xl'>
          <input
            value={query}
            onChange={e => setQuery(e.target.value)}
            placeholder='Search...'
            className='w-full border rounded-lg px-3 py-2'
          />
        </Dialog.Panel>
      </Dialog>
    </Transition>
  );
}

Best Practices for UI Transitions

Keep animations short and purposeful. Enter transitions should be 150-300ms — long enough to feel smooth but short enough not to make users wait. Leave transitions should be 10-30% shorter than enters — exits should feel crisp. Use ease-out for enters (fast start, slow finish feels natural) and ease-in for exits (slow start, fast finish feels intentional). Never animate for the sake of animation — every motion should communicate state change.

/* Recommended timing guidelines */

/* Tooltip / small dropdown */
enter: 'transition ease-out duration-100'
leave: 'transition ease-in duration-75'

/* Modal dialog */
enter: 'ease-out duration-300'
leave: 'ease-in duration-200'

/* Sidebar drawer */
enter: 'transition ease-out duration-300'
leave: 'transition ease-in duration-250'

/* Page transitions */
enter: 'transition ease-out duration-500'
leave: 'transition ease-in duration-300'

/* Avoid: too slow (>500ms) feels laggy */
/* Avoid: ease-in for enters (starts slow, looks stuck) */

Respecting Reduced Motion Preferences

Some users experience motion sickness or seizures from on-screen animations. The prefers-reduced-motion media query allows the operating system to signal this preference to the browser. Always respect it in your Transition animations. Use Tailwind's motion-reduce: variant (or a custom variant) to strip transitions for users who need reduced motion, keeping the UI functional but static.

/* globals.css — respect reduced motion globally */
@media (prefers-reduced-motion: reduce) {
  .transition,
  .transition-all,
  .transition-colors,
  .transition-transform {
    transition-duration: 0.01ms !important;
  }
}

<!-- In Tailwind with motion-reduce: variant -->
<div
  class='
    transition-all ease-out duration-300
    motion-reduce:transition-none
    opacity-0 translate-y-4
    open:opacity-100 open:translate-y-0
  '
>
  Animates for standard users, instant for reduced-motion users
</div>

Quick Check

Test your understanding of Tailwind CSS Mastery concepts from this lesson.

Lesson Recap

In this lesson you learned: Transition wraps mounted elements to enable enter/leave animations, Transition.Child coordinates timing for multiple elements in a parent Transition, and afterLeave runs cleanup callbacks only after the exit animation completes. Next up we dive into writing custom Tailwind plugins to extend the utility system.

よくある質問

「Headless UI によるトランジション」レッスンは無料ですか?

はい。「Headless UI によるトランジション」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Tailwind CSS Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Tailwind CSS Academyコースには全4レッスンが含まれています。

「Headless UI によるトランジション」で何を学びますか?

Transition コンポーネントと Tailwind の transition ユーティリティを使い、ダイアログ、ドロップダウン、ドロワーの表示・非表示状態をアニメーションさせます。 ブラウザで直接実行するハンズオンコードでTailwind CSS Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Tailwind CSS Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのTailwind CSS Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「Headless UI によるトランジション」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このTailwind CSS Academyレッスンでコードを書いて実行できますか?

はい。すべてのTailwind CSS Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Headless UI 入門
  2. Headless Menu とドロップダウンのスタイリング
  3. アクセシブルなダイアログの構築
  4. Headless UI によるトランジション
← Tailwind CSS Academyに戻る