Blog / CSS/JS

Animated Accordion, Tabs and Modals

Learn to create interactive and accessible UI components with smooth animations: accordion with max-height transition, tabs with sliding indicator, modals with animated entry/exit and toast notifications.

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.

💡
Good to know

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

What is an accordion? +
An accordion is a UI component that allows you to show and hide content sections interactively, saving vertical space on the page.
Why animate the opening? +
Animation guides the user's eye and creates a visual connection between the action (the click) and the result (the content appearing). Without animation, the change is abrupt and disorienting.
Which technique should you use? +
The max-height technique is the most reliable. We go from max-height: 0 to a value large enough to reveal the content, with a CSS transition for the animation.
accordion.css
.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;
}
accordion.js
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: 0 with overflow: hidden hides the content
  • Open state: max-height: 200px reveals the content with a smooth transition
  • The + icon rotates 45 degrees to form an x thanks to transform: rotate(45deg)
💡
Performance tip

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

Create your mockups with Figma or Sketch. Define colors, typography, and spacing before coding. A consistent design system accelerates development.
Write clean and semantic code. Use CSS variables for consistency and reusable components for maintainability.
Test your interface on different devices and browsers. Check performance, accessibility, and responsiveness before going to production.
tabs.css
.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);
  }
}
tabs.js
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';
}

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.css
.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);
  }
}
modal.js
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();
});
⚠️
Watch out for scroll

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.css
.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;
}
toast.js
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.css
.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.

💡
Dynamic positioning

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 transform and opacity for animations: these properties are GPU-optimized and do not trigger reflow
  • Avoid animating height directly: use max-height or transform: scaleY() instead
  • Use will-change sparingly on frequently animated elements

Accessibility

ARIA attributes are essential to make your components usable by everyone:

accessibility.html
<!-- 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 ease or cubic-bezier rather than linear for 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-motion for users sensitive to motion
reduced-motion.css
@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.

🎨
Go further

Discover our collection of ready-to-use components in the effects library, with one-click copyable code and interactive demos.

Action completed successfully!