Mastering CSS - Sliding Background to an Active Button

August 30th, 2023
Blurry sunflowers

Today I'm going to show you how to make this button menu with a sliding background animation. After walking through this tutorial, you'll be one step closer to becoming a CSS wizard 🧙‍♂️… Here's what we'll be building:

The Setup

As always, if you'd like to reference my finished code, feel free to look at my example on codepen… Now, let's start with the markup. We'll use three buttons inside a container div.

<div class="container">
  <button>Home</button>
  <button>About</button>
  <button>Contact</button>
</div>

Pretty straightforward. Now let's add some styles to our container.

.container {
  display: flex;
  border-radius: 0.5rem;
  overflow: hidden;
  background: #fff;
}

First, we are setting our container div to be a flexbox. This will allow us to easily line up our buttons in a two dimensional layout, as a row. Then we'll make sure our container has some nice rounded corners by setting the border-radius and hiding anything that might go outside the boundary of the container's newly rounded corners with overflow: hidden;. And, of course, we can also explicitly set the background color to white.

This is all fine and dandy, but this will still look pretty ugly until we remove some of the default styles on our buttons.

.container > button {
  border: none;
  padding: 0.75rem 1.5rem;
  width: 150px;
  cursor: pointer;
  background: transparent;
}

Here, we are selecting all button elements that are an immediate child of the element with a class of 'container'. We'll remove their default borders, and give each button a padding and width. Then we'll specify that the cursor should be a pointer so that when you hover your mouse over this element, it will show the pointy icon. And finally, we'll make each button have a transparent background so they will take on the color of the container element (which we previously set to white). Now our button group is starting to take shape!

Button group

Adding a Pseudo-Element Background

Now that we've got our button group setup, let's add a background that we can slide from one element to another. In order to do this, we'll use a pseudo-element on the container div like so:

.container {
  ...
  position: relative;
}

.container::after {
  content: '';
  width: 33.33%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  background: rgba(44, 222, 115, 0.5);
}

We can use the ::after pseudo-selector to add an aesthetic change to the container div. This will create a “pseudo” element that is not rendered on the page, but can be used to append content or add more custom styles. In our case, we will use this pseudo-element to create an additional “background” for the container that only covers one third of the full width.

Since we want to position this new background relative to the container div, we first need to add position: relative; to the container. Then when we add position: absolute; to the pseudo-element, it will know to position itself relative to the container div.

We know we want the width of the background to take up one third of the full container's width, so we will set width: 33.33%, and we want the background to fill the full height of the parent container, so we set height: 100%;. By setting the top and left properties to zero, we are telling our pseudo-element to align with the top left corner of the container.

And of course, to make our pseudo-element stand out, we need to give it a unique color. It is important that we use a color that is semi-transparent, because our pseudo-element is actually sitting above the container div, and if we don't make give it any transparency, we won't be able to see our button's text underneath. This is why I've used an rgba value to specify an alpha of 0.5.

Making the Background Slide

Awesome! Now all that's left to do is reposition that pseudo-element whenever a button is clicked. This is going to require some JavaScript 😊

But first, let's talk about how we are going to accomplish this. In order to move the pseudo-element's horizontal position, we will need to update the value of the left property. As of right now, we have it hardcoded to zero, but if we wanted the background to cover the middle button, we would need to set left: 33.33%. And if we wanted it to cover the last button, we would need to set left: 66.66%, essentially offsetting the background's starting position by an additional one third of the container width for every button.

Before we add the JavaScript to update the value of the pseudo-element's left property, there is one more problem we need to address. In JavaScript, we can't set the value of a pseudo-element's style. In order to work around this, we'll set a custom property on the container div, and use that custom property's value in the pseudo-element:

.container {
  --bg-offset: 0%;
  ...
}

.container::after {
  ...
  left: var(--bg-offset);
}

This is accomplishing the same thing as left: 0%;, but it allows us to update the value of the custom property using JavaScript… Now we can add some fun JavaScript.

const WIDTH = 33.33;

const btnContainer = document.querySelector(".container");

function slideBg(n) {
  const bgOffset = WIDTH * n;
  btnContainer.style.setProperty("--bg-offset", `${bgOffset}%`);
}

We have three things here. First, we define a constant called WIDTH that represents a percentage width of each button. Since we have three buttons in the container, each button takes up 33.33% of the container's width. Next, we are grabbing the container div via querySelector and storing that in a constant called ‘btnContainer'. And finally, we have defined a function that will update the custom property, and thus the left position of our pseudo-element, based on which button (n) was selected.

To use our function, we can attach it as onClick event handlers on each of our buttons.

<div class="container">
  <button onClick="slideBg(0)">Home</button>
  <button onClick="slideBg(1)">About</button>
  <button onClick="slideBg(2)">Contact</button>
</div>

Now you should see the pseudo-element repositioning itself over each button whenever they are clicked. To animate the movement of the pseudo-element, we simply need to add one more line of CSS.

.container::after {
  ...
  transition: left 0.3s;
}

This tells the pseudo-element that it should use a 0.3 second transition whenever the left property is updated. Now we have our sliding background transitioning nicely to the button that was clicked!

To wrap up this example, let's add some borders in between the buttons so the user can easily distinguish which button they are clicking on.

.container > button:not(:last-of-type) {
  border-right: 1px solid #eee;
}

With this selector, we are targeting every button that is an immediate child of the container div, except for the last one. In our case, this is applying a border to the right side of the first and second buttons.


And just like that, we've created this awesome effect with CSS and a little JavaScript. You are now one step close to being a CSS wizard. Use your powers wisely.