Create Smooth Toggle Button Animations with View Transition API

Category: Animation , Javascript | December 29, 2025
Authorsimoncoudeville
Last UpdateDecember 29, 2025
LicenseMIT
Views40 views
Create Smooth Toggle Button Animations with View Transition API

A JavaScript implementation that creates a moving background highlight effect for toggle buttons, tabs UI, navigation menus, and segmented controls.

It uses the native View Transition API to animate an active indicator as users switch between options. Works with any button group structure and requires minimal setup.

See it in action:

How to use it:

1. Start with a semantic list structure for your toggle buttons. Each button sits inside a list item:

<ul class="toggle-buttons" aria-label="Frameworks">
  <li class="toggle-buttons__item">
    <button class="toggle-buttons__button" type="button">CSSScript</button>
  </li>
  <li class="toggle-buttons__item toggle-buttons__item--react">
    <button class="toggle-buttons__button" type="button">jQueryScript</button>
  </li>
  <li class="toggle-buttons__item toggle-buttons__item--vue">
    <button class="toggle-buttons__button" type="button">ReactScript</button>
  </li>
  <li class="toggle-buttons__item toggle-buttons__item--angular">
    <button class="toggle-buttons__button" type="button">VueScript</button>
  </li>
  <li class="toggle-buttons__item toggle-buttons__item--svelte">
    <button class="toggle-buttons__button" type="button">ScriptByAI</button>
  </li>
</ul>

2. The following CSS handles both the CSS styles and the View Transition API:

The View Transition API properties (view-transition-name and view-transition-class) tell the browser which elements to animate. The pseudo-elements control the animation behavior.

/* Remove default button styling */
button {
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  outline: none;
}
button:focus-visible {
  outline-style: solid;
  outline-color: hsl(0 0% 100% / 0.2);
  outline-width: 2px;
}
/* Container for all toggle buttons */
.toggle-buttons {
  list-style: none;
  display: flex;
  gap: 0.25rem; /* Space between buttons */
  padding: 0.25rem;
  border-radius: 3rem; /* Pill shape */
  background-color: #000;
}
/* Each button wrapper - uses CSS Grid for layering */
.toggle-buttons__item {
  display: grid;
  position: relative;
}
/* Stack button and active indicator in the same grid cell */
.toggle-buttons__button,
.toggle-buttons__active {
  grid-area: 1 / 1; /* Both occupy the first cell */
}
.toggle-buttons__button {
  padding: 0.5rem 1.25rem;
  position: relative;
  z-index: 1; /* Keeps button above the background */
  view-transition-class: toggle-button; /* Groups all buttons for transitions */
}
/* The moving active background indicator */
.toggle-buttons__active {
  background-color: deeppink; /* Default color */
  border-radius: 3rem;
  view-transition-name: active-toggle-button; /* Unique identifier for animation */
}
/* Custom colors for different framework options */
.toggle-buttons__item--react .toggle-buttons__active {
  background-color: #4e98b6;
}
.toggle-buttons__item--vue .toggle-buttons__active {
  background-color: #42b883;
}
.toggle-buttons__item--angular .toggle-buttons__active {
  background-color: #dd0031;
}
.toggle-buttons__item--svelte .toggle-buttons__active {
  background-color: #ff3e00;
}
/* View Transition API pseudo-elements */
::view-transition-group(.toggle-button) {
  z-index: 2; /* Buttons stay on top during transition */
}
::view-transition-group(active-toggle-button) {
  z-index: 1; /* Background animates beneath */
  animation-timing-function: cubic-bezier(0.8, -0.4, 0.5, 1); /* Custom easing */
  animation-duration: 0.25s;
}
/* Ensure background fills the full height during animation */
::view-transition-old(active-toggle-button),
::view-transition-new(active-toggle-button) {
  height: 100%;
}

3. Create the active indicator and manages click events. The script checks for View Transition API support through document.startViewTransition. Browsers without support get instant transitions instead of animations.

// Select all toggle buttons
const buttons = document.querySelectorAll(".toggle-buttons__button");
// Create the moving background element
const activeSpan = document.createElement("span");
activeSpan.classList.add("toggle-buttons__active");
activeSpan.setAttribute("aria-hidden", "true"); // Hide from screen readers
// Place the active indicator in the first button by default
const firstItem = buttons[0].parentElement;
firstItem.appendChild(activeSpan);
// Set up each button
buttons.forEach((button, index) => {
  // Assign unique view transition name for animation tracking
  button.style.viewTransitionName = `button-${index + 1}`;
  
  // Mark first button as pressed, others as not pressed
  button.setAttribute("aria-pressed", index === 0);
  // Handle button clicks
  button.addEventListener("click", () => {
    // Reset all buttons to unpressed state
    buttons.forEach((b) => b.setAttribute("aria-pressed", "false"));
    
    // Mark clicked button as pressed
    button.setAttribute("aria-pressed", "true");
    // Get the parent list item of the clicked button
    const newItem = button.parentElement;
    // Check if browser supports View Transition API
    if (!document.startViewTransition) {
      // Fallback: move indicator without animation
      newItem.appendChild(activeSpan);
      return;
    }
    // Use View Transition API for smooth animation
    document.startViewTransition(() => {
      newItem.appendChild(activeSpan);
    });
  });
});

FAQs:

Q: How do I add more toggle buttons?
A: Add new list items to the HTML with the same structure. The JavaScript automatically detects all buttons with the .toggle-buttons__button class.

Q: Why does the animation feel sluggish?
A: Check your animation-duration value in the ::view-transition-group(active-toggle-button) rule. The default is 0.25 seconds. Lower values create snappier transitions.

Q: How do I change the animation easing?
A: Modify the animation-timing-function property in the ::view-transition-group(active-toggle-button) CSS rule. The current cubic-bezier creates a bouncy effect. Use ease-in-out for smoother motion or create custom cubic-bezier values.

You Might Be Interested In:


Leave a Reply