How can I create a CSS fade-in effect on page load for a paragraph?

I’m trying to apply a CSS fade-in effect to a text paragraph when the page loads. I have the following markup:

<div id="test">
  <p>This is a test</p>
</div>

And here’s the CSS I’m using:

#test p {
  opacity: 0;
  margin-top: 25px;
  font-size: 21px;
  text-align: center;
  transition: opacity 2s ease-in;
}

The transition is defined, but it doesn’t automatically trigger when the page loads.

What’s the proper way to make this fade-in effect happen as soon as the page loads, without using JavaScript if possible?

Should I be toggling a class or using a body.loaded type of approach with CSS animations instead? Looking for the cleanest solution using pure CSS, if achievable.

Absolutely, the cleanest way to handle a CSS fade in on page load without JavaScript is by using @keyframes. You can animate the opacity and even adjust the delay or duration as needed:

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
#test p {
  opacity: 0;
  animation: fadeIn 2s ease-in forwards;
}

This will trigger the animation as soon as the element renders. I use this method a lot for hero sections or landing page intros, it’s simple, elegant, and pure CSS.

If you plan to have multiple elements fade in at different intervals, CSS animation-delay is super handy.

Here’s how you could make your CSS fade in more dynamic:

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
#test p {
  opacity: 0;
  animation: fadeIn 2s ease-in 0.5s forwards;
}

I use this approach on dashboards or content-heavy pages where I want to gently reveal text or cards in sequence. Keeps things lightweight and user-friendly.

If you’re using modern browsers that support :has() (Safari/Chrome 105+), you can simulate a CSS fade in trigger by using a container class.

Combine this with a utility class like .loaded added to the body (manually or through server-side rendering):

body:has(.loaded) #test p {
  opacity: 1;
  transition: opacity 2s ease-in;
}

Just make sure .loaded is present on initial render.

I’ve used this in JAMstack sites to simulate load states without touching JavaScript, it’s progressive enhancement done right.