
In this post, I’m excited to share a project that I built for one of my projects—a dynamic hero animation that seamlessly transitions between multiple content layers. This animation doesn’t just look cool—it’s a great exercise in combining HTML, CSS, and JavaScript (with Anime.js) to create an engaging introduction for your website.
The idea is to offer a visually engaging hero section that uses a video background, headlines, and text transitions. In my implementation, the hero section consists of two main parts: one showing the video with introductory text and the other with secondary overlay content. I also handle scroll events to toggle CSS classes and control the animation.
The HTML Structure
Below is the HTML structure for the hero animation:
<section class="section-hero">
<div class="careers-hero" id="careers-hero">
<div class="hero-1" id="hero-1">
<div class="hero-1-video mb-6 mt-12 lg:my-12" id="hero-1-video">
@include('includes.video', ['src' => $hero_video, 'poster' => $hero_image])
</div>
<div class="hero-1-text mx-auto text-center">
<h1 class="mb-6 text-5xl text-white lg:mb-8 lg:text-6xl">{!! modify($hero_title)->nl2br() !!}</h1>
<p class="max-w-3xl text-center text-lg text-base-gray">{!! modify($hero_text)->nl2br() !!}</p>
</div>
</div>
<div class="hero-2" id="hero-2">
<div class="hero-2-text text-center">
<h2 class="mb-6 text-4xl lg:mb-8 lg:text-5xl">{!! modify($hero_colored_title)->nl2br() !!}</h2>
<p class="mx-auto max-w-3xl text-base">{!! modify($hero_colored_text)->nl2br() !!}</p>
</div>
</div>
</div>
</section>
In this markup, Hero 1 contains the video element and associated text, while Hero 2 provides a secondary overlay with additional messaging. Blade includes and helper functions allow easy updating of the hero’s video, background image, title, and text.
The Animation Code
The following JavaScript uses Anime.js to control the transitions based on the scroll progress. It updates element classes to switch between natural, fixed, and absolute positioning. This is how the animation becomes interactive.
import anime from 'animejs'
export default function () {
let hero1El = document.getElementById('hero-1')
let hero2El = document.getElementById('hero-2')
let hero = document.getElementById('careers-hero')
let videoElement = document.querySelector('#hero-1-video video')
function registerAnimations() {
let w = window.innerWidth
function adaptLayout(progress) {
if (progress === 0) {
// Natural flow; remove fixed or absolute positioning.
hero1El.classList.remove('hero-1-fixed')
hero1El.classList.remove('hero-1-absolute')
hero2El.classList.remove('hero-2-fixed')
hero2El.classList.remove('hero-2-absolute')
videoElement?.play()
} else if (progress === 100) {
// When fully scrolled, switch to absolute positioning.
hero1El.classList.add('hero-1-absolute')
hero2El.classList.add('hero-2-absolute')
hero1El.classList.remove('hero-1-fixed')
hero2El.classList.remove('hero-2-fixed')
videoElement?.play()
} else if (progress > 0 && progress < 100 && !hero1El.classList.contains('hero-1-fixed')) {
// Mid-transition: fix the hero sections to create a parallax effect.
hero1El.classList.add('hero-1-fixed')
hero2El.classList.add('hero-2-fixed')
hero1El.classList.remove('hero-1-absolute')
hero2El.classList.remove('hero-2-absolute')
videoElement?.pause()
}
}
if (w >= 768) {
let tl2 = anime.timeline({
autoplay: false,
update: function (anim) {
adaptLayout(anim.progress)
},
})
let hero1text = {
targets: '.hero-1-text',
opacity: [1, 0],
duration: 60,
easing: 'easeInOutQuad',
}
let hero1video = {
targets: '.hero-1-video',
opacity: [1, 0.9],
filter: ['blur(0px)', 'blur(50px)'],
scale: [1, 4],
duration: 180,
easing: 'easeInOutQuad',
}
let hero2 = {
targets: '.hero-2',
opacity: [0, 1],
duration: 300,
easing: 'linear',
}
let hero2text = {
targets: '.hero-2-text',
opacity: [0, 1],
translateY: [150, 0],
duration: 80,
easing: 'easeInOutQuad',
}
let heroStay = {
targets: '.hero-2',
opacity: [1, 1],
duration: 50,
}
tl2.add(hero1text)
.add(hero1video)
.add(hero2)
.add(hero2text)
.add(heroStay)
const scrollPercent = () => {
const bodyST = document.body.scrollTop
const docST = document.documentElement.scrollTop
const docSH = document.documentElement.scrollHeight
const docCH = document.documentElement.clientHeight
const divHeightPerc = hero.offsetHeight / docSH
const perc = (docST + bodyST) / (docSH - docCH)
return (perc / divHeightPerc) * 2
}
const handleScroll = () => {
tl2.seek(scrollPercent() * tl2.duration)
}
window.onscroll = handleScroll
}
}
registerAnimations()
}
This script selects key elements (including the video) and uses a timeline defined with Anime.js. The timeline consists of animations for fading out the text, blurring and scaling the video, fading in Hero 2, and moving its text upward. During the scroll, the code calculates the scroll percentage relative to the hero section’s height and updates the timeline accordingly.
The CSS (Using Tailwind CSS)
The below style definitions utilize Tailwind CSS utilities and helper classes to manage the layout and transition positions. Classes are added and removed via JavaScript.
.careers-hero {
@apply overflow-hidden h-full md:h-[140vh] relative w-full z-[10];
.hero-1 {
@apply relative md:absolute flex flex-col items-center justify-center px-6 py-16 md:top-0 md:z-20 md:z-[1](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) md:h-full md:w-full md:p-0;
@apply w-full md:h-screen top-0 z-[1](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks);
}
.hero-1-fixed {
@apply !fixed;
}
.hero-1-absolute {
@apply !absolute !bottom-0;
top: unset;
}
.hero-2 {
@apply md:absolute flex md:h-screen w-full flex-col items-center justify-center bg-gradient-to-r from-[#3157FF] to-[#B42DFF];
@apply !py-32 px-6 text-center text-white md:fixed md:top-0 md:z-[6](https://webapps.stackexchange.com/questions/169461/how-can-i-format-code-properly-in-a-chatgpt-prompt) md:p-0 md:opacity-0;
@apply top-0 left-0;
}
.hero-2-fixed {
@apply !fixed !top-0;
}
.hero-2-absolute {
@apply !absolute !bottom-0;
top: unset;
}
}
This CSS uses both fixed and absolute positioning depending on scroll progress. The utility classes ensure responsiveness across mobile and desktop.
Conclusion
This hero animation project is a great way to learn how to combine technologies like CSS, JavaScript, and Anime.js to create an interactive digital experience. It provides a visually engaging entry point for your website and demonstrates practical handling of scroll events to drive animations.
I hope this guide inspires you to experiment with similar interactive effects on your personal projects or blog. Feel free to modify this code to suit your needs.