Animated Space Landing Page with CSS

Build an animated space-themed landing page using CSS keyframes and transitions.

Introduction

30 Second Summary

Every website you visit uses motion to grab your attention. Buttons pulse, menus slide in, and elements fade into view using just a few CSS techniques.

In this project, you will build an animated space landing page for a fictional company called "Nova Voyages." You'll create twinkling stars, a floating rocket, and smooth hover effects using only HTML and CSS.

What You'll Build

A space-themed landing page with entrance animations, continuous floating effects, and interactive hover transitions built entirely with vanilla HTML and CSS.

By the end of this project, you'll have:

  • A fully animated space landing page with twinkling stars, a floating rocket, and staggered entrance animations running in your browser.
  • Hands-on experience with CSS @keyframes, transitions, and transform functions that bring static pages to life.
  • A reusable collection of animation patterns (fade-in, float, pulse, hover scale) you can apply to any future web project.
  • Secret Mission: Add a shooting star that streaks across the sky using multi-step percentage keyframes and animation-delay.

Are there any prerequisites?

None. This project starts from scratch and requires no prior coding experience. You just need a text editor (VS Code) and a web browser (Chrome).

Before We Start

Before We Start...

Set Up Your Project

Every web page needs two core files: an HTML file for content and structure, and a CSS file for visual styling.

In this step, you'll create a project folder on your Desktop, write the HTML skeleton for the Nova Voyages landing page, and verify it loads in your browser.

In this step, get ready to:
  • Create a nova-voyages folder and project files.
  • Write the page's HTML structure with navigation and hero content.
  • Open the page in Chrome to confirm it loads correctly.
Create your project folder and files

Before you can write any code, you need a dedicated folder to hold your project files.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running this command:
mkdir nova-voyages
  • Now open this folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the nova-voyages folder, and click Open.

VS Code now shows the empty nova-voyages folder in the Explorer sidebar on the left.

  • In the Explorer sidebar, hover over the folder name and click the New File icon (the page icon with a plus sign).
  • Type index.html and press Enter.
  • Click the New File icon again.
  • Type style.css and press Enter.
  • Confirm both files exist by checking the Explorer sidebar.

You should see index.html and style.css listed under the nova-voyages folder.

Don't see both files?

Make sure you clicked the New File icon (page with a plus sign), not the New Folder icon (folder with a plus sign).

If you accidentally created a folder, right-click it and select Delete, then try again.

Help me create files in VS Code

Write the HTML structure

The HTML file defines what content appears on the page and how it is organized.

Your landing page has three main sections: decorative star elements scattered across the background, a navigation bar with links, and a hero section with the company name and a call-to-action button.

  • Click on index.html in the Explorer sidebar to open it in the editor.
  • Copy the following code and paste it into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nova Voyages - Explore the Cosmos</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="star" style="top: 10%; left: 20%;"></div>
    <div class="star" style="top: 25%; left: 75%;"></div>
    <div class="star" style="top: 50%; left: 40%;"></div>
    <div class="star" style="top: 15%; left: 90%;"></div>
    <div class="star" style="top: 70%; left: 15%;"></div>
    <div class="star" style="top: 80%; left: 60%;"></div>
    <div class="star" style="top: 40%; left: 5%;"></div>
    <div class="star" style="top: 60%; left: 85%;"></div>

    <nav class="main-nav">
        <a href="#" class="nav-link">Home</a>
        <a href="#" class="nav-link">Missions</a>
        <a href="#" class="nav-link">About</a>
        <a href="#" class="nav-link">Contact</a>
    </nav>

    <main class="hero">
        <div class="rocket">🚀</div>
        <h1 class="hero-title">Nova Voyages</h1>
        <p class="hero-subtitle">Your journey to the stars begins here.</p>
        <button class="cta-button">Launch Mission</button>
    </main>
</body>
</html>

What does this code do?

  • The <head> section sets the page title and links to your style.css file so CSS styles will apply later.
  • Eight <div class="star"> elements are scattered across the page using inline top and left percentages. These will become twinkling stars once you style them.
  • The <nav> holds four navigation links that you'll animate with hover effects.
  • The <main class="hero"> section contains the rocket emoji, page heading, subtitle, and a call-to-action button. All of these will get entrance animations.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Seeing red underlines or errors?

Check that every opening tag has a matching closing tag. Common culprits: a missing </nav> or </main>.

Make sure the <link> tag is inside the <head> section, not inside <body>.

Help me fix my HTML

Open your page in Chrome

