Fade in and fade out with a CSS3 transition
A fade is a change of opacity spread over time. CSS transitions do exactly that without a line of JavaScript: you describe the two states, say which property should animate and for how long, and the browser draws the frames in between. The details that make a fade feel finished are making the hidden element non-interactive and respecting users who ask for less motion.
The minimal fade on hover
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hover fade</title>
<style>
.photo {
opacity: 1;
transition: opacity 0.4s ease;
}
.photo:hover,
.photo:focus {
opacity: 0.5;
}
</style>
</head>
<body>
<img class="photo" src="harbour.jpg" alt="Boats in a harbour" width="320" height="213" tabindex="0">
</body>
</html>
The transition shorthand lists the property, the duration and the timing function. It sits on the element's base rule, so the animation runs both when the hover starts and when it ends. Putting it only on :hover would animate the fade-out but snap back instantly.
The transition properties
| Longhand | Meaning | Example |
|---|---|---|
transition-property | Which property animates | opacity |
transition-duration | How long it takes | 300ms, 0.3s |
transition-timing-function | The speed curve | ease, linear, ease-in-out, cubic-bezier() |
transition-delay | Wait before starting | 0.1s |
List several transitions separated by commas: transition: opacity 0.3s ease, transform 0.3s ease;. Avoid transition: all; it animates properties you did not intend to and makes later changes harder to reason about.
Opacity and transform are the cheapest properties to animate: browsers can composite them without recalculating layout, so they stay smooth on modest devices.
Fading an element in and out with a class
For a message or panel that appears and disappears, toggle a class from script and let CSS do the rest. The important part is visibility: an element at opacity: 0 is invisible but still there, so it can cover buttons underneath, receive clicks and be read by screen readers. visibility: hidden removes it from interaction and from the accessibility tree.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fade panel</title>
<style>
.panel {
opacity: 0;
visibility: hidden;
transition: opacity 0.35s ease, visibility 0s linear 0.35s;
padding: 1rem;
border: 1px solid #999;
max-width: 24rem;
}
.panel.is-visible {
opacity: 1;
visibility: visible;
transition: opacity 0.35s ease, visibility 0s linear 0s;
}
</style>
</head>
<body>
<button type="button" id="toggle" aria-expanded="false" aria-controls="info">Show details</button>
<div class="panel" id="info">
<p>Delivery takes two to four working days.</p>
</div>
<script>
var button = document.getElementById("toggle");
var panel = document.getElementById("info");
button.addEventListener("click", function () {
var visible = panel.classList.toggle("is-visible");
button.setAttribute("aria-expanded", visible);
button.textContent = visible ? "Hide details" : "Show details";
});
</script>
</body>
</html>
How the visibility trick works: visibility is animatable in discrete steps. When hiding, its transition has a delay equal to the fade's duration, so the element stays visible while opacity drops, then becomes hidden. When showing, the delay is zero, so it is visible immediately and the opacity rises.
Why not display: none?
Historically, display could not be transitioned, so switching to display: none cut the fade off. Recent browsers support transition-behavior: allow-discrete together with @starting-style to animate to and from display: none, but support is still newer than the visibility pattern above, which works everywhere. If the hidden panel should take no space in the layout, you can combine the fade with a transitionend listener that sets hidden afterwards.
Fade in when the page loads
A transition needs a change of state; on page load there is none. Use a keyframe animation instead:
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.intro {
animation: fade-in 0.6s ease-out both;
}
.intro + .intro { animation-delay: 0.15s; }
both (the animation-fill-mode) keeps the element at opacity 0 during the delay and at 1 after the end.
Cross-fading two images
Stack two images in a container and fade the top one on hover:
<style>
.swap { position: relative; display: inline-block; }
.swap img { display: block; }
.swap .after {
position: absolute; inset: 0;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
.swap:hover .after,
.swap:focus-within .after { opacity: 1; }
</style>
<a class="swap" href="/gallery/">
<img src="before.jpg" alt="Garden in winter" width="300" height="200">
<img class="after" src="after.jpg" alt="" width="300" height="200">
</a>
The second image has an empty alt because it is decorative in this pattern; the link is described by the first image.
Respect reduced motion
Some people get dizzy or distracted by animation and ask their operating system to reduce motion. Honour it with a media query. A fade is mild, so shortening it is usually enough:
@media (prefers-reduced-motion: reduce) {
.panel, .panel.is-visible, .photo, .swap .after {
transition-duration: 0.01ms;
}
.intro { animation: none; }
}
Using it in an ASP.NET page
Server controls render ordinary HTML, so the same classes apply: <asp:Panel ID="pnlInfo" runat="server" CssClass="panel"> renders a <div class="panel">. Note that Visible="false" on the server means the panel is not rendered at all, so there is nothing to fade on the client; leave it visible on the server and control the class instead. For a message shown after a postback, add is-visible from the code-behind (pnlInfo.CssClass += " is-visible";) and combine it with the keyframe animation so it fades in on arrival.
jQuery alternative
jQuery's fadeIn(), fadeOut() and fadeToggle() animate opacity with JavaScript and set display: none at the end. They remain useful for older code; see jQuery fading effects. For new work, CSS transitions are lighter and run off the main JavaScript thread. The same technique drives the CSS3 tooltip example.