Transitions CORE+

Add smooth CSS animations to elements when they appear or disappear using the data-transition attribute.

Key Concept: Transitions automatically apply CSS classes during show/hide operations, enabling smooth animations without manual JavaScript. The framework handles timing, cleanup, and even rapid toggle interruptions.

How Transitions Work

When an element with data-transition changes visibility, the framework applies CSS classes in a specific sequence:

Entering (showing)
  1. .{name}-enter - Initial state (e.g., opacity: 0)
  2. .{name}-enter-active - Active state with transition (opacity: 1)
  3. Classes removed after transition completes
Leaving (hiding)
  1. .{name}-leave - Initial state (e.g., opacity: 1)
  2. .{name}-leave-active - Active state with transition (opacity: 0)
  3. 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-info mt-3">
        I fade in and out smoothly!
    </div>
</div>
wildflower.component('fade-demo', {
    state: { isVisible: true },

    toggle() {
        this.isVisible = !this.isVisible
    }
})
Live Preview

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
    }
})
Live Preview

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]
    }
})
Live Preview

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; }
Why grid-template-rows? The 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)
    }
})
Use Cases for Hooks:
  • 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 transform and opacity for best performance
  • Match enter and leave durations for consistency
  • Use will-change for frequently animated elements
  • Test transitions on slower devices
Don't
  • Animate width, height, or top/left (causes reflow)
  • Use transitions longer than 500ms for UI elements
  • Forget the -active classes (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.

Why AutoAnimate? Frameworks like Vue and Svelte have built-in <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();
}
Cleanup: Always disable or destroy third-party library instances in your component's 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
Core Kanban Board

Animated transitions on cards that move between columns, alongside drag-and-drop and modals.