0Pricing
Tailwind CSS Academy · Lección

Transiciones con Headless UI

Utilice el componente Transition para animar los estados de entrada y salida de diálogos, desplegables y paneles laterales mediante las utilidades de transición de Tailwind.

Transiciones con Headless UI es una lección gratuita de Tailwind CSS Academy en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Tailwind CSS Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Tailwind CSS Academy incluye 4 lecciones en total.

Partes de esta lección aún no han sido traducidas y se muestran en inglés.

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.

Preguntas frecuentes

¿La lección «Transiciones con Headless UI» es gratis?

Sí — el texto completo de «Transiciones con Headless UI» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Tailwind CSS Academy, actualiza a CoddyKit PRO. El curso de Tailwind CSS Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Transiciones con Headless UI»?

Utilice el componente Transition para animar los estados de entrada y salida de diálogos, desplegables y paneles laterales mediante las utilidades de transición de Tailwind. Practicas Tailwind CSS Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar Tailwind CSS Academy?

No se requiere experiencia previa. Tailwind CSS Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Transiciones con Headless UI»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de Tailwind CSS Academy?

Sí. Cada lección de Tailwind CSS Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Introducción a Headless UI
  2. Estilizar menús y desplegables headless
  3. Crear diálogos accesibles
  4. Transiciones con Headless UI
← Volver a Tailwind CSS Academy