Transitions CORE+
Add smooth CSS animations to elements when they appear or disappear using the data-transition attribute.
How Transitions Work
When an element with data-transition changes visibility, the framework applies CSS classes in a specific sequence:
Entering (showing)
.{name}-enter- Initial state (e.g., opacity: 0).{name}-enter-active- Active state with transition (opacity: 1)- Classes removed after transition completes
Leaving (hiding)
.{name}-leave- Initial state (e.g., opacity: 1).{name}-leave-active- Active state with transition (opacity: 0)- Element hidden after transition completes
Basic Usage
Add data-transition to any element with data-show or data-render:
<div data-component="fade-demo">
<button class="btn btn-primary" data-action="toggle">Toggle Fade</button>
<div data-show="isVisible" data-transition="fade"
class="alert alert-success mt-3">
I fade in and out smoothly!
</div>
</div>
wildflower.component('fade-demo', {
state: { isVisible: true },
toggle() {
this.isVisible = !this.isVisible
}
})
CSS Requirements
For transitions to work, you need to define the CSS classes. Here's the fade transition CSS:
/* Define the transition CSS classes */
.fade-enter {
opacity: 0;
}
.fade-enter-active {
transition: opacity 0.3s ease;
opacity: 1;
}
.fade-leave {
opacity: 1;
}
.fade-leave-active {
transition: opacity 0.3s ease;
opacity: 0;
}
Built-in Transition Patterns
Here are common transition patterns you can use. Just define the CSS classes:
Fade
Simple opacity transition - great for modals and overlays.
.fade-enter { opacity: 0; }
.fade-enter-active { transition: opacity 0.3s ease; opacity: 1; }
.fade-leave { opacity: 1; }
.fade-leave-active { transition: opacity 0.3s ease; opacity: 0; }
Slide
Horizontal slide effect - great for sidebars and panels.
.slide-enter {
transform: translateX(-100%);
opacity: 0;
}
.slide-enter-active {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: translateX(0);
opacity: 1;
}
.slide-leave {
transform: translateX(0);
opacity: 1;
}
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: translateX(100%);
opacity: 0;
}
Scale
Zoom effect - great for popups and cards.
<div data-component="scale-demo">
<button class="btn btn-primary" data-action="toggle">Toggle Scale</button>
<div class="d-flex justify-content-center mt-3">
<div data-show="isVisible" data-transition="scale"
class="card" style="width: 200px;">
<div class="card-body text-center">
<h5 class="card-title">Popup Card</h5>
<p class="card-text">I scale up from nothing!</p>
</div>
</div>
</div>
</div>
wildflower.component('scale-demo', {
state: { isVisible: true },
toggle() {
this.isVisible = !this.isVisible
}
})
Scale transition CSS:
.scale-enter {
transform: scale(0);
opacity: 0;
}
.scale-enter-active {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: scale(1);
opacity: 1;
}
.scale-leave {
transform: scale(1);
opacity: 1;
}
.scale-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: scale(0);
opacity: 0;
}
Slide Down
Vertical expand/collapse - great for accordions and dropdowns.
<div data-component="accordion-demo">
<div class="accordion">
<div class="accordion-item">
<button class="btn btn-outline-primary w-100 text-start"
data-action="toggleSection" data-section="1">
Section 1
</button>
<div data-show="section1Open" data-transition="slide-down">
<div>
<div class="p-3 bg-light border">
<p class="mb-0">Content for section 1. This slides down smoothly.</p>
</div>
</div>
</div>
</div>
<div class="accordion-item mt-2">
<button class="btn btn-outline-primary w-100 text-start"
data-action="toggleSection" data-section="2">
Section 2
</button>
<div data-show="section2Open" data-transition="slide-down">
<div>
<div class="p-3 bg-light border">
<p class="mb-0">Content for section 2. Each section animates independently.</p>
</div>
</div>
</div>
</div>
</div>
</div>
wildflower.component('accordion-demo', {
state: {
section1Open: false,
section2Open: false
},
toggleSection(e) {
const section = e.target.dataset.section
const key = 'section' + section + 'Open'
this[key] = !this[key]
}
})
Slide down transition CSS:
/* Base: grid + transition are permanent so the layout is stable before any class toggle */
[data-transition="slide-down"] {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.3s ease;
}
[data-transition="slide-down"] > * { overflow: hidden; min-height: 0; }
/* Transition classes only change the track size */
.slide-down-enter { grid-template-rows: 0fr; }
.slide-down-enter-active { grid-template-rows: 1fr; }
.slide-down-leave-active { grid-template-rows: 0fr; }
max-height trick requires a magic pixel value and produces mismatched timing. grid-template-rows: 0fr → 1fr animates the actual content height with no guesswork. The base styles keep display: grid and transition permanent so the grid layout is stable before any class toggle. Important: the direct child of the transition element must be a bare wrapper with no padding or border, otherwise the grid track can't collapse to zero. Put visual styling on an inner element.
With data-show vs data-render
Transitions work with both conditional directives:
data-show + transition
Element remains in DOM, visibility toggled. Animation plays on each show/hide.
<div data-show="visible"
data-transition="fade">
Content
</div>
data-render + transition
Element inserted/removed from DOM. Animation plays on insert/removal.
<div data-render="exists"
data-transition="scale">
Content
</div>
JavaScript Lifecycle Hooks
For advanced control, components can define transition hooks that are called at key moments:
wildflower.component('advanced-transitions', {
state: { isVisible: true },
toggle() {
this.isVisible = !this.isVisible
},
// Called before enter transition starts
onBeforeEnter(el) {
console.log('About to show:', el)
},
// Called when enter transition starts
onEnter(el, done) {
console.log('Entering:', el)
// Call done() if you want to control when transition completes
},
// Called after enter transition completes
onAfterEnter(el) {
console.log('Fully visible:', el)
},
// Called before leave transition starts
onBeforeLeave(el) {
console.log('About to hide:', el)
},
// Called when leave transition starts
onLeave(el, done) {
console.log('Leaving:', el)
},
// Called after leave transition completes
onAfterLeave(el) {
console.log('Fully hidden:', el)
}
})
- Focus management (focus an input after modal appears)
- Analytics tracking (log when content becomes visible)
- Cleanup operations (cancel pending requests when hiding)
- Chained animations (start another animation after one completes)
Example: Modal Dialog
A complete modal implementation with fade transition:
<div data-component="modal-demo">
<button data-action="openModal">Open Modal</button>
<div data-show="isOpen" data-transition="fade" class="modal-overlay">
<div class="modal-content">
<h3>Modal Title</h3>
<p>This modal fades in and out smoothly.</p>
<button data-action="closeModal">Close</button>
</div>
</div>
</div>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
max-width: 400px;
}
/* Fade transition */
.fade-enter { opacity: 0; }
.fade-enter-active { transition: opacity 0.3s ease; opacity: 1; }
.fade-leave { opacity: 1; }
.fade-leave-active { transition: opacity 0.3s ease; opacity: 0; }
wildflower.component('modal-demo', {
state: { isOpen: false },
openModal() {
this.isOpen = true
},
closeModal() {
this.isOpen = false
},
// Focus trap when modal opens
onAfterEnter(el) {
const firstButton = el.querySelector('button')
if (firstButton) firstButton.focus()
}
})
Example: Accordion
Collapsible sections with slide-down animation:
<div data-component="accordion-demo">
<button data-action="toggleSection" data-section="1">Section 1</button>
<div data-show="section1Open" data-transition="slide-down">
<div>
<div class="accordion-content">
<p>Content for section 1.</p>
</div>
</div>
</div>
<button data-action="toggleSection" data-section="2">Section 2</button>
<div data-show="section2Open" data-transition="slide-down">
<div>
<div class="accordion-content">
<p>Content for section 2.</p>
</div>
</div>
</div>
</div>
wildflower.component('accordion-demo', {
state: {
section1Open: false,
section2Open: false
},
toggleSection(e) {
const section = e.target.dataset.section
const key = `section${section}Open`
this[key] = !this[key]
}
})
Example: Notification Stack
Using data-render with transitions for notifications that are added/removed from DOM:
<div data-component="notification-stack">
<button data-action="addNotification">Add Notification</button>
<div class="notification-area">
<div data-render="notifications.length > 0">
<div data-list="notifications">
<template>
<div data-transition="slide" class="notification">
<span data-bind="message"></span>
<button data-action="dismiss">×</button>
</div>
</template>
</div>
</div>
</div>
</div>
Rapid Toggle Handling
The framework gracefully handles rapid state changes during transitions:
- If visibility changes mid-transition, the current transition is cancelled
- The new transition starts immediately from the current visual state
- No "stuck" states or animation conflicts occur
- Final state always matches the last state change
wildflower.component('rapid-toggle-demo', {
state: { isVisible: true },
rapidToggle() {
// Even rapid toggles work correctly
for (let i = 0; i < 5; i++) {
setTimeout(() => {
this.isVisible = !this.isVisible
}, i * 50)
}
}
})
Custom Transition Names
Use any name you want - just match it in your CSS:
<!-- Use a custom transition name -->
<div data-show="active" data-transition="bounce">
Bouncy content!
</div>
/* Custom bounce transition */
.bounce-enter {
transform: scale(0.3);
opacity: 0;
}
.bounce-enter-active {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55),
opacity 0.3s ease;
transform: scale(1);
opacity: 1;
}
.bounce-leave {
transform: scale(1);
opacity: 1;
}
.bounce-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: scale(0.3);
opacity: 0;
}
Best Practices
Do
- Keep transitions short (200-400ms) for responsiveness
- Use
transformandopacityfor best performance - Match enter and leave durations for consistency
- Use
will-changefor frequently animated elements - Test transitions on slower devices
Don't
- Animate
width,height, ortop/left(causes reflow) - Use transitions longer than 500ms for UI elements
- Forget the
-activeclasses (nothing will animate) - Animate too many elements simultaneously
- Ignore users who prefer reduced motion
Respecting User Preferences
Consider users who prefer reduced motion:
@media (prefers-reduced-motion: reduce) {
.fade-enter-active,
.fade-leave-active,
.slide-enter-active,
.slide-leave-active,
.slide-down-enter-active,
.slide-down-leave-active,
.scale-enter-active,
.scale-leave-active {
transition: none !important;
}
}
Simple Syntax
Transitions use a single attribute with CSS class conventions:
<!-- Apply fade transition -->
<div data-transition="fade" data-show="isVisible">
Content fades in and out
</div>
No wrapper elements required. The transition name maps directly to CSS classes (.fade-enter, .fade-leave, etc.), giving you full control over the animation with standard CSS.
List Item Animations with AutoAnimate
WildflowerJS transitions (data-transition) handle individual element show/hide. For list item animations (smoothly animating items as they are added, removed, reordered, or filtered), use AutoAnimate, a zero-config FLIP animation library.
<TransitionGroup> components for list animations. WildflowerJS takes a different approach: rather than building a custom solution, we recommend AutoAnimate, a framework-agnostic library that works with any DOM manipulation, including WildflowerJS's data-list rendering.
Setup
AutoAnimate is loaded on demand via dynamic import(). No build step needed:
wildflower.component('animated-list', {
state: {
items: [
{ id: 1, name: 'First item' },
{ id: 2, name: 'Second item' }
],
nextId: 3
},
init() {
var self = this;
var listEl = this.element.querySelector('[data-list]');
if (!listEl) return;
// Load AutoAnimate and apply to the list container
import('https://cdn.jsdelivr.net/npm/@formkit/auto-animate@0.8.2/+esm')
.then(function(mod) {
self._animController = mod.default(listEl, { duration: 150 });
}).catch(function() {});
},
addItem() {
this.items.push({
id: this.nextId++,
name: 'Item ' + this.nextId
});
},
removeItem(event, element, details) {
var index = details.index;
this.items.splice(index, 1);
},
destroy() {
// Always clean up: disable the observer when component is removed
if (this._animController) this._animController.disable();
}
});
<div data-component="animated-list">
<button data-action="addItem">Add Item</button>
<ul data-list="items" data-key="id">
<template>
<li>
<span data-bind="name"></span>
<button data-action="removeItem">Remove</button>
</li>
</template>
</ul>
</div>
Using with SortableJS (Drag and Drop)
When combining AutoAnimate with SortableJS for drag-and-drop, disable AutoAnimate during drag operations to prevent the two libraries from conflicting:
init() {
var self = this;
var listEl = this.element.querySelector('.sortable-list');
// SortableJS for drag and drop
this._sortable = new Sortable(listEl, {
animation: 75,
onStart: function() {
if (self._animController) self._animController.disable();
},
onEnd: function(evt) {
// Re-enable after one frame so SortableJS finishes
// removing ghost/chosen classes first
requestAnimationFrame(function() {
if (self._animController) self._animController.enable();
});
// ... handle reorder via store
}
});
// AutoAnimate for add/remove/filter animations
import('https://cdn.jsdelivr.net/npm/@formkit/auto-animate@0.8.2/+esm')
.then(function(mod) {
self._animController = mod.default(listEl, { duration: 150 });
}).catch(function() {});
},
destroy() {
if (this._sortable) this._sortable.destroy();
if (this._animController) this._animController.disable();
}
destroy() method to prevent memory leaks. See Third-Party Integration for more details.
Configuration Options
AutoAnimate accepts an options object:
// Customize animation behavior
autoAnimate(element, {
duration: 150, // Animation duration in ms (default: 250)
easing: 'ease-out' // CSS easing function (default: 'ease-in-out')
});
// The returned controller can enable/disable animations
var controller = autoAnimate(element);
controller.disable(); // Pause animations
controller.enable(); // Resume animations
controller.isEnabled(); // Check status