The <link rel="stylesheet" href="style.css"> tag in your HTML head connects the page to your stylesheet. The stylesheet is empty for now, so you'll see unstyled content.

  • Open your file manager by pressing Cmd+Space and typing Finder (macOS), or pressing the Windows key and typing File Explorer (Windows).
  • Navigate to your Desktop, then open the nova-voyages folder.
  • Double-click index.html to open it in your default browser.

What if it doesn't open in Chrome?

If the file opens in a different browser, right-click index.html instead. Select Open With and choose Google Chrome.

You should see a plain white page with unstyled text: navigation links at the top, a rocket emoji, the heading "Nova Voyages", a subtitle, and a "Launch Mission" button.

Seeing a blank page?

Make sure you saved index.html before opening it in the browser. Unsaved files show a dot on the tab in VS Code.

If you see the raw HTML code instead of rendered content, you may have opened the file as text. Make sure you opened it from the file manager (double-click), not by pasting the file path into a text editor.

Help me get my page to show in Chrome

✔️ Awesome, I've got everything!

Great. Double check that you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file you should have:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nova Voyages - Explore the Cosmos</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="star" style="top: 10%; left: 20%;"></div>
    <div class="star" style="top: 25%; left: 75%;"></div>
    <div class="star" style="top: 50%; left: 40%;"></div>
    <div class="star" style="top: 15%; left: 90%;"></div>
    <div class="star" style="top: 70%; left: 15%;"></div>
    <div class="star" style="top: 80%; left: 60%;"></div>
    <div class="star" style="top: 40%; left: 5%;"></div>
    <div class="star" style="top: 60%; left: 85%;"></div>

    <nav class="main-nav">
        <a href="#" class="nav-link">Home</a>
        <a href="#" class="nav-link">Missions</a>
        <a href="#" class="nav-link">About</a>
        <a href="#" class="nav-link">Contact</a>
    </nav>

    <main class="hero">
        <div class="rocket">🚀</div>
        <h1 class="hero-title">Nova Voyages</h1>
        <p class="hero-subtitle">Your journey to the stars begins here.</p>
        <button class="cta-button">Launch Mission</button>
    </main>
</body>
</html>

Your style.css file should be empty for now.

Your project skeleton is ready. Next up, you'll turn this plain white page into a dark space scene with a gradient background and twinkling stars.

Style the Space Background and Layout

Your HTML structure is in place, but right now Chrome is showing plain text on a white background. A space-themed landing page needs to feel like you're looking out into the cosmos.

In this step, you'll transform that blank white page into a dark, star-filled sky using CSS gradients and animations. By the end, your page will have a deep space background with twinkling stars scattered across it.

In this step, get ready to:
  • Style the page with a dark radial gradient background and centered layout.
  • Create twinkling star animations using @keyframes.
  • Give each star a unique animation delay so they twinkle independently.
Add the dark gradient background and page layout

Every browser adds default margins and padding to HTML elements. A CSS reset removes these defaults so your design starts from a clean slate.

  • In VS Code, open style.css from the Explorer sidebar.
  • Add the CSS reset and body styling by pasting this code:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d1a 50%, #000000 100%);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: #ffffff;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
}

What does this code do?

The * selector targets every element on the page and removes default margins and padding. box-sizing: border-box makes element sizing more predictable by including padding in width calculations.

The body styles create the space effect. radial-gradient draws a gradient that radiates outward from the center, transitioning from dark navy to pure black at the edges. display: flex with align-items: center centers all content horizontally.

  • Save style.css.
  • Switch to Chrome and refresh the page. You should see a deep dark background that fades from dark navy at the center to black at the edges, with white text and navigation links visible.

Still seeing a white background?

  • Make sure you saved style.css. Unsaved files show a dot next to their name in the VS Code tab.
  • Check that your index.html still has <link rel="stylesheet" href="style.css"> in the <head>. This line connects your CSS file to the HTML.
  • Confirm both files are in the same nova-voyages folder.

Why isn't my CSS background showing up in Chrome?

Create the twinkling stars effect

Your HTML already has eight <div class="star"> elements with inline top and left positions. Now you need CSS to make them look and behave like stars.

  • In style.css, add the star styling and twinkle animation below your body styles:
/* Stars */
.star {
    position: absolute;
    width: 3px;
    height: 3px;
    background: #ffffff;
    border-radius: 50%;
    animation: twinkle 2s ease-in-out infinite alternate;
}

@keyframes twinkle {
    from {
        opacity: 0.3;
    }
    to {
        opacity: 1;
    }
}

What does this code do?

The .star class styles each star as a tiny 3px white circle. position: absolute lets the inline top and left values in the HTML place each star at a specific spot on the page.

