SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/fade-in-animations

7 CSS Fade In Animations

2 Pure CSS5 with JSPublished 2026-10-05Free · no sign-up

Seven fade-ins for everyday interface moments: content that arrives in sequence on page load, a native popover that fades, paragraphs that fade in as you scroll, an image that only appears once loaded, a dialog with a fading backdrop, a cross-fading tip and tab panels that fade.

01. Page Content Fades In on Load

FadeLoad

Header, text and button fade in one after another when the page first paints, a gentle start that guides the eye. Each element carries an index for its delay, and the fade starts from a nearly finished state so slow devices never show blank space for long.

View code
<section class="fi1"><p class="k" style="--i:0">New season</p><h3 style="--i:1">Light layers for warm days</h3><a href="#shop" style="--i:2">Shop the edit</a></section>
.fi1 { display: grid; justify-items: start; gap: 8px; width: 280px; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.fi1 > * { animation: fi1 .7s ease both; animation-delay: calc(var(--i) * 120ms); }
.fi1 .k { margin: 0; color: #b54708; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
.fi1 h3 { margin: 0; color: #101828; font-size: 26px; line-height: 1.2; }
.fi1 a { margin-top: 6px; padding: 10px 16px; border-radius: 8px; background: #101828; color: #fff; font-weight: 700; font-size: 14px; text-decoration: none; }
.fi1 a:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@keyframes fi1 { from { opacity: 0; transform: translateY(8px); } }
@media (prefers-reduced-motion: reduce) { .fi1 > * { animation: none; } }

02. Popover Fade with @starting-style

FadePopover

A native popover that fades and scales in when opened and fades out when closed. The popover attribute gives light dismiss and Escape for free, @starting-style supplies the opening frame, and allow-discrete keeps it visible until the fade-out finishes.

View code
<div class="fi2"><button popovertarget="fi2p">Account</button><div id="fi2p" popover><b>Ana Ribeiro</b><a href="#settings">Settings</a><a href="#signout">Sign out</a></div></div>
.fi2 { font-family: Inter, system-ui, sans-serif; }
.fi2 button { padding: 10px 16px; border: 1px solid #3a4670; border-radius: 8px; background: #1b2238; color: #e2e8f0; font: 600 14px Inter, system-ui, sans-serif; cursor: pointer; }
.fi2 [popover] { inset: auto; top: 90px; left: 50%; translate: -50% 0; display: none; flex-direction: column; gap: 4px; min-width: 180px; margin: 0; padding: 10px; border: 1px solid #2a3350; border-radius: 12px; background: #151b2e; color: #e2e8f0; font-size: 14px; opacity: 0; transform: scale(.96); transition: opacity .2s, transform .2s, overlay .2s allow-discrete, display .2s allow-discrete; }
.fi2 [popover]:popover-open { display: flex; opacity: 1; transform: none; }
@starting-style { .fi2 [popover]:popover-open { opacity: 0; transform: scale(.96); } }
.fi2 b { padding: 4px 6px 8px; border-bottom: 1px solid #2a3350; }
.fi2 a { padding: 8px 6px; border-radius: 6px; color: #cbd5e1; text-decoration: none; } .fi2 a:hover { background: #1e2640; }
.fi2 :focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .fi2 [popover] { transition: none; } }

03. Fade In on Scroll, Once

FadeScroll

Paragraphs fade in as they enter the scrolling panel and then stay. It is the plain fade without movement, which is the safest scroll effect for motion-sensitive readers, and it still switches off under reduced motion.

View code
<div class="fi3" tabindex="0" role="region" aria-label="Article, scroll"><p class="sp">Scroll ↓</p><p class="f">The kiln reaches 1,240 degrees on a long firing.</p><p class="f">Glazes shift colour as the temperature climbs.</p><p class="f">Every bowl comes out a little different.</p></div>
.fi3 { width: 260px; max-width: 100%; height: 260px; overflow-y: auto; padding: 16px; box-sizing: border-box; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font: 16px/1.6 Georgia, serif; color: #344054; }
.fi3:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.fi3 .sp { height: 200px; margin: 0; color: #475467; font: 14px Inter, system-ui, sans-serif; }
.fi3 .f { margin: 0 0 140px; }
.fi3.js .f { opacity: 0; transition: opacity .8s ease; } .fi3.js .f.in { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .fi3.js .f { opacity: 1; transition: none; } }
var box = document.querySelector('.fi3'); box.classList.add('js');
var io = new IntersectionObserver(function (es) { es.forEach(function (e) { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { root: box, threshold: .5 });
box.querySelectorAll('.f').forEach(function (p) { io.observe(p); });

04. Image Fades In When Loaded

FadeImages

An image that fades in only once it has actually loaded, so people never see it paint in strips. The img starts transparent only after script runs, the load event adds a class, and an already-cached image is handled by checking complete.

View code
<figure class="fi4"><img width="240" height="160" alt="A soft gradient standing in for a photo" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 160'%3E%3Cdefs%3E%3ClinearGradient id='g' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%2306b6d4'/%3E%3Cstop offset='1' stop-color='%237c3aed'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='240' height='160' fill='url(%23g)'/%3E%3C/svg%3E"><figcaption>Fades in on load</figcaption></figure>
.fi4 { width: 240px; max-width: 100%; margin: 0; font: 13px Inter, system-ui, sans-serif; color: #94a3b8; }
.fi4 img { display: block; width: 100%; height: auto; border-radius: 14px; background: #1e293b; }
.fi4.js img { opacity: 0; transition: opacity .8s ease; } .fi4.js img.ok { opacity: 1; }
.fi4 figcaption { margin-top: 6px; }
@media (prefers-reduced-motion: reduce) { .fi4.js img { opacity: 1; transition: none; } }
var f = document.querySelector('.fi4'), img = f.querySelector('img'); f.classList.add('js');
function done() { img.classList.add('ok'); }
if (img.complete) requestAnimationFrame(done); else img.addEventListener('load', done);

05. Dialog with Fading Backdrop

FadeDialog

A confirmation dialog whose box and dimmed backdrop both fade in together. The native dialog element handles focus and Escape; the ::backdrop pseudo-element gets its own transition so the page darkens smoothly rather than snapping.

View code
<div class="fi5"><button class="open">Delete project</button><dialog aria-labelledby="fi5h"><h3 id="fi5h">Delete this project?</h3><p>This removes 14 files. It cannot be undone.</p><div><button class="no">Cancel</button><button class="yes">Delete</button></div></dialog></div>
.fi5 { font-family: Inter, system-ui, sans-serif; }
.fi5 .open { padding: 10px 16px; border: 1px solid #fda29b; border-radius: 8px; background: #fff; color: #b42318; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.fi5 dialog { width: min(280px, 90vw); padding: 20px; border: 0; border-radius: 16px; color: #101828; opacity: 0; transform: translateY(8px); transition: opacity .25s, transform .25s, overlay .25s allow-discrete, display .25s allow-discrete; }
.fi5 dialog[open] { opacity: 1; transform: none; }
@starting-style { .fi5 dialog[open] { opacity: 0; transform: translateY(8px); } }
.fi5 dialog::backdrop { background: rgba(16,24,40,0); transition: background .25s, overlay .25s allow-discrete, display .25s allow-discrete; }
.fi5 dialog[open]::backdrop { background: rgba(16,24,40,.5); }
@starting-style { .fi5 dialog[open]::backdrop { background: rgba(16,24,40,0); } }
.fi5 h3 { margin: 0 0 6px; } .fi5 p { margin: 0 0 16px; color: #475467; font-size: 14px; }
.fi5 dialog div { display: flex; justify-content: flex-end; gap: 8px; }
.fi5 dialog button { padding: 9px 14px; border-radius: 8px; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.fi5 .no { border: 1px solid #d0d5dd; background: #fff; color: #344054; } .fi5 .yes { border: 0; background: #b42318; color: #fff; }
.fi5 button:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .fi5 dialog, .fi5 dialog::backdrop { transition: none; } }
var d = document.querySelector('.fi5 dialog');
document.querySelector('.fi5 .open').onclick = function () { d.showModal(); };
d.querySelectorAll('button').forEach(function (b) { b.onclick = function () { d.close(); }; });

06. Cross-Fading Quote Swap

FadeContent

Pressing Next fades the current tip out and the next one in, rather than replacing the text abruptly. The text is swapped at the midpoint of a two-part transition, and the paragraph is a live region so each new tip is announced.

View code
<div class="fi6"><p aria-live="polite">Name your colours by role, not by hue.</p><button>Next tip</button></div>
.fi6 { display: grid; gap: 12px; width: 260px; max-width: 100%; padding: 18px; box-sizing: border-box; border-radius: 14px; background: #151b2e; font-family: Inter, system-ui, sans-serif; }
.fi6 p { min-height: 2.8em; margin: 0; color: #e2e8f0; font-size: 16px; font-weight: 600; line-height: 1.4; transition: opacity .25s; }
.fi6 p.hide { opacity: 0; }
.fi6 button { justify-self: start; padding: 8px 14px; border: 1px solid #3a4670; border-radius: 8px; background: transparent; color: #c7d2fe; font: 600 13px Inter, system-ui, sans-serif; cursor: pointer; }
.fi6 button:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .fi6 p { transition: none; } }
var p = document.querySelector('.fi6 p'), tips = ['Name your colours by role, not by hue.', 'Space with a scale, not ad-hoc pixels.', 'Test focus states before shipping.'], k = 0;
document.querySelector('.fi6 button').onclick = function () { p.classList.add('hide'); setTimeout(function () { k = (k + 1) % tips.length; p.textContent = tips[k]; p.classList.remove('hide'); }, 250); };

07. Tab Panels That Fade

FadeTabs

Switching tabs fades the new panel in instead of snapping, using a keyframe animation on the panel that becomes visible. The tabs follow the ARIA tabs pattern with roving tabindex and arrow keys, so the fade sits on a fully accessible component.

View code
<div class="fi7"><div role="tablist" aria-label="Plan details"><button role="tab" id="fi7a" aria-selected="true" aria-controls="fi7pa">Features</button><button role="tab" id="fi7b" aria-selected="false" aria-controls="fi7pb" tabindex="-1">Limits</button></div><div role="tabpanel" id="fi7pa" aria-labelledby="fi7a">Unlimited pages, custom domain, analytics.</div><div role="tabpanel" id="fi7pb" aria-labelledby="fi7b" hidden>Up to 5 editors and 50 GB of storage.</div></div>
.fi7 { width: 280px; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.fi7 [role=tablist] { display: flex; gap: 4px; border-bottom: 1px solid #e4e7ec; }
.fi7 [role=tab] { padding: 10px 14px; border: 0; border-bottom: 2px solid transparent; background: none; color: #475467; font: 600 14px Inter, system-ui, sans-serif; cursor: pointer; }
.fi7 [aria-selected=true] { border-bottom-color: #7f56d9; color: #53389e; }
.fi7 [role=tabpanel] { padding: 14px 2px; color: #344054; font-size: 14px; animation: fi7 .35s ease both; }
.fi7 :focus-visible { outline: 3px solid #d6bbfb; outline-offset: 2px; }
@keyframes fi7 { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .fi7 [role=tabpanel] { animation: none; } }
var tabs = document.querySelectorAll('.fi7 [role=tab]');
function sel(t) { tabs.forEach(function (x) { var on = x === t; x.setAttribute('aria-selected', on); x.tabIndex = on ? 0 : -1; document.getElementById(x.getAttribute('aria-controls')).hidden = !on; }); t.focus(); }
tabs.forEach(function (t, i) { t.onclick = function () { sel(t); }; t.onkeydown = function (e) { if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault(); sel(tabs[(i + (e.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length]); } }; });

About these CSS Fade In Animations

Fade, the gentlest motion

A fade changes only opacity, with no movement, which makes it the safest animation for people sensitive to motion. It still switches off under prefers-reduced-motion in these demos, but it is a good default.

Fading things in from display: none

Elements that appear, such as popovers and dialogs, need @starting-style for their first frame and allow-discrete on display and overlay so they stay visible while fading out.

Wait for images to load

Fading in an image before it has loaded just fades in an empty box. Listen for the load event, and check img.complete for images already in the cache.

Never trap content at zero opacity

Set the hidden starting state from script or a supported feature check. If the page loads without JavaScript, everything should simply be visible.

Frequently asked questions

What duration should a fade use?

Around 200 to 400ms for interface elements, up to 800ms for large images.

Do popovers need JavaScript?

No. popovertarget opens and closes them, including light dismiss and Escape.

Does fading hurt Largest Contentful Paint?

Fading the main hero image delays when it counts as painted; fade secondary content instead.