defuss-shadcn / Guides / Animations / anim-down
Slide DownATM
The slide animation in depth: the element glides in from an edge (or out toward one) while fading - the imperative twin of the motion component's data-df-entrance="down".
On this page (4)
Slide Down is the directional slide animation: slideIn starts the element at the named edge and settles it into place, slideOut sends it back out toward that same edge - with direction: 'north' (the default) the element starts above and slides down, matching the motion component's down entrance. All four orientation variants (north, south, east, west) ship per variant, and everything is driven by the df$.anim.slideIn.*() / df$.anim.slideOut.*() channels - play, pause, resume, finish, reset, state.
§In / out, directions and lifecycle
slideIn starts at the named edge, slideOut exits toward it. Direction buttons pick the edge (N is the default - the slide-DOWN motion); Pause/Resume/Finish/Reset act on whichever channel played last. Duration and easing apply to the next play.
§Scroll-bound slide-in
opts.scroll ties the animation's progress to a scroll position instead of time - scroll the stage and the box slides down in proportion. The platform's ScrollTimeline drives it where available; a rAF-throttled listener scrubs currentTime elsewhere.
The same binding with { target: 'viewport' } (the default when target is omitted) ties progress to the page's own scroll position instead of a container - the parallax use for presentations.
§Config axes
direction-'north' | 'south' | 'east' | 'west'(default'north'): the edge the in-variant starts at and the out-variant exits toward.distance- the slide travel as any CSS length (default'24px'), e.g.'48px'for a longer drift.duration- total time in ms (default1500).easing- any CSS easing (defaultcubic-bezier(0.16, 1, 0.3, 1)).
§CSS view file
The shared motion stylesheet - the declarative data-df-entrance twin of this page's df$.anim channels: the --df-motion-… tunables, the fifteen df-enter-… keyframes and the reduced-motion flattening.
/* -- Motion component ------------------------------------------------- The shared entrance-animation vocabulary: CSS @keyframes are the single source of truth (geometry, easing, defaults, reduced-motion), driven either declaratively via [data-df-entrance] or imperatively through the JS controller `ddf$.entrance(el, effect, opts)` (src/shared/motion.ts - same functions under df$.shadcn.entrance). The JS never defines keyframes; it only triggers/retriggers, overrides timing vars and owns the finished/cancel lifecycle via the platform's CSSAnimation objects. Why animations (not transitions): they run deterministically when the attribute first applies - including on freshly inserted DOM - and they replay cleanly (cancel → play). Transitions need a rendered "from" state and never fire on initial application (the reason the first vocabulary demo showed a static grid). Why animation-composition: add - transform/filter keyframes COMPOSE with any pre-existing transform/filter on the element instead of replacing it, so entrances are safe on arbitrarily styled DOM (no transform inspection or reconstruction). The paired fade runs with `replace`. Limits: non-replaced inline boxes (display:inline spans) are not transformable per CSS - this is deliberate and NOT worked around (silent display changes would alter text wrapping). Wrap inline content or let the caller promote it. */@layer components { /* timing tunables - :root defaults so ANY ancestor (deck mount, container, inline style) can override by ordinary cascade; inline styles win last */ :root { --df-motion-duration: 1500ms; --df-motion-delay: 0ms; --df-motion-ease: cubic-bezier(0.16, 1, 0.3, 1); --df-motion-distance: 24px; --df-motion-scale-in: 0.9; --df-motion-scale-out: 1.1; --df-motion-blur: 12px; --df-motion-angle: 12deg; --df-motion-spin: -0.5turn; /* JS writes the element's CURRENT computed opacity here, so fading an opacity:.6 element ends at .6 - never brightens content it doesn't own */ --df-motion-base-opacity: 1; } /* one animation declaration, parameterized entirely by the vars above */ [data-df-entrance] { animation-duration: var(--df-motion-duration); animation-delay: var(--df-motion-delay); animation-timing-function: var(--df-motion-ease); animation-fill-mode: both; /* transform-channel entrances pair with the fade (composition: add keeps the element's own transform underneath); clip-path entrances need no fade - the clip alone reveals the content */ &[data-df-entrance="up"] { animation-name: df-enter-up, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="down"] { animation-name: df-enter-down, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="left"] { animation-name: df-enter-left, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="right"] { animation-name: df-enter-right, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="zoom"] { animation-name: df-enter-zoom, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="zoom-out"] { animation-name: df-enter-zoom-out, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="pop"] { animation-name: df-enter-pop, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="spin"] { animation-name: df-enter-spin, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="flip"] { animation-name: df-enter-flip, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="skew"] { animation-name: df-enter-skew, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="blur"] { animation-name: df-enter-blur, df-enter-fade; animation-composition: add, replace; } &[data-df-entrance="wipe"] { animation-name: df-enter-wipe; } &[data-df-entrance="wipe-up"] { animation-name: df-enter-wipe-up; } &[data-df-entrance="iris"] { animation-name: df-enter-iris; } &[data-df-entrance="fade"] { animation-name: df-enter-fade; } } /* stagger: children of a [data-df-stagger] container get graded delays (--df-motion-stagger-step per container) while keeping their own direction. ponytail: beyond the 8th child the delay caps at *7 - long lists should chunk into columns. An inline --df-motion-delay on a child still wins (inline beats any stylesheet rule). */ [data-df-stagger] { --df-motion-stagger-step: 120ms; & > :nth-child(1) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 0); } & > :nth-child(2) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 1); } & > :nth-child(3) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 2); } & > :nth-child(4) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 3); } & > :nth-child(5) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 4); } & > :nth-child(6) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 5); } & > :nth-child(7) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 6); } & > :nth-child(n + 8) { --df-motion-delay: calc(var(--df-motion-stagger-step) * 7); } } /* SVG draw-in - a distinct primitive, not a DOM entrance: pathLength="1" calibrates distance-along-path so ONE dash rule (1 → 0) fits every geometry regardless of actual path length. (The CSS `path-length` property stays experimental - the SVG attribute is the portable form.) */ [data-df-draw][pathLength="1"] { stroke-dasharray: 1; stroke-dashoffset: 1; animation: df-draw var(--df-motion-duration) var(--df-motion-ease) var(--df-motion-delay) both; } /* vocabulary - every direction is exactly one keyframes rule */ @keyframes df-enter-fade { from { opacity: 0; } to { opacity: var(--df-motion-base-opacity); } } /* "up" travels upward into its final position (from below) */ @keyframes df-enter-up { from { transform: translateY(var(--df-motion-distance)); } to { transform: translateY(0); } } @keyframes df-enter-down { from { transform: translateY(calc(-1 * var(--df-motion-distance))); } to { transform: translateY(0); } } @keyframes df-enter-left { from { transform: translateX(var(--df-motion-distance)); } to { transform: translateX(0); } } @keyframes df-enter-right { from { transform: translateX(calc(-1 * var(--df-motion-distance))); } to { transform: translateX(0); } } @keyframes df-enter-zoom { from { transform: scale(var(--df-motion-scale-in)); } to { transform: scale(1); } } @keyframes df-enter-zoom-out { from { transform: scale(var(--df-motion-scale-out)); } to { transform: scale(1); } } @keyframes df-enter-pop { 0% { transform: scale(0.65); } 65% { transform: scale(1.07); } 100% { transform: scale(1); } } @keyframes df-enter-spin { from { transform: rotate(var(--df-motion-spin)); } to { transform: rotate(0deg); } } @keyframes df-enter-flip { from { transform: perspective(900px) rotateY(-70deg); } to { transform: perspective(900px) rotateY(0deg); } } @keyframes df-enter-skew { from { transform: skewX(var(--df-motion-angle)); } to { transform: skewX(0deg); } } @keyframes df-enter-blur { from { filter: blur(var(--df-motion-blur)); } to { filter: blur(0); } } @keyframes df-enter-wipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0); } } @keyframes df-enter-wipe-up { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0); } } @keyframes df-enter-iris { from { clip-path: circle(0% at 50% 50%); } to { clip-path: circle(150% at 50% 50%); } } @keyframes df-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } } /* accessibility: near-instant instead of none - the JS finished/cancel contract behaves identically (a finished 1ms animation still resolves), while nothing visibly moves. REQUIRED for every animation. */ @media (prefers-reduced-motion: reduce) { [data-df-entrance], [data-df-draw] { --df-motion-duration: 1ms !important; --df-motion-delay: 0ms !important; } }}Comments, ideas or improvements? Edit this page's source on GitHub