The animation shorthand applies the twinkle keyframes over 2 seconds, repeating forever (infinite) and reversing direction each cycle (alternate). The keyframes fade each star between 30% and 100% opacity, creating a gentle pulsing glow.

What is @keyframes?

A @keyframes rule defines the stages of a CSS animation. You give it a name (like twinkle) and then specify what properties change at each point. The browser smoothly transitions between those points automatically.

You can use from/to for simple two-state animations, or percentage values (like 0%, 50%, 100%) for more complex multi-step animations.

  • Save style.css and refresh Chrome.
  • You should see small white dots scattered across the dark background, all fading in and out together.

Don't see any stars?

  • Make sure the @keyframes twinkle block is outside of the .star rule. Keyframes are standalone rules, not nested inside selectors.
  • Check that animation: twinkle in .star matches the name after @keyframes exactly. Names are case-sensitive.

Why aren't my star elements showing up on the page?

Give each star a unique twinkle timing

Right now all eight stars pulse in perfect unison. Real stars twinkle at different rates, so you need to offset their animation start times using animation-delay.

  • In style.css, add these nth-child selectors between your .star rule and the @keyframes twinkle block:
.star:nth-child(2) {
    animation-delay: 0.3s;
    width: 2px;
    height: 2px;
}

.star:nth-child(3) {
    animation-delay: 0.7s;
}

.star:nth-child(4) {
    animation-delay: 1.1s;
    width: 2px;
    height: 2px;
}

.star:nth-child(5) {
    animation-delay: 0.5s;
}

.star:nth-child(6) {
    animation-delay: 1.4s;
    width: 2px;
    height: 2px;
}

.star:nth-child(7) {
    animation-delay: 0.9s;
}

.star:nth-child(8) {
    animation-delay: 1.7s;
    width: 2px;
    height: 2px;
}

What does this code do?

Each :nth-child() selector targets a specific star by its position in the HTML. The animation-delay property offsets when each star begins its twinkle cycle. Star 2 waits 0.3 seconds, star 3 waits 0.7 seconds, and so on.

Some stars also get reduced to 2px width and height. This size variation makes the star field feel more natural, as if some stars are farther away.

  • Save style.css and refresh Chrome.
  • Your stars should now twinkle independently at different rates. You'll see some fade in while others fade out, creating a convincing night sky effect.

Stars still blinking in sync?

  • Make sure each nth-child selector has a different animation-delay value. If they all share the same delay, they'll still pulse together.
  • Verify your curly braces match up. A missing } on one selector can break everything below it.

Help me debug why my stars all animate at the same time.

✔️ Awesome, I've got everything!

Great. Double check you've saved style.css before moving on.

ⓧ I'd like to double check the full code

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d1a 50%, #000000 100%);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: #ffffff;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* Stars */
.star {
    position: absolute;
    width: 3px;
    height: 3px;
    background: #ffffff;
    border-radius: 50%;
    animation: twinkle 2s ease-in-out infinite alternate;
}

.star:nth-child(2) {
    animation-delay: 0.3s;
    width: 2px;
    height: 2px;
}

.star:nth-child(3) {
    animation-delay: 0.7s;
}

.star:nth-child(4) {
    animation-delay: 1.1s;
    width: 2px;
    height: 2px;
}

.star:nth-child(5) {
    animation-delay: 0.5s;
}

.star:nth-child(6) {
    animation-delay: 1.4s;
    width: 2px;
    height: 2px;
}

.star:nth-child(7) {
    animation-delay: 0.9s;
}

.star:nth-child(8) {
    animation-delay: 1.7s;
    width: 2px;
    height: 2px;
}

@keyframes twinkle {
    from {
        opacity: 0.3;
    }
    to {
        opacity: 1;
    }
}

Your space background is alive with twinkling stars. Next up, you'll animate the hero content so the heading, subtitle, and button slide into view when the page loads.

Animate the Hero Content on Page Load

Your space background is alive with twinkling stars. Now it's time to make the main content feel just as dynamic.

Right now the hero heading, subtitle, and button just sit on the page with no sense of arrival. In this step, you'll create an entrance animation that fades and slides each element into view, one after another.

In this step, get ready to:
  • Create a fadeSlideIn keyframes animation.
  • Apply the animation to the hero heading with the animation shorthand.
  • Add staggered delays so elements appear in sequence.
Create the fadeSlideIn keyframes animation

A @keyframes rule defines the stages of an animation. You give it a name and describe what CSS properties should look like at the start and end (or at any percentage in between).

Before we animate, we need to position the hero content properly. Let's add layout styles for the navigation and hero section first, then define the entrance animation.

  • In style.css, find the closing brace of your @keyframes twinkle block (the last line in your file).
  • Add the navigation and hero layout styles below it by copying this code:
