Building Animated Background Stripes With CSS
Most front-end developers reach for background-size when they need to cover an image or fill an element. But the property has more advanced uses. One of them happens to be the key to creating background stripes that smoothly transition on hover.
The effect relies on chaining multiple backgrounds, carefully controlling their sizes, and using blend modes to layer content. Here’s how to build it from scratch.
Starting With a Simple Setup
Begin with a single styled <div> — a green square with fixed dimensions:
<div></div>
div {
width: 500px;
height: 500px;
background: palegreen;
}

Creating the Stripes With Gradients
A repeating gradient won't work here since the stripes need uneven widths and should transition independently. Instead, chain five separate backgrounds on top of the existing background color, positioning them toward the top-right of the container:
div {
width: 500px;
height: 500px;
background:
linear-gradient(black, black) top right,
linear-gradient(black, black) top 100px right,
linear-gradient(black, black) top 200px right,
linear-gradient(black, black) top 300px right,
linear-gradient(black, black) top 400px right,
palegreen;
}
The example above produces horizontal stripes, though vertical ones work equally well with this technique. Custom properties keep the code cleaner:
div {
--gt: linear-gradient(black, black);
--n: 100px;
width: 500px;
height: 500px;
background:
var(--gt) top right,
var(--gt) top var(--n) right,
var(--gt) top calc(var(--n) * 2) right,
var(--gt) top calc(var(--n) * 3) right,
var(--gt) top calc(var(--n) * 4) right,
palegreen;
}
Here --gt holds the gradient and --n is a constant that nudges the stripes downward so they sit at an offset. Notice the gradients are solid black rather than true color transitions — that's deliberate and becomes important when blending layers later.
Prevent the backgrounds from tiling so they don't fill the whole space:
div {
--gt: linear-gradient(black, black);
--n: 100px;
width: 500px;
height: 500px;
background:
var(--gt) top right,
var(--gt) top var(--n) right,
var(--gt) top calc(var(--n) * 2) right,
var(--gt) top calc(var(--n) * 3) right,
var(--gt) top calc(var(--n) * 4) right,
palegreen;
background-repeat: no-repeat;
}
The background-repeat property is written separately here for readability, though it could be folded into the background shorthand.
Controlling Stripe Size and Offset
At this point the stripes are indistinguishable from a solid black square because they lack spacing and cover the entire container. That's where background-size comes in. Its two-value syntax sets both height and width, and multiple values can be comma-separated to match the chained backgrounds.
Setting only widths using the single-value syntax defaults heights to auto, which causes stripes to overlap — the wider stripes hide those beneath them:
div {
--gt: linear-gradient(black, black);
--n: 100px;
width: 500px;
height: 500px;
background:
var(--gt) top right,
var(--gt) top var(--n) right,
var(--gt) top calc(var(--n) * 2) right,
var(--gt) top calc(var(--n) * 3) right,
var(--gt) top calc(var(--n) * 4) right,
palegreen;
background-repeat: no-repeat;
background-size: 60%, 90%, 70%, 40%, 10%;
}
Adding explicit heights makes each stripe visible. Reusing the existing --n variable keeps the values consistent:
div {
--gt: linear-gradient(black, black);
--n: 100px;
width: 500px;
height: 500px;
background:
var(--gt) top right,
var(--gt) top var(--n) right,
var(--gt) top calc(var(--n) * 2) right,
var(--gt) top calc(var(--n) * 3) right,
var(--gt) top calc(var(--n) * 4) right,
palegreen;
background-repeat: no-repeat;
background-size: 60% var(--n), 90% var(--n), 70% var(--n), 40% var(--n), 10% var(--n); // HIGHLIGHT 15
}
Adding Gaps Between Stripes
To create visible gaps, shrink each stripe's height slightly so it falls short of the full vertical space. Subtract a small offset — here 5px — from the stripe height using calc():
background-size: 60% calc(var(--n) - 5px), 90% calc(var(--n) - 5px), 70% calc(var(--n) - 5px), 40% calc(var(--n) - 5px), 10% calc(var(--n) - 5px);
A variable eliminates the repetition:
div {
--h: calc(var(--n) - 5px);
/* etc. */
background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
}
Masking and Blending Layers
Swap the green background for white. A black-and-white pattern is ideal for masking and blending:
div {
/* etc. */
background:
var(--gt) top right,
var(--gt) top var(--n) right,
var(--gt) top calc(var(--n) * 2) right,
var(--gt) top calc(var(--n) * 3) right,
var(--gt) top calc(var(--n) * 4) right,
#fff;
/* etc. */
}
Wrap the original <div> in a parent container and add a second <div> beneath it:
<section>
<div></div>
<div></div>
</section>
Move the fixed width and height properties from the original <div> to the new parent:
section {
width: 500px;
height: 500px;
}
CSS Grid stacks the two <div> elements on top of each other. Both children occupy the full container via grid-area, aligned toward the center:
section {
display: grid;
align-items: center;
justify-items: center;
width: 500px;
height: 500px;
}
section > div {
width: inherit;
height: inherit;
grid-area: 1 / 1;
}
The solid black gradient now pays off. Rather than true masking with the mask property, the contrast between the two layers determines visibility: white areas remain white, black areas let the underlying layer show through. Blend modes document this behavior well.
Apply the actual gradient to the first <div>, while the second one gets the stripe rules from the original setup, selected via :nth-child():
div:nth-child(1) {
background: linear-gradient(to right, red, orange);
}
div:nth-child(2) {
--gt: linear-gradient(black, black);
--n: 100px;
--h: calc(var(--n) - 5px);
background:
var(--gt) top right,
var(--gt) top var(--n) right,
var(--gt) top calc(var(--n) * 2) right,
var(--gt) top calc(var(--n) * 3) right,
var(--gt) top calc(var(--n) * 4) right,
white;
background-repeat: no-repeat;
background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
}
Without blending, there's no visual difference yet. Adding the screen blend mode reveals the effect:
div:nth-child(2) {
/* etc. */
mix-blend-mode: screen;
}
A slightly darker beige background, rather than pure white, allows some color to bleed through from behind.
Animating on Hover
The hover effect stretches the stripes to full width. The selector targets the parent container — a <section> here — and changes the background-size of the stripes in the second <div>:
/* When <section> is hovered, change the second div's styles */
section:hover > div:nth-child(2){
/* styles go here */
}
Set the stripe widths to cover the full container while keeping their heights:
section:hover > div:nth-child(2){
background-size: 100% var(--h);
}
Without a transition the change snaps instantly. Adding one makes the stripes expand smoothly on hover:
section:hover > div:nth-child(2){
background-size: 100% var(--h);
transition: background-size 1s;
}
If text is placed over the effect, check color contrast against the gradient colors for WCAG compliance. It's also worth respecting prefers-reduced-motion settings.
The technique is maintainable: stripe height, colors, and direction are adjustable by changing a few values. Additional custom properties for widths and colors would make it even more configurable for reuse across a design system.



