Transition Group
<quiet-transition-group>
Transition groups improve the user's experience by adding subtle animations as items are added, removed, and reordered in the group.
Wrap a collection of elements in a transition group and use normal DOM APIs to add, remove, and reorder them. The transition group will automatically apply the appropriate animations as elements enter and exit the group.
<div id="transition-group__boxes"> <!-- Add your elements here --> <quiet-transition-group> <div class="box">1</div> <div class="box">2</div> <div class="box">3</div> <div class="box">4</div> <div class="box">5</div> <div class="box">6</div> <div class="box">7</div> <div class="box">8</div> </quiet-transition-group> <div class="buttons"> <quiet-button data-action="add">Add random</quiet-button> <quiet-button data-action="remove">Remove random</quiet-button> <quiet-button data-action="shuffle">Shuffle</quiet-button> <quiet-button data-action="disable" toggle="off">Disable transitions</quiet-button> </div> </div> <!-- Everything below is for the demo --> <script> const container = document.getElementById('transition-group__boxes'); const transitionGroup = container.querySelector('quiet-transition-group'); const shuffleButton = container.querySelector('quiet-button[data-action="shuffle"]'); const addButton = container.querySelector('quiet-button[data-action="add"]'); const removeButton = container.querySelector('quiet-button[data-action="remove"]'); const disableButton = container.querySelector('quiet-button[data-action="disable"]'); let count = transitionGroup.children.length; function addRandomBox() { if (transitionGroup.isTransitioning) return; const children = [...transitionGroup.children]; const randomSibling = children[Math.floor(Math.random() * children.length)]; const box = document.createElement('div'); box.classList.add('box'); box.textContent = String(++count); if (randomSibling) { randomSibling.before(box); } else { transitionGroup.append(box) } } function removeRandomBox() { if (transitionGroup.isTransitioning) return; const boxes = [...transitionGroup.children]; if (boxes.length > 0) { const randomIndex = Math.floor(Math.random() * boxes.length); boxes[randomIndex].remove(); } } function shuffleBoxes() { if (transitionGroup.isTransitioning) return; const boxes = [...transitionGroup.children]; boxes.sort(() => Math.random() - 0.5); boxes.forEach(box => transitionGroup.append(box)); } // Handle button clicks addButton.addEventListener('click', addRandomBox); removeButton.addEventListener('click', removeRandomBox); shuffleButton.addEventListener('click', shuffleBoxes); disableButton.addEventListener('click', () => { transitionGroup.disableTransitions = disableButton.toggle !== 'off'; }); </script> <style> #transition-group__boxes { quiet-transition-group { display: flex; gap: 1rem; flex-wrap: wrap; flex-direction: row; margin-block-end: 2rem; } .buttons { display: flex; gap: .5rem; flex-wrap: wrap; } .box { display: flex; align-items: center; justify-content: center; width: 100px; height: 100px; font-size: 1.5rem; font-weight: var(--quiet-font-weight-semibold); background: var(--quiet-primary-fill-mid); border-radius: var(--quiet-border-radius); color: var(--quiet-primary-text-on-mid); @media screen and (max-width: 959px) { font-size: 1.25rem; width: 80px; height: 80px; } } } </style>
This example includes logic and styles for the demo, but the minimal markup you need for a transition group is shown below. Note that only direct children of the transition group will be animated.
<quiet-transition-group> <div class="item">...</div> <div class="item">...</div> <div class="item">...</div> </quiet-transition-group>
For best results, avoid applying transitions, animations, and inline styles to transition group items, as they may interfere with the component's animations. Also avoid modifying the DOM during a transition to prevent interruptions.
Transition groups honor the user's
prefers-reduced-motion
setting. If you're not seeing animations, this might be why. To override this behavior, which is generally
not recommended, use the ignore-reduced-motion
attribute.
Examples Jump to heading
Working with elements Jump to heading
Use standard DOM APIs to add, remove, and reorder elements and the transition group will automatically animate your changes. This example demonstrates how adding, removing, and reordering list items will be handled by the transition group.
<div id="transition-group__list"> <!-- A template for list items --> <template> <div class="list-item"> <span class="label"></span> <quiet-button-group label="Sort"> <quiet-button size="sm" data-action="up" icon-label="Move up"><quiet-icon name="arrow-up"></quiet-icon></quiet-button> <quiet-button size="sm" data-action="down" icon-label="Move down"><quiet-icon name="arrow-down"></quiet-icon></quiet-button> </quiet-button-group> <quiet-button size="sm" variant="text" data-action="delete" icon-label="Delete"><quiet-icon name="trash"></quiet-icon></quiet-button> </div> </template> <quiet-transition-group> <!-- Items are generated using the <template> above --> </quiet-transition-group> <form> <quiet-text-field placeholder="New item" appearance="filled" pill></quiet-text-field> <quiet-button type="submit" variant="primary" icon-label="Add to list" pill><quiet-icon name="plus"></quiet-icon></quiet-button> <quiet-divider orientation="vertical"></quiet-divider> <quiet-button pill data-action="shuffle">Shuffle</quiet-button> </form> </div> <script> const container = document.getElementById('transition-group__list'); const transitionGroup = container.querySelector('quiet-transition-group'); const template = container.querySelector('template'); const addForm = container.querySelector('form'); const shuffleButton = container.querySelector('[data-action="shuffle"]'); let count = transitionGroup.children.length; function createListItem(label = '') { const listItem = template.content.cloneNode(true); listItem.querySelector('.label').textContent = label; return listItem; } // Create the initial list if (transitionGroup.children.length === 0) { ['Feed the cats', 'Change the litter box', 'Buy catnip', 'Playtime'].forEach(label => { const listItem = createListItem(label); transitionGroup.append(listItem); updateListItems(); }); } // Update the list now that the content has changed transitionGroup.addEventListener('quiet-content-changed', updateListItems); // Add an item addForm.addEventListener('submit', event => { const textField = addForm.querySelector('quiet-text-field'); const label = textField.value; const listItem = createListItem(label || '(Untitled)'); transitionGroup.append(listItem); textField.value = ''; event.preventDefault(); }); // Shuffle items shuffleButton.addEventListener('click', () => { const items = [...transitionGroup.children]; items.sort(() => Math.random() - 0.5); items.forEach(item => transitionGroup.append(item)); }); // Handle item actions container.addEventListener('click', event => { const button = event.target.closest('[data-action]'); if (!button) return; const action = button.getAttribute('data-action'); const listItem = button.closest('.list-item'); // Delete an item if (action === 'delete') { listItem.remove(); } // Move an item up if (action === 'up' && listItem.previousElementSibling) { listItem.previousElementSibling.before(listItem); } // Move an item down if (action === 'down' && listItem.nextElementSibling) { listItem.nextElementSibling.after(listItem); } }); // Make sure the first/last items have the up/down buttons disabled function updateListItems() { const listItems = transitionGroup.querySelectorAll('.list-item'); listItems.forEach((listItem, index) => { listItem.querySelector('[data-action="up"]').disabled = index === 0; listItem.querySelector('[data-action="down"]').disabled = index === listItems.length - 1; }); } </script> <style> #transition-group__list { quiet-transition-group { display: flex; flex-direction: column; gap: .5rem; margin-block-end: 1rem; } form { display: flex; gap: .5rem; align-items: center; quiet-text-field { flex: 1 1 auto; } quiet-button { flex: 0 0 auto; } quiet-divider { height: 2rem; } quiet-text-field::part(label), quiet-text-field::part(description) { display: none; } } .list-item { display: flex; gap: .5rem; align-items: center; justify-content: start; width: 100%; background-color: var(--quiet-paper-color); border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer); border-radius: var(--quiet-border-radius); box-shadow: var(--quiet-shadow-softer); color: var(--quiet-neutral-text-on-soft); padding: .5rem 1rem; .label { flex: 1 1 auto; } quiet-button { flex: 0 0 auto; } quiet-button[data-action="delete"] quiet-icon { color: var(--quiet-destructive-text-colorful); } } } </style>
As elements are added, removed, and reordered, their positions in the DOM change instantly. The elements
are then animated from their old positions to their new positions, meaning CSS selectors such as
:first-child
, :last-child
, :nth-child()
, et al will apply as soon
as the transition starts, which may not always be desirable. To avoid this, use ids and classes in lieu of
position-based selectors.
Awaiting transitions Jump to heading
Avoid making DOM changes while a transition is running. The transitionComplete
property holds a
promise that resolves when the current or next transition ends. You can await it to be sure all animations
are complete before proceeding with further DOM changes.
<div id="transition-group__awaiting"> <quiet-transition-group style="--duration: 1000ms;"> <div class="box" style="background-color: deeppink;"></div> <div class="box" style="background-color: dodgerblue;"></div> </quiet-transition-group> <div class="controls"> <quiet-button>Swap</quiet-button> </div> </div> <script> const container = document.getElementById('transition-group__awaiting'); const transitionGroup = container.querySelector('quiet-transition-group'); const swapButton = container.querySelector('quiet-button'); swapButton.addEventListener('click', async () => { const box = transitionGroup.querySelector('.box'); // Disable the button and swap the elements swapButton.disabled = true; transitionGroup.append(box); // Wait for the transition to complete and enable the button await transitionGroup.transitionComplete; swapButton.disabled = false; }); </script> <style> #transition-group__awaiting { quiet-transition-group { flex-direction: row; gap: 1rem; } .box { width: 80px; height: 80px; border-radius: var(--quiet-border-radius); } .controls { margin-block-start: 2rem; } } </style>
Changing the layout Jump to heading
Transition groups use a columnar flex layout, by default. To change the layout, apply
flex-direction: row
to the transition group. You can add spacing between items by setting the
gap
property.
quiet-transition-group { flex-direction: row; gap: 2rem; }
For best results, avoid using complex layouts within transition groups.
Changing the duration Jump to heading
To change the animation speed, set the --duration
custom property on the transition group. Each
transition is made up of one or more steps (e.g. add, remove, reposition) and the duration applies to each
individual step, not the total transition time.
<quiet-transition-group style="--duration: 1250ms;" id="transition-group__duration"> <div class="box" style="background-color: deeppink;"></div> <div class="box" style="background-color: dodgerblue;"></div> <div class="box" style="background-color: rebeccapurple;"></div> <div class="box" style="background-color: tomato;"></div> </quiet-transition-group> <script> const transitionGroup = document.getElementById('transition-group__duration'); function moveTheBox() { const lastChild = transitionGroup.lastElementChild; if (lastChild) { transitionGroup.prepend(lastChild); } } // Move the box again after the transition ends transitionGroup.addEventListener('quiet-transition-end', () => { setTimeout(moveTheBox, 1000); }); // Ensure the element is registered Promise.all([ customElements.whenDefined('quiet-transition-group'), transitionGroup.updateComplete ]) .then(moveTheBox); </script> <style> #transition-group__duration { flex-direction: row; gap: 1rem; .box { width: 80px; height: 80px; border-radius: var(--quiet-border-radius); } } </style>
Changing the animation Jump to heading
Transition groups use the
Web Animations API
to move elements around. To customize the enter and exit animations, pass a
QuietTransitionAnimation
object to the transition group's
transitionAnimation
property. A QuietTransitionAnimation
includes
keyframes
and
easings
for entering and exiting animations. The interface looks like this:
interface QuietTransitionAnimation { enter: { keyframes: Keyframe[]; easing: string; }; exit: { keyframes: Keyframe[]; easing: string; }; }
Here's an example of a custom animation that scales and fades elements in and out as they enter and leave.
const transitionGroup = document.querySelector('quiet-transition-group'); transitionGroup.transitionAnimation = { enter: { keyframes: [ { opacity: 0, scale: 0.75 }, { opacity: 1, scale: 1 } ], easing: 'cubic-bezier(0.33, 1.2, 0.66, 1)' }, exit: { keyframes: [ { opacity: 1, scale: 1 }, { opacity: 0, scale: 0.75 } ], easing: 'cubic-bezier(0.33, 0, 0.67, 0.2)' } };
Using Scurry animations Jump to heading
Quiet's Scurry module provides a number of ready-to-use, RTL-friendly animations that work great with transition groups. You can install Scurry locally using npm or import animations directly from the CDN.
If you're using npm, install Scurry using the following command.
npm i @quietui/scurry
Here you can preview the animations that are available in Scurry.
Import any of the transition animation functions as shown below. Animations are RTL-aware, so make sure to
call each function with the dir
parameter to get a QuietTransitionAnimation
object
with proper directionality. Then, apply it to the appropriate transition group's
transitionAnimation
property.
// Import an animation import { tornado } from '@quietui/scurry'; // Get a reference to the transition group const transitionGroup = document.querySelector('quiet-transition-group'); // Change the animation transitionGroup.transitionAnimation = tornado({ dir: 'ltr' });
Disabling transitions Jump to heading
Add the disable-transitions
attribute to disable transition animations. Note that the DOM will
still be modified when this option is enabled.
<div id="transition-group__disabling"> <quiet-transition-group> <div class="circle" style="background-color: deeppink;"></div> <div class="circle" style="background-color: dodgerblue;"></div> <div class="circle" style="background-color: rebeccapurple;"></div> </quiet-transition-group> <quiet-switch label="Disable transitions"></quiet-switch> </div> <script> const container = document.getElementById('transition-group__disabling'); const transitionGroup = container.querySelector('quiet-transition-group'); const disableSwitch = container.querySelector('quiet-switch'); // Toggle transitions disableSwitch.addEventListener('quiet-change', () => { transitionGroup.disableTransitions = !transitionGroup.disableTransitions; }); // Rotate the circles every second setInterval(() => { const firstChild = transitionGroup.firstElementChild; if (firstChild) { transitionGroup.append(firstChild); } }, 1000); </script> <style> #transition-group__disabling { text-align: center; quiet-transition-group { --duration: 500ms; --easing: ease-in-out; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 60px; height: 220px; margin-block-end: 2rem; } .circle { width: 80px; height: 80px; border-radius: 50%; &:nth-child(1) { grid-column: 1 / -1; grid-row: 1; justify-self: center; } &:nth-child(2) { grid-column: 1; grid-row: 2; justify-self: end; } &:nth-child(3) { grid-column: 2; grid-row: 2; justify-self: start; } } } </style>
API Jump to heading
Importing Jump to heading
The autoloader is the recommended way to import components but, if you prefer to do it manually, the following code snippets will be helpful.
To manually import <quiet-transition-group>
from the CDN, use the following code.
import 'https://cdn.jsdelivr.net/npm/@quietui/quiet@1.0.0/dist/components/transition-group/transition-group.js';
To manually import <quiet-transition-group>
from npm, use the following code.
import '@quietui/quiet/dist/components/transition-group/transition-group.js';
Slots Jump to heading
Transition Group supports the following slots. Learn more about using slots
Name | Description |
---|---|
(default) | One or more elements to transition when adding, removing, and reordering the DOM. |
Properties Jump to heading
Transition Group has the following properties that can be set with corresponding attributes. In many cases, the attribute's name is the same as the property's name. If an attribute is different, it will be displayed after the property. Learn more about attributes and properties
Property / Attribute | Description | Reflects | Type | Default |
---|---|---|---|---|
isTransitioning
|
Determines if the transition group is currently animating. (Property only) |
|
boolean
|
false
|
transitionAnimation
|
A custom animation to use for enter/exit transitions, Works well with animations from
@quietui/scurry . (Property only)
|
|
QuietTransitionAnimation
|
|
transitionComplete
|
A promise that resolves when the current or next transition ends. This is a great way to ensure transitions have stopped before doing something else. (Property only) |
|
Promise<void>
|
|
disableTransitions
disable-transitions
|
Disables transition animations. However, the quiet-content-changed and
quiet-transition-end events will still be dispatched.
|
|
boolean
|
false
|
ignoreReducedMotion
ignore-reduced-motion
|
By default, no animation will occur when the user indicates a preference for reduced motion. Use this attribute to override this behavior when necessary. |
|
boolean
|
false
|
Methods Jump to heading
Transition Group supports the following methods. You can obtain a reference to the element and call them like functions in JavaScript. Learn more about methods
Name | Description | Arguments |
---|---|---|
updateElementPositions() |
Updates the cached coordinates of all child elements in the transition group. In most cases, you shouldn't have to call this method. However, if you're resizing or animating elements imperatively, you may need to call this immediately before appending or removing elements to ensure a smooth transition. |
Events Jump to heading
Transition Group dispatches the following custom events. You can listen to them the same way was native events. Learn more about custom events
Name | Description |
---|---|
quiet-content-changed |
Emitted when content changes and before the transition animation begins. |
quiet-transition-end |
Emitted when transition animations end. |
CSS custom properties Jump to heading
Transition Group supports the following CSS custom properties. You can style them like any other CSS property. Learn more about CSS custom properties
Name | Description | Default |
---|---|---|
--duration |
The duration of each individual step (not the total transition time). |
0.25s
|
Custom States Jump to heading
Transition Group has the following custom states. You can target them with CSS using the selectors shown below. Learn more about custom states
Name | Description | CSS selector |
---|---|---|
transitioning |
Applied when a transition is active. |
:state(transitioning)
|