/* Navigation */
.main-nav {
    padding: 2rem;
    display: flex;
    gap: 2rem;
    z-index: 10;
}

.nav-link {
    color: #aaaacc;
    text-decoration: none;
    font-size: 1rem;
    transition: color 0.3s ease, transform 0.3s ease;
}

/* Hero Section */
.hero {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    z-index: 10;
}

.rocket {
    font-size: 4rem;
    margin-bottom: 1rem;
}

What does this code do?

.main-nav uses flexbox to space out the navigation links horizontally. z-index: 10 ensures the nav sits above the stars.

.hero centers all the hero content vertically and horizontally using flexbox. flex: 1 makes it expand to fill the remaining viewport height.

The transition on .nav-link prepares the links for smooth hover effects you'll add in a later step.

  • Save style.css and refresh Chrome.
  • You should see the navigation links (Home, Missions, About, Contact) evenly spaced at the top, and the rocket emoji, heading, subtitle, and button centered on the page.

Content not centered?

Make sure your body still has display: flex and align-items: center from the previous step. These work together with the hero's flex: 1 to center everything vertically.

Help me figure out why my hero content isn't centered on the page.

Now that the content is positioned, let's define the entrance animation.

  • Add the @keyframes fadeSlideIn animation below your .rocket rule by copying this code:
