Introduction
Interactive UI components are the foundation of any modern web interface. Accordions, tabs, modals and toast notifications are found on the majority of sites, but the quality of their animation makes all the difference between an ordinary experience and a premium interface.
In this tutorial, we will build 5 essential components with smooth CSS animations and accessible JavaScript. Each component comes with an interactive demo and the complete code to copy.
All components in this tutorial use CSS transitions for smoothness and vanilla JavaScript for logic. No external dependencies are required.
1. Animated accordion
The accordion is the ideal component for displaying collapsible content. Our version uses the max-height technique for a smooth transition on open and close.
Interactive demo
.accordion-item {
border: 1px solid #2a2a35;
border-radius: 12px;
margin-bottom: 8px;
overflow: hidden;
}
.accordion-header {
padding: 16px 20px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
.accordion-icon {
transition: transform 0.3s ease;
}
.accordion-item.active .accordion-icon {
transform: rotate(45deg);
}
/* Smooth transition with max-height */
.accordion-body {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease;
}
.accordion-item.active .accordion-body {
max-height: 200px;
}
function toggleAccordion(item) {
// Close other items (exclusive mode)
const siblings = item.parentElement
.querySelectorAll('.accordion-item');
siblings.forEach(sibling => {
if (sibling !== item) {
sibling.classList.remove('active');
}
});
// Toggle the clicked item
item.classList.toggle('active');
}
How it works
The technique relies on the max-height property:
- Closed state:
max-height: 0withoverflow: hiddenhides the content - Open state:
max-height: 200pxreveals the content with a smooth transition - The + icon rotates 45 degrees to form an x thanks to
transform: rotate(45deg)
Choose a max-height value slightly larger than the actual content. Too large a value visually slows the transition because the browser animates up to that value even if the content is smaller.
2. Animated tabs
Tabs allow organizing content into accessible sections without page changes. Our version integrates a sliding indicator that follows the active tab with a smooth transition.
Interactive demo
.tabs-nav {
display: flex;
border-bottom: 2px solid #2a2a35;
position: relative;
}
.tab-btn {
padding: 12px 24px;
background: none;
border: none;
color: #a1a1aa;
font-weight: 600;
cursor: pointer;
transition: color 0.3s;
}
.tab-btn.active {
color: #6366f1;
}
/* Indicateur glissant */
.tabs-indicator {
position: absolute;
bottom: -2px;
height: 2px;
background: #6366f1;
transition: left 0.3s ease,
width 0.3s ease;
}
/* Panel appearance animation */
.tab-panel {
display: none;
animation: tabFadeIn 0.3s ease;
}
.tab-panel.active {
display: block;
}
@keyframes tabFadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
function switchTab(btn, panelId) {
const container = btn.closest('.tabs-container');
// Disable all tabs and panels
container.querySelectorAll('.tab-btn')
.forEach(b => b.classList.remove('active'));
container.querySelectorAll('.tab-panel')
.forEach(p => p.classList.remove('active'));
// Activate selected tab and panel
btn.classList.add('active');
document.getElementById(panelId)
.classList.add('active');
// Move the indicator
updateIndicator(btn);
}
function updateIndicator(activeBtn) {
const indicator = document
.getElementById('tabIndicator');
indicator.style.left =
activeBtn.offsetLeft + 'px';
indicator.style.width =
activeBtn.offsetWidth + 'px';
}
3. Modal with animation
Modals are essential for confirmations, forms and important messages. Our version combines an entry animation with bounce (cubic-bezier) and an overlay with backdrop-filter.
Interactive demo
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
z-index: 2000;
align-items: center;
justify-content: center;
}
.modal-overlay.show {
display: flex;
animation: overlayIn 0.3s ease;
}
@keyframes overlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Entry animation with bounce */
.modal-box {
background: #12121a;
border-radius: 20px;
padding: 40px;
max-width: 480px;
width: 90%;
animation: modalIn 0.4s
cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
function openModal() {
const overlay = document
.getElementById('modalOverlay');
overlay.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeModal() {
const overlay = document
.getElementById('modalOverlay');
overlay.classList.remove('show');
document.body.style.overflow = '';
}
// Close by clicking the overlay
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeModal();
});
Remember to add overflow: hidden on the body when opening the modal to prevent background scrolling, and remove it on close.
4. Toast Notification
Toast notifications are temporary, non-intrusive messages. Our version uses a slide-in from the right with a cubic-bezier timing for a subtle bounce effect.
Interactive demo
.toast {
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
background: #12121a;
border: 1px solid #10b981;
border-radius: 14px;
display: flex;
align-items: center;
gap: 12px;
z-index: 3000;
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
/* Initial state: off-screen to the right */
transform: translateX(calc(100% + 40px));
opacity: 0;
transition: transform 0.4s
cubic-bezier(0.34, 1.56, 0.64, 1),
opacity 0.4s ease;
}
.toast.show {
transform: translateX(0);
opacity: 1;
}
function showToast() {
const toast = document
.getElementById('toastDemo');
toast.classList.add('show');
// Hide after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
5. Animated tooltip
Tooltips provide contextual information on hover. Our version uses a fade + translate with a CSS arrow in a pseudo-element for an elegant and lightweight result.
.tooltip-wrapper {
position: relative;
display: inline-block;
}
.tooltip {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%)
translateY(8px);
padding: 8px 16px;
background: #1a1a25;
border: 1px solid #2a2a35;
border-radius: 8px;
font-size: 0.85rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease,
transform 0.2s ease;
}
/* Tooltip arrow */
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1a1a25;
}
/* Show on hover */
.tooltip-wrapper:hover .tooltip {
opacity: 1;
transform: translateX(-50%)
translateY(0);
}
The tooltip is positioned above the parent element thanks to bottom: calc(100% + 10px). The arrow is created with an ::after pseudo-element using the CSS borders technique.
For a tooltip that automatically adapts to the position in the viewport (top, bottom, left or right), you will need to add JavaScript to detect available space and adjust the positioning class.
Best practices
Here are the essential recommendations for professional-quality UI components:
Performance
- Prefer
transformandopacityfor animations: these properties are GPU-optimized and do not trigger reflow - Avoid animating
heightdirectly: usemax-heightortransform: scaleY()instead - Use
will-changesparingly on frequently animated elements
Accessibility
ARIA attributes are essential to make your components usable by everyone:
<!-- Accordion accessible -->
<button
role="button"
aria-expanded="false"
aria-controls="panel-1"
>Section title</button>
<div
id="panel-1"
role="region"
aria-hidden="true"
>Contenu</div>
<!-- Modal accessible -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<h2 id="modal-title">Titre</h2>
</div>
<!-- Tabs accessibles -->
<div role="tablist">
<button
role="tab"
aria-selected="true"
aria-controls="tab-panel-1"
>Onglet 1</button>
</div>
UX
- Animation duration: 200-400ms is the ideal range. Below that, it's too fast to be perceived; above, it slows the user down
- Natural easing: use
easeorcubic-bezierrather thanlinearfor organic movement - Immediate feedback: the component must react to a click in less than 100ms, even if the full animation takes longer
- Respect
prefers-reduced-motionfor users sensitive to motion
@media (prefers-reduced-motion: reduce) {
.accordion-body,
.tabs-indicator,
.modal-box,
.toast,
.tooltip {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
Conclusion
Animated UI components are the key to a pleasant and professional web interface. By combining CSS transitions for smoothness, @keyframes for complex animations and vanilla JavaScript for logic, you get performant and accessible components.
The techniques presented in this tutorial -- max-height for accordions, sliding indicator for tabs, cubic-bezier for modals and translateX for toasts -- cover the majority of interactive component needs. Adapt the durations, easings and colors to your design system for a consistent result.
Discover our collection of ready-to-use components in the effects library, with one-click copyable code and interactive demos.