Headless UI를 사용한 전환 효과
Transition 컴포넌트를 사용하여 Tailwind의 전환 유틸리티로 대화 상자, 드롭다운, 서랍의 진입 및 이탈 상태를 애니메이션으로 처리합니다.
Headless UI를 사용한 전환 효과은(는) CoddyKit의 무료 Tailwind CSS Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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.
AI 튜터와 함께 HTML을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 30
- 레슨
- 120
자주 묻는 질문
“Headless UI를 사용한 전환 효과” 강의는 무료인가요?
네 — “Headless UI를 사용한 전환 효과” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Tailwind CSS Academy 강의 전체를 잠금 해제할 수 있습니다. Tailwind CSS Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“Headless UI를 사용한 전환 효과”에서 뭘 배우나요?
Transition 컴포넌트를 사용하여 Tailwind의 전환 유틸리티로 대화 상자, 드롭다운, 서랍의 진입 및 이탈 상태를 애니메이션으로 처리합니다. 브라우저에서 직접 실행하는 실습 코드로 Tailwind CSS Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Tailwind CSS Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Tailwind CSS Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“Headless UI를 사용한 전환 효과” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Tailwind CSS Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Tailwind CSS Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Headless UI 입문
- 헤드리스 메뉴와 드롭다운 스타일 지정
- 접근성 높은 대화 상자 만들기
- Headless UI를 사용한 전환 효과