@keyframes fadeSlideIn {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

What does this animation do?

This keyframes rule defines two states. from is where the element starts: invisible (opacity: 0) and pushed 30 pixels below its final spot (translateY(30px)).

to is where it ends: fully visible (opacity: 1) and in its natural position (translateY(0)). The browser smoothly transitions between these two states.

Why use transform instead of top or margin?

Animating transform and opacity is significantly more performant than animating box model properties like margin or top. Transform and opacity only trigger the browser's composition step, avoiding costly layout recalculations.

Apply the animation to the hero heading

The keyframes are just a recipe. To make an element use that recipe, you apply it with the animation shorthand property, which lets you set the duration, timing function, and fill mode all in one line.

  • Add the .hero-title styles between your .rocket rule and the @keyframes fadeSlideIn block by copying this code:
.hero-title {
    font-size: 4rem;
    font-weight: 700;
    letter-spacing: 2px;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out forwards;
}

What does the animation shorthand mean?

The line animation: fadeSlideIn 1s ease-out forwards packs four values into one shorthand:

  • fadeSlideIn is the name of the @keyframes rule to use.
  • 1s is the duration (one second from start to finish).
  • ease-out is the timing function (starts fast, then decelerates for a natural feel).
  • forwards is the fill mode (keeps the element in its final state after the animation ends).

The opacity: 0 at the top hides the heading by default. Without it, the heading would flash briefly before the animation starts.

  • Save style.css and refresh Chrome.
  • You should see the "Nova Voyages" heading fade in and slide up from below over one second. The subtitle and button remain invisible for now.

Heading appears instantly without animation?

Check that .hero-title has opacity: 0 set before the animation line. Without it, the heading is already visible and there's nothing to animate in.

Also confirm the animation name fadeSlideIn in your .hero-title rule matches the name in your @keyframes fadeSlideIn definition exactly. Names are case-sensitive.

Help me debug why my CSS entrance animation isn't working on the hero title.

Add staggered animation delays to the subtitle and button

A single entrance animation is good, but staggering multiple elements creates a cinematic sequence. The animation-delay property tells the browser to wait before starting an animation, so each element can appear slightly after the previous one.

  • Add the .hero-subtitle and .cta-button styles between your .hero-title rule and the @keyframes fadeSlideIn block by copying this code:
.hero-subtitle {
    font-size: 1.3rem;
    color: #ccccee;
    margin-top: 1rem;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.3s forwards;
}

.cta-button {
    margin-top: 2rem;
    padding: 1rem 2.5rem;
    font-size: 1.1rem;
    font-weight: 600;
    color: #ffffff;
    background: linear-gradient(135deg, #6a11cb, #2575fc);
    border: none;
    border-radius: 50px;
    cursor: pointer;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.6s forwards;
}

How does the stagger work?

Both elements use the same fadeSlideIn animation. The difference is the delay value (the third time value in the shorthand):

  • .hero-title has no delay (starts immediately).
  • .hero-subtitle waits 0.3s before starting.
  • .cta-button waits 0.6s before starting.

Each element is hidden with opacity: 0 until its turn arrives. The forwards fill mode keeps each element visible after its animation completes.

  • Save style.css and refresh Chrome.
  • You should see three elements animate in sequence: first the "Nova Voyages" heading slides up, then the subtitle appears 0.3 seconds later, and finally the purple gradient "Launch Mission" button fades in at 0.6 seconds.

Subtitle or button never appears?

If an element stays invisible, check that its animation line includes forwards at the end. Without the fill mode, the element snaps back to opacity: 0 the instant the animation finishes.

Also make sure the delay value (0.3s or 0.6s) comes after the duration (1s) in the shorthand. If you accidentally swap them, the animation runs too fast or starts too late.

Help me debug why my subtitle or button isn't appearing after the animation.

What is animation-fill-mode?

By default, CSS animations don't retain their end state. Once the animation finishes, the element reverts to its original styles. Setting forwards overrides this. The element stays in the state defined by the last keyframe (to) permanently.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full style.css file so far. Compare yours against this:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d1a 50%, #000000 100%);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: #ffffff;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* Stars */
.star {
    position: absolute;
    width: 3px;
    height: 3px;
    background: #ffffff;
    border-radius: 50%;
    animation: twinkle 2s ease-in-out infinite alternate;
}

.star:nth-child(2) {
    animation-delay: 0.3s;
    width: 2px;
    height: 2px;
}

.star:nth-child(3) {
    animation-delay: 0.7s;
}

.star:nth-child(4) {
    animation-delay: 1.1s;
    width: 2px;
    height: 2px;
}

.star:nth-child(5) {
    animation-delay: 0.5s;
}

.star:nth-child(6) {
    animation-delay: 1.4s;
    width: 2px;
    height: 2px;
}

.star:nth-child(7) {
    animation-delay: 0.9s;
}

.star:nth-child(8) {
    animation-delay: 1.7s;
    width: 2px;
    height: 2px;
}

@keyframes twinkle {
    from {
        opacity: 0.3;
    }
    to {
        opacity: 1;
    }
}

/* Navigation */
.main-nav {
    padding: 2rem;
    display: flex;
    gap: 2rem;
    z-index: 10;
}

.nav-link {
    color: #aaaacc;
    text-decoration: none;
    font-size: 1rem;
    transition: color 0.3s ease, transform 0.3s ease;
}

/* Hero Section */
.hero {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    z-index: 10;
}

.rocket {
    font-size: 4rem;
    margin-bottom: 1rem;
}

.hero-title {
    font-size: 4rem;
    font-weight: 700;
    letter-spacing: 2px;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out forwards;
}

.hero-subtitle {
    font-size: 1.3rem;
    color: #ccccee;
    margin-top: 1rem;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.3s forwards;
}

.cta-button {
    margin-top: 2rem;
    padding: 1rem 2.5rem;
    font-size: 1.1rem;
    font-weight: 600;
    color: #ffffff;
    background: linear-gradient(135deg, #6a11cb, #2575fc);
    border: none;
    border-radius: 50px;
    cursor: pointer;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.6s forwards;
}

@keyframes fadeSlideIn {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

Your hero content now makes a dramatic entrance every time the page loads. Next up, you'll bring the rocket emoji to life with a continuous floating animation that runs forever.

Create the Floating Rocket Animation

Your hero content now fades in beautifully with staggered timing. The heading, subtitle, and button appear one after another, creating a polished entrance sequence.

The rocket emoji is sitting above the title, but it feels static compared to the animated text below it. In this step, you will give the rocket a continuous floating motion that makes it look like it is gently drifting in space.

In this step, get ready to:
  • Define a @keyframes float animation that moves the rocket up and down with a tilt.
  • Apply the animation to the rocket so it loops continuously.
Define the @keyframes float animation

The twinkle animation you built earlier changes a single property (opacity). This new animation combines two transform functions at once to create more natural-looking motion.

  • In style.css, scroll down to the .rocket rule.
  • Add the following @keyframes float block directly below the closing } of the .rocket rule:
@keyframes float {
    from {
        transform: translateY(-10px) rotate(-3deg);
    }
    to {
        transform: translateY(10px) rotate(3deg);
    }
}

What does this animation do?

This keyframe defines two states the rocket will move between.

  • translateY(-10px) moves the rocket 10 pixels above its natural position. translateY(10px) moves it 10 pixels below.
  • rotate(-3deg) and rotate(3deg) add a subtle tilt as the rocket moves. This makes the bobbing motion feel more natural, like an object gently drifting in zero gravity.

You will not see a change yet. The keyframes define the motion, but the next substep connects this animation to the rocket element.

Apply the float animation to the rocket

A @keyframes block on its own does nothing until you attach it to an element with the animation property. The shorthand property lets you set the animation name, duration, timing, and behavior in a single line.

  • In style.css, find the .rocket rule. It currently looks like this:
.rocket {
    font-size: 4rem;
    margin-bottom: 1rem;
}
  • Add the animation line between font-size and margin-bottom, so the rule now looks like this:
.rocket {
    font-size: 4rem;
    animation: float 3s ease-in-out infinite alternate;
    margin-bottom: 1rem;
}

What does this animation shorthand mean?

  • float is the name of the @keyframes block you just defined.
  • 3s sets the duration. One full from-to cycle takes 3 seconds.
  • ease-in-out makes the motion start slow, speed up in the middle, and slow down at each end. This avoids the jerky feeling of linear motion.
  • infinite repeats the animation forever instead of playing once.
  • alternate reverses direction each cycle. The rocket goes from top to bottom, then bottom to top, creating a smooth continuous bob instead of jumping back to the start.
  • Save style.css and refresh your browser.
  • You should see the rocket emoji gently bobbing up and down with a slight tilt, creating a floating-in-space effect.

Rocket not moving?

  • Check that the animation name in .rocket is float and matches the @keyframes float name exactly.
  • Make sure the @keyframes float block is outside the .rocket rule, not inside its curly braces.
  • Confirm you saved the file. Unsaved changes will not appear in the browser.

Why isn't my rocket float animation working?

Why animate transform instead of top or margin?

Animating transform and opacity is a performance best practice. These properties only trigger style recalculation and composition, not layout or repaint.

Animating box model properties like margin or top forces the browser to recalculate layout for the entire page on every frame. With transform, the browser handles the movement on the GPU, keeping animations smooth even on slower devices.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css file so far. Compare yours against it:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d1a 50%, #000000 100%);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: #ffffff;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* Stars */
.star {
    position: absolute;
    width: 3px;
    height: 3px;
    background: #ffffff;
    border-radius: 50%;
    animation: twinkle 2s ease-in-out infinite alternate;
}

.star:nth-child(2) {
    animation-delay: 0.3s;
    width: 2px;
    height: 2px;
}

.star:nth-child(3) {
    animation-delay: 0.7s;
}

.star:nth-child(4) {
    animation-delay: 1.1s;
    width: 2px;
    height: 2px;
}

.star:nth-child(5) {
    animation-delay: 0.5s;
}

.star:nth-child(6) {
    animation-delay: 1.4s;
    width: 2px;
    height: 2px;
}

.star:nth-child(7) {
    animation-delay: 0.9s;
}

.star:nth-child(8) {
    animation-delay: 1.7s;
    width: 2px;
    height: 2px;
}

@keyframes twinkle {
    from {
        opacity: 0.3;
    }
    to {
        opacity: 1;
    }
}

/* Navigation */
.main-nav {
    padding: 2rem;
    display: flex;
    gap: 2rem;
    z-index: 10;
}

.nav-link {
    color: #aaaacc;
    text-decoration: none;
    font-size: 1rem;
    transition: color 0.3s ease, transform 0.3s ease;
}

/* Hero Section */
.hero {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    z-index: 10;
}

.rocket {
    font-size: 4rem;
    animation: float 3s ease-in-out infinite alternate;
    margin-bottom: 1rem;
}

@keyframes float {
    from {
        transform: translateY(-10px) rotate(-3deg);
    }
    to {
        transform: translateY(10px) rotate(3deg);
    }
}

.hero-title {
    font-size: 4rem;
    font-weight: 700;
    letter-spacing: 2px;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out forwards;
}

.hero-subtitle {
    font-size: 1.3rem;
    color: #ccccee;
    margin-top: 1rem;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.3s forwards;
}

.cta-button {
    margin-top: 2rem;
    padding: 1rem 2.5rem;
    font-size: 1.1rem;
    font-weight: 600;
    color: #ffffff;
    background: linear-gradient(135deg, #6a11cb, #2575fc);
    border: none;
    border-radius: 50px;
    cursor: pointer;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.6s forwards;
}

@keyframes fadeSlideIn {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

Your rocket is now gently floating in space. Next up, you will add interactive hover effects to the navigation links and a pulsing glow to the call-to-action button.

Add Hover Transitions to Navigation and Button

Your page already has entrance animations and a floating rocket. The next layer of polish is interactivity. Right now, hovering over the nav links and button does nothing.

In this step, you will make the navigation links glow and scale when hovered, and give the call-to-action button a continuously pulsing glow that draws the visitor's eye.

In this step, get ready to:
  • Add hover effects to the nav links using CSS transitions.
  • Create a pulsing glow animation on the call-to-action button.
  • Test all animations together in Chrome.
Add hover effects to nav links

Your .nav-link rule already includes a transition property that says "animate color and transform changes over 0.3 seconds." Now you need the hover state that actually triggers those changes.

  • In style.css, find the .nav-link rule, then add this new rule directly below it:
.nav-link:hover {
    color: #ffffff;
    transform: scale(1.1);
    text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
}

What does this code do?

When the learner hovers over a nav link, three things happen simultaneously:

  • color: #ffffff brightens the link text from grey to white.
  • transform: scale(1.1) makes the link grow to 110% of its size.
  • text-shadow adds a soft white glow around the text, like starlight.

Because the transition property on .nav-link already handles color and transform, these changes animate smoothly over 0.3 seconds instead of snapping instantly.

What is a CSS transition?

A CSS transition smoothly animates a property from one value to another when that property changes (for example, on hover). You define the property to watch, the duration, and the timing curve. The browser handles the in-between frames automatically.

  • Save style.css and refresh Chrome.
  • Hover over any nav link (Home, Missions, About, Contact). You should see the text brighten to white, grow slightly, and glow.

Links not glowing on hover?

  • Make sure the new rule is .nav-link:hover (with a colon, no space). A missing colon or a space before :hover will break the selector.
  • Confirm you saved style.css. Unsaved changes won't appear in the browser.

Still stuck? Ask for help.

Create the pulse animation for the button

The call-to-action button should draw the visitor's attention even when they are not hovering over it. A pulsing glow creates a subtle "look here" signal without being distracting.

  • In style.css, find the .cta-button rule.
  • Find this line:
    animation: fadeSlideIn 1s ease-out 0.6s forwards;
  • Replace that line with this updated version that adds a second animation after a comma:
    animation: fadeSlideIn 1s ease-out 0.6s forwards, pulse 2s ease-in-out 1.6s infinite;

Why two animations on one property?

CSS lets you stack multiple animations on a single element by separating them with commas. The first animation (fadeSlideIn) runs once when the page loads. The second (pulse) starts after a 1.6-second delay and loops forever.

The 1.6s delay on pulse ensures the glow effect starts only after the entrance animation finishes. This prevents the two animations from visually clashing.

Now you need the hover state and the pulse keyframes themselves.

  • Add the following code directly below the closing } of the .cta-button rule:
.cta-button:hover {
    transform: scale(1.05);
    transition: transform 0.3s ease;
}

@keyframes pulse {
    0% {
        box-shadow: 0 0 0 0 rgba(37, 117, 252, 0.5);
    }
    50% {
        box-shadow: 0 0 20px 10px rgba(37, 117, 252, 0);
    }
    100% {
        box-shadow: 0 0 0 0 rgba(37, 117, 252, 0);
    }
}

What does this code do?

The .cta-button:hover rule makes the button grow slightly (to 105% scale) when hovered, with a smooth 0.3-second transition.

The @keyframes pulse animation uses box-shadow to create the glow. At 0% the shadow starts as a solid blue ring. At 50% it expands outward and fades to transparent. At 100% it resets. Because the animation is set to infinite, this cycle repeats endlessly.

  • Save style.css.
  • Refresh Chrome. After the button fades in (about 1.6 seconds), you should see a blue glow pulsing outward from the button repeatedly.

Button not pulsing?

  • Check that the comma between the two animations is present: forwards, pulse. A missing comma will break both animations.
  • Make sure the keyframe name pulse in your @keyframes declaration exactly matches the name in the animation property. CSS animation names are case-sensitive.
  • Wait at least 2 seconds after the page loads. The pulse has a 1.6s delay so it starts only after the entrance animation completes.

Need more help debugging?

Test all your animations in Chrome

Your page now has every animation layer in place. Time to verify they all work together smoothly.

  • Hard-refresh Chrome by pressing Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear any cached CSS.
  • Watch the page load sequence: stars should twinkle, the rocket should float, and the heading, subtitle, and button should fade in one after another.
  • After about 2 seconds, confirm the button starts pulsing with a blue glow.
  • Hover over each nav link. Each one should brighten, grow slightly, and glow.
  • Hover over the Launch Mission button. It should scale up slightly.

What should I see?

Your page now has five distinct animation layers running together: twinkling stars, a floating rocket, staggered entrance animations, nav link hover transitions, and a pulsing button glow. Each layer uses a different CSS animation technique.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the full style.css file as it should look at the end of this step:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d1a 50%, #000000 100%);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: #ffffff;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* Stars */
.star {
    position: absolute;
    width: 3px;
    height: 3px;
    background: #ffffff;
    border-radius: 50%;
    animation: twinkle 2s ease-in-out infinite alternate;
}

.star:nth-child(2) {
    animation-delay: 0.3s;
    width: 2px;
    height: 2px;
}

.star:nth-child(3) {
    animation-delay: 0.7s;
}

.star:nth-child(4) {
    animation-delay: 1.1s;
    width: 2px;
    height: 2px;
}

.star:nth-child(5) {
    animation-delay: 0.5s;
}

.star:nth-child(6) {
    animation-delay: 1.4s;
    width: 2px;
    height: 2px;
}

.star:nth-child(7) {
    animation-delay: 0.9s;
}

.star:nth-child(8) {
    animation-delay: 1.7s;
    width: 2px;
    height: 2px;
}

@keyframes twinkle {
    from {
        opacity: 0.3;
    }
    to {
        opacity: 1;
    }
}

/* Navigation */
.main-nav {
    padding: 2rem;
    display: flex;
    gap: 2rem;
    z-index: 10;
}

.nav-link {
    color: #aaaacc;
    text-decoration: none;
    font-size: 1rem;
    transition: color 0.3s ease, transform 0.3s ease;
}

.nav-link:hover {
    color: #ffffff;
    transform: scale(1.1);
    text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
}

/* Hero Section */
.hero {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    z-index: 10;
}

.rocket {
    font-size: 4rem;
    animation: float 3s ease-in-out infinite alternate;
    margin-bottom: 1rem;
}

@keyframes float {
    from {
        transform: translateY(-10px) rotate(-3deg);
    }
    to {
        transform: translateY(10px) rotate(3deg);
    }
}

.hero-title {
    font-size: 4rem;
    font-weight: 700;
    letter-spacing: 2px;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out forwards;
}

.hero-subtitle {
    font-size: 1.3rem;
    color: #ccccee;
    margin-top: 1rem;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.3s forwards;
}

.cta-button {
    margin-top: 2rem;
    padding: 1rem 2.5rem;
    font-size: 1.1rem;
    font-weight: 600;
    color: #ffffff;
    background: linear-gradient(135deg, #6a11cb, #2575fc);
    border: none;
    border-radius: 50px;
    cursor: pointer;
    opacity: 0;
    animation: fadeSlideIn 1s ease-out 0.6s forwards, pulse 2s ease-in-out 1.6s infinite;
}

.cta-button:hover {
    transform: scale(1.05);
    transition: transform 0.3s ease;
}

@keyframes fadeSlideIn {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes pulse {
    0% {
        box-shadow: 0 0 0 0 rgba(37, 117, 252, 0.5);
    }
    50% {
        box-shadow: 0 0 20px 10px rgba(37, 117, 252, 0);
    }
    100% {
        box-shadow: 0 0 0 0 rgba(37, 117, 252, 0);
    }
}

Your landing page now responds to the visitor. Every element has motion, and hovering triggers smooth visual feedback. Next up, you will tackle the Secret Mission and add a shooting star streaking across the sky.

Secret mission

Add a Shooting Star

Your space scene has twinkling stars, a floating rocket, and smooth entrance animations. But real night skies have something more dramatic. Can you add a shooting star that streaks across the page using multi-step percentage keyframes and animation-delay?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Your files sit on your Desktop and only use a few kilobytes of storage.

Decide whether to keep your project files to revisit later, or delete them entirely.

Resources you used:

  • The nova-voyages folder on your Desktop (contains index.html and style.css).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your animations or use this page as a reference for future projects.

  • Your nova-voyages folder stays on your Desktop.
  • You can reopen index.html in Chrome anytime to see your animated page.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files are already sitting idle on your Desktop.

  • Close the Chrome tab showing your page.
  • Close the nova-voyages folder in VS Code.
  • Your files remain on your Desktop whenever you want to come back.

Delete - I don't want to use this again

Remove the project folder entirely to free up space and start fresh.

  • Close the Chrome tab showing your page.
  • Close the nova-voyages folder in VS Code.
  • Delete the folder by running one of these commands in your terminal:

macOS

rm -rf ~/Desktop/nova-voyages

Windows

rmdir /s /q %USERPROFILE%\Desktop\nova-voyages
  • Confirm the folder is gone by checking your Desktop.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully animated space landing page from scratch using nothing but HTML and CSS.

You've learned how to:

  • Built a space-themed landing page with twinkling star animations using @keyframes and a dark radial gradient background.
  • Animated page content on load with staggered entrance animations using animation-delay, ease-out timing, and animation-fill-mode: forwards to keep elements visible after animating in.
  • Created a continuously floating rocket with infinite and alternate animation direction, plus smooth hover transitions on navigation links and a pulsing glow effect on the call-to-action button.
  • Added shooting stars that streak across the page using multi-step percentage keyframes and transform: translate().

Ready to quiz yourself?