defuss-shadcn / Guides / animations
Animations & Transitions
All enter and exit animations are CSS-only. No JavaScript class toggling, no animation libraries - just @starting-style, allow-discrete transitions, and native CSS features.
On this page (8)
§The challenge
Animating elements that go from display: none to visible (popovers, dialogs, accordions) has historically been impossible in CSS. The element either exists or doesn't - there was no in-between state to transition through. Libraries solved this with JS class toggling and requestAnimationFrame hacks.
Modern CSS solves this with two new features: @starting-style (defines the initial state before an element appears) and allow-discrete (lets display participate in transitions).
§@starting-style
The @starting-style at-rule defines the values a property should start from when the element first appears (enters the top layer, gains display, gets inserted into the DOM). The browser applies these values first, then transitions to the element's actual computed styles.
/* 1. The element's normal (visible) state */.popover:popover-open { opacity: 1; translate: 0 0;}/* 2. The starting state - before the transition begins */@starting-style { .popover:popover-open { opacity: 0; translate: 0 -4px; }}/* Flow: @starting-style values → transition → :popover-open values Result: a smooth fade-in + slide-up animation */§allow-discrete transitions
The display property was historically not animatable - it snaps between none and block instantly. The allow-discrete keyword in the transition shorthand tells the browser to keep the element visible during the exit transition before hiding it.
.popover { /* Animate opacity, translate, AND display */ transition: opacity 150ms ease, translate 150ms ease, display 150ms allow-discrete; /* Exit state: invisible */ opacity: 0; translate: 0 -4px;}/* Without allow-discrete: element disappears instantly on close With allow-discrete: element fades out, THEN display changes to none */§Popover & dialog animation pattern
Most overlay components (popover, dropdown, tooltip, dialog, sheet, toast) follow the same three-part animation recipe. Here's the complete pattern used by the Popover component:
@layer components { .popover { /* 1. Base state (also the exit target) */ opacity: 0; translate: 0 -4px; transition: opacity 150ms ease, translate 150ms ease, display 150ms allow-discrete; /* 2. Open state - what it transitions TO */ &:popover-open { opacity: 1; translate: 0 0; } } /* 3. Starting style - what it transitions FROM */ @starting-style { .popover:popover-open { opacity: 0; translate: 0 -4px; } }}@starting-style values → transition → :popover-open values. The popover fades in and slides up.Exit:popover-open values → transition → base state values. The allow-discrete on display keeps the element visible during the fade-out.§Dialog & sheet animations
Dialogs and sheets use the same pattern but with dialog[open] instead of :popover-open, and they also animate the ::backdrop pseudo-element.
dialog.dialog { opacity: 0; translate: 0 -0.5rem; scale: 0.98; transition: opacity 200ms ease, translate 200ms ease, scale 200ms ease, display 200ms allow-discrete; &[open] { opacity: 1; translate: 0 0; scale: 1; } /* Backdrop fades in/out too */ &::backdrop { background: oklch(0 0 0 / 0); backdrop-filter: blur(0px); transition: all 200ms ease, display 200ms allow-discrete; } &[open]::backdrop { background: oklch(0 0 0 / 0.45); backdrop-filter: blur(3px); }}@starting-style { dialog.dialog[open] { opacity: 0; translate: 0 -0.5rem; scale: 0.98; } dialog.dialog[open]::backdrop { background: oklch(0 0 0 / 0); backdrop-filter: blur(0px); }}Sheets use translate instead of scale - a sheet with data-side="right" slides in from translateX(100%).
§Accordion animations
Accordions animate their height using the ::details-content pseudo-element and the block-size property. This is one of the most-requested CSS features - animating height: 0 to height: auto - now possible natively.
.accordion-item { /* Target the collapsible content area */ &::details-content { block-size: 0; overflow-y: clip; transition: block-size 200ms ease, content-visibility 200ms allow-discrete; } /* When open, expand to natural height */ &[open]::details-content { block-size: auto; }}/* Start from 0 when opening */@starting-style { .accordion-item[open]::details-content { block-size: 0; }}0 to that value. No JavaScript height measurement needed.content-visibilityThe allow-discrete on content-visibility prevents the content from disappearing before the close animation finishes.§Programmatic exit animations
Most components handle exit purely through CSS transitions. For cases where you need to run logic after the exit animation completes (like removing a toast element from the DOM), the Web Animations API provides a promise-based approach.
// Animate out (Web Animations API), then remove through df$el.animate( [ { opacity: 1, translate: '0 0' }, { opacity: 0, translate: '0 0.5rem' }, ], { duration: 200, easing: 'ease', fill: 'forwards' },).finished.then(() => { el.hidePopover(); df$(el).remove();});Used by: Toast (dismiss animation before removing the element from the DOM)
§Animation summary
Which components use animations and what type of animation they use.
| Component | Animation | Technique |
|---|---|---|
| Popover | Fade + slide up | @starting-style + :popover-open |
| Dropdown | Fade + slide up | @starting-style + :popover-open |
| Tooltip | Fade in | @starting-style + :popover-open |
| Dialog | Fade + slide + scale | @starting-style + dialog[open] + ::backdrop |
| Alert Dialog | Fade + slide + scale | @starting-style + dialog[open] + ::backdrop |
| Sheet | Slide from edge | @starting-style + dialog[open] + ::backdrop |
| Toast | Slide in, slide out | @starting-style + Web Animations API for exit |
| Accordion | Height expand/collapse | @starting-style + ::details-content + block-size |
| Collapsible | Height expand/collapse | @starting-style + ::details-content + block-size |
| Command | Fade + scale up | @starting-style + dialog[open] |
All of these techniques are pure CSS (except toast exit). No animation libraries, no requestAnimationFrame hacks, no JS class toggling. See the Native Web APIs page for the broader set of platform APIs used across the system.
Comments, ideas or improvements? Edit this page's source on GitHub