30 CSS Notifications15 / 30
Sticky Top Notification Banner CSS
Two sticky-banner behaviors over real scrolling content: a dismissible announcement bar whose position:sticky anatomy is visible live (scroll and watch it hold the top edge) and whose dismissal collapses smoothly instead of yanking the page up — and a zero-JS scroll-driven banner that condenses from a tall promo into a slim strip as you scroll, powered by animation-timeline: scroll().
Published
The code
<div class="ntf-15-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-15a" aria-label="Sticky announcement banner over scrolling content">
<div class="ntf-15a__page" id="ntf-15a-page">
<div class="ntf-15a__banner" id="ntf-15a-banner" role="region" aria-label="Announcement">
<div class="ntf-15a__clip">
<div class="ntf-15a__inner">
<span class="ntf-15a__tag" aria-hidden="true">NEW</span>
<p class="ntf-15a__msg"><b>Snippets 2.0 is live</b> — 30 fresh notification patterns, all copy-paste ready.</p>
<a class="ntf-15a__cta" href="#" onclick="return false">Browse</a>
<button class="ntf-15a__x" id="ntf-15a-x" type="button" aria-label="Dismiss announcement">✕</button>
</div>
</div>
</div>
<header class="ntf-15a__hero"><h2>The banner above is <em>sticky</em></h2><p>Scroll this panel — it holds the top edge. Dismiss it — the page glides, never jumps.</p></header>
<article class="ntf-15a__prose"><h3>Why sticky beats fixed</h3><p>A fixed banner floats over the document, so the page needs manual padding to avoid hiding content beneath it. A sticky banner occupies real layout space and only pins once its edge reaches the viewport.</p><h3>Dismissal without the yank</h3><p>Removing a 56px banner node scrolls everything up 56 pixels instantly. Collapsing grid-template-rows from 1fr to 0fr animates the same change over 400ms, and the reading position eases instead of teleporting.</p><h3>Filler paragraph</h3><p>Keep scrolling to feel the pin. The banner belongs to this scroll container, so it pins to the panel's top rather than the page's.</p><p>Sticky elements need every ancestor to keep visible overflow — one stray overflow:hidden and the pin silently dies.</p></article>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400..800&display=swap" rel="stylesheet">
<section class="ntf-15b" aria-label="Banner that condenses as you scroll, no JavaScript">
<div class="ntf-15b__page">
<div class="ntf-15b__banner" role="region" aria-label="Promotion">
<p class="ntf-15b__big" aria-hidden="true">EARLY ACCESS · <b>40% OFF</b> annual plans this week</p>
<p class="ntf-15b__small">40% off — <b>claim</b></p>
</div>
<article class="ntf-15b__prose">
<h2>Scroll me</h2>
<p>The banner's padding, size and even its message are keyframes mapped onto scroll position with <b>animation-timeline: scroll()</b>. No listeners, no throttling, runs off the main thread.</p>
<p>Scroll back up and it re-expands — the timeline plays in both directions because it IS the scrollbar.</p>
<p>The full promo line cross-fades into a compact one at the 60% mark of the 180px range; both live in the DOM, so screen readers always get the complete message.</p>
<p>Support: Chrome 115+, Edge 115+, Safari 26. The @supports guard keeps older browsers on the expanded banner permanently — nothing breaks, it just doesn't condense.</p>
<p>Filler to make the panel scroll. The pattern shines on marketing sites where the promo must stay present but shouldn't keep shouting once the visitor commits to reading.</p>
</article>
</div>
</section>
</div><div class="ntf-15-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-15a" aria-label="Sticky announcement banner over scrolling content">
<div class="ntf-15a__page" id="ntf-15a-page">
<div class="ntf-15a__banner" id="ntf-15a-banner" role="region" aria-label="Announcement">
<div class="ntf-15a__clip">
<div class="ntf-15a__inner">
<span class="ntf-15a__tag" aria-hidden="true">NEW</span>
<p class="ntf-15a__msg"><b>Snippets 2.0 is live</b> — 30 fresh notification patterns, all copy-paste ready.</p>
<a class="ntf-15a__cta" href="#" onclick="return false">Browse</a>
<button class="ntf-15a__x" id="ntf-15a-x" type="button" aria-label="Dismiss announcement">✕</button>
</div>
</div>
</div>
<header class="ntf-15a__hero"><h2>The banner above is <em>sticky</em></h2><p>Scroll this panel — it holds the top edge. Dismiss it — the page glides, never jumps.</p></header>
<article class="ntf-15a__prose"><h3>Why sticky beats fixed</h3><p>A fixed banner floats over the document, so the page needs manual padding to avoid hiding content beneath it. A sticky banner occupies real layout space and only pins once its edge reaches the viewport.</p><h3>Dismissal without the yank</h3><p>Removing a 56px banner node scrolls everything up 56 pixels instantly. Collapsing grid-template-rows from 1fr to 0fr animates the same change over 400ms, and the reading position eases instead of teleporting.</p><h3>Filler paragraph</h3><p>Keep scrolling to feel the pin. The banner belongs to this scroll container, so it pins to the panel's top rather than the page's.</p><p>Sticky elements need every ancestor to keep visible overflow — one stray overflow:hidden and the pin silently dies.</p></article>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400..800&display=swap" rel="stylesheet">
<section class="ntf-15b" aria-label="Banner that condenses as you scroll, no JavaScript">
<div class="ntf-15b__page">
<div class="ntf-15b__banner" role="region" aria-label="Promotion">
<p class="ntf-15b__big" aria-hidden="true">EARLY ACCESS · <b>40% OFF</b> annual plans this week</p>
<p class="ntf-15b__small">40% off — <b>claim</b></p>
</div>
<article class="ntf-15b__prose">
<h2>Scroll me</h2>
<p>The banner's padding, size and even its message are keyframes mapped onto scroll position with <b>animation-timeline: scroll()</b>. No listeners, no throttling, runs off the main thread.</p>
<p>Scroll back up and it re-expands — the timeline plays in both directions because it IS the scrollbar.</p>
<p>The full promo line cross-fades into a compact one at the 60% mark of the 180px range; both live in the DOM, so screen readers always get the complete message.</p>
<p>Support: Chrome 115+, Edge 115+, Safari 26. The @supports guard keeps older browsers on the expanded banner permanently — nothing breaks, it just doesn't condense.</p>
<p>Filler to make the panel scroll. The pattern shines on marketing sites where the promo must stay present but shouldn't keep shouting once the visitor commits to reading.</p>
</article>
</div>
</section>
</div>.ntf-15-group {
display: flex;
flex-direction: column;
width: 100%;
}
.ntf-15-group > section {
width: 100%;
}
.ntf-15a,
.ntf-15a *,
.ntf-15a *::before,
.ntf-15a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15a {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: radial-gradient(120% 100% at 50% 0%,#151a26,#0b0e15);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.ntf-15a__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #f5f6f9;
border: 1px solid rgba(165,190,230,.2);
box-shadow: 0 34px 80px -40px rgba(0,0,0,.9);
}
.ntf-15a__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .4s cubic-bezier(.4,0,.2,1),opacity .3s ease;
}
.ntf-15a__banner.is-dismissed {
grid-template-rows: 0fr;
opacity: 0;
}
.ntf-15a__clip {
overflow: hidden;
}
.ntf-15a__inner {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
background: linear-gradient(90deg,oklch(0.45 0.16 280),oklch(0.5 0.17 250));
color: #fff;
}
.ntf-15a__tag {
flex: none;
font-size: 10px;
font-weight: 700;
letter-spacing: .1em;
padding: 4px 8px;
border-radius: 6px;
background: rgb(255 255 255 / .18);
border: 1px solid rgb(255 255 255 / .3);
}
.ntf-15a__msg {
font-size: 13.5px;
line-height: 1.4;
min-width: 0;
}
.ntf-15a__msg b {
font-weight: 700;
}
.ntf-15a__cta {
margin-left: auto;
flex: none;
font-size: 12.5px;
font-weight: 700;
color: oklch(0.35 0.12 270);
background: #fff;
text-decoration: none;
padding: 7px 14px;
border-radius: 8px;
transition: scale .12s;
}
.ntf-15a__cta:hover {
scale: 1.04;
}
.ntf-15a__x {
flex: none;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
font-size: 12px;
color: rgb(255 255 255 / .8);
background: transparent;
cursor: pointer;
transition: background .15s;
}
.ntf-15a__x:hover {
background: rgb(255 255 255 / .15);
}
.ntf-15a__hero {
padding: 38px 30px 10px;
color: #1b1f2a;
}
.ntf-15a__hero h2 {
font-size: 26px;
font-weight: 600;
letter-spacing: -.01em;
}
.ntf-15a__hero h2 em {
font-style: normal;
color: oklch(0.5 0.17 260);
}
.ntf-15a__hero p {
margin-top: 8px;
font-size: 14px;
color: rgba(27,31,42,.6);
}
.ntf-15a__prose {
padding: 10px 30px 44px;
color: rgba(27,31,42,.75);
}
.ntf-15a__prose h3 {
margin-top: 26px;
font-size: 16px;
font-weight: 600;
color: #1b1f2a;
}
.ntf-15a__prose p {
margin-top: 8px;
font-size: 13.5px;
line-height: 1.75;
text-wrap: pretty;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15a__banner {
transition-duration: .01s;
}
}
.ntf-15b,
.ntf-15b *,
.ntf-15b *::before,
.ntf-15b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,#f6f2e9,#ece4d2);
font-family: 'Outfit','Segoe UI',system-ui,sans-serif;
}
.ntf-15b__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #fffdf8;
border: 1px solid rgba(35,31,24,.12);
box-shadow: 0 30px 70px -38px rgba(35,31,24,.55);
}
.ntf-15b__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
place-items: center;
background: #211d16;
color: #fdfaf2;
padding: 26px 20px;
}
.ntf-15b__big {
font-size: clamp(16px,2.6vw,21px);
font-weight: 500;
letter-spacing: .01em;
}
.ntf-15b__big b {
font-weight: 800;
color: oklch(0.85 0.14 85);
}
.ntf-15b__small {
position: absolute;
font-size: 13px;
font-weight: 600;
opacity: 0;
}
.ntf-15b__small b {
color: oklch(0.85 0.14 85);
}
@supports (animation-timeline: scroll()) {
.ntf-15b__banner {
animation: ntf-15b-shrink linear both;
animation-timeline: scroll();
animation-range: 0 180px;
}
.ntf-15b__big {
animation: ntf-15b-hide linear both;
animation-timeline: scroll();
animation-range: 0 140px;
}
.ntf-15b__small {
animation: ntf-15b-show linear both;
animation-timeline: scroll();
animation-range: 100px 180px;
}
@keyframes ntf-15b-shrink {
to {
padding: 9px 20px;
background: #181510;
}
}
@keyframes ntf-15b-hide {
to {
opacity: 0;
scale: .92;
}
}
@keyframes ntf-15b-show {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
}
.ntf-15b__prose {
padding: 34px 30px 46px;
color: rgba(35,31,24,.75);
}
.ntf-15b__prose h2 {
font-size: 24px;
font-weight: 700;
letter-spacing: -.01em;
color: #211d16;
}
.ntf-15b__prose p {
margin-top: 14px;
font-size: 14px;
line-height: 1.8;
text-wrap: pretty;
}
.ntf-15b__prose b {
color: #211d16;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15b__banner,
.ntf-15b__big,
.ntf-15b__small {
animation: none;
}
.ntf-15b__small {
position: static;
opacity: 0;
display: none;
}
}.ntf-15-group {
display: flex;
flex-direction: column;
width: 100%;
}
.ntf-15-group > section {
width: 100%;
}
.ntf-15a,
.ntf-15a *,
.ntf-15a *::before,
.ntf-15a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15a {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: radial-gradient(120% 100% at 50% 0%,#151a26,#0b0e15);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.ntf-15a__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #f5f6f9;
border: 1px solid rgba(165,190,230,.2);
box-shadow: 0 34px 80px -40px rgba(0,0,0,.9);
}
.ntf-15a__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .4s cubic-bezier(.4,0,.2,1),opacity .3s ease;
}
.ntf-15a__banner.is-dismissed {
grid-template-rows: 0fr;
opacity: 0;
}
.ntf-15a__clip {
overflow: hidden;
}
.ntf-15a__inner {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
background: linear-gradient(90deg,oklch(0.45 0.16 280),oklch(0.5 0.17 250));
color: #fff;
}
.ntf-15a__tag {
flex: none;
font-size: 10px;
font-weight: 700;
letter-spacing: .1em;
padding: 4px 8px;
border-radius: 6px;
background: rgb(255 255 255 / .18);
border: 1px solid rgb(255 255 255 / .3);
}
.ntf-15a__msg {
font-size: 13.5px;
line-height: 1.4;
min-width: 0;
}
.ntf-15a__msg b {
font-weight: 700;
}
.ntf-15a__cta {
margin-left: auto;
flex: none;
font-size: 12.5px;
font-weight: 700;
color: oklch(0.35 0.12 270);
background: #fff;
text-decoration: none;
padding: 7px 14px;
border-radius: 8px;
transition: scale .12s;
}
.ntf-15a__cta:hover {
scale: 1.04;
}
.ntf-15a__x {
flex: none;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
font-size: 12px;
color: rgb(255 255 255 / .8);
background: transparent;
cursor: pointer;
transition: background .15s;
}
.ntf-15a__x:hover {
background: rgb(255 255 255 / .15);
}
.ntf-15a__hero {
padding: 38px 30px 10px;
color: #1b1f2a;
}
.ntf-15a__hero h2 {
font-size: 26px;
font-weight: 600;
letter-spacing: -.01em;
}
.ntf-15a__hero h2 em {
font-style: normal;
color: oklch(0.5 0.17 260);
}
.ntf-15a__hero p {
margin-top: 8px;
font-size: 14px;
color: rgba(27,31,42,.6);
}
.ntf-15a__prose {
padding: 10px 30px 44px;
color: rgba(27,31,42,.75);
}
.ntf-15a__prose h3 {
margin-top: 26px;
font-size: 16px;
font-weight: 600;
color: #1b1f2a;
}
.ntf-15a__prose p {
margin-top: 8px;
font-size: 13.5px;
line-height: 1.75;
text-wrap: pretty;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15a__banner {
transition-duration: .01s;
}
}
.ntf-15b,
.ntf-15b *,
.ntf-15b *::before,
.ntf-15b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,#f6f2e9,#ece4d2);
font-family: 'Outfit','Segoe UI',system-ui,sans-serif;
}
.ntf-15b__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #fffdf8;
border: 1px solid rgba(35,31,24,.12);
box-shadow: 0 30px 70px -38px rgba(35,31,24,.55);
}
.ntf-15b__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
place-items: center;
background: #211d16;
color: #fdfaf2;
padding: 26px 20px;
}
.ntf-15b__big {
font-size: clamp(16px,2.6vw,21px);
font-weight: 500;
letter-spacing: .01em;
}
.ntf-15b__big b {
font-weight: 800;
color: oklch(0.85 0.14 85);
}
.ntf-15b__small {
position: absolute;
font-size: 13px;
font-weight: 600;
opacity: 0;
}
.ntf-15b__small b {
color: oklch(0.85 0.14 85);
}
@supports (animation-timeline: scroll()) {
.ntf-15b__banner {
animation: ntf-15b-shrink linear both;
animation-timeline: scroll();
animation-range: 0 180px;
}
.ntf-15b__big {
animation: ntf-15b-hide linear both;
animation-timeline: scroll();
animation-range: 0 140px;
}
.ntf-15b__small {
animation: ntf-15b-show linear both;
animation-timeline: scroll();
animation-range: 100px 180px;
}
@keyframes ntf-15b-shrink {
to {
padding: 9px 20px;
background: #181510;
}
}
@keyframes ntf-15b-hide {
to {
opacity: 0;
scale: .92;
}
}
@keyframes ntf-15b-show {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
}
.ntf-15b__prose {
padding: 34px 30px 46px;
color: rgba(35,31,24,.75);
}
.ntf-15b__prose h2 {
font-size: 24px;
font-weight: 700;
letter-spacing: -.01em;
color: #211d16;
}
.ntf-15b__prose p {
margin-top: 14px;
font-size: 14px;
line-height: 1.8;
text-wrap: pretty;
}
.ntf-15b__prose b {
color: #211d16;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15b__banner,
.ntf-15b__big,
.ntf-15b__small {
animation: none;
}
.ntf-15b__small {
position: static;
opacity: 0;
display: none;
}
}(function(){
var banner=document.getElementById('ntf-15a-banner'); if(!banner) return;
document.getElementById('ntf-15a-x').addEventListener('click',function(){
banner.classList.add('is-dismissed');
banner.addEventListener('transitionend',function(e){
if(e.propertyName==='grid-template-rows') banner.remove();
/* In production: localStorage.setItem('banner-snippets-2', 'dismissed') before removing. */
});
});
})();
/* No JavaScript — animation-timeline:scroll() binds three keyframe sets to the panel's scroll position (banner shrinks, long message fades out, short one fades in); scrolling up plays everything backwards for free. */(function(){
var banner=document.getElementById('ntf-15a-banner'); if(!banner) return;
document.getElementById('ntf-15a-x').addEventListener('click',function(){
banner.classList.add('is-dismissed');
banner.addEventListener('transitionend',function(e){
if(e.propertyName==='grid-template-rows') banner.remove();
/* In production: localStorage.setItem('banner-snippets-2', 'dismissed') before removing. */
});
});
})();
/* No JavaScript — animation-timeline:scroll() binds three keyframe sets to the panel's scroll position (banner shrinks, long message fades out, short one fades in); scrolling up plays everything backwards for free. */Here's a working CSS Notification from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Sticky Top Notification Banner CSS
Source: https://codefronts.com/snippets/css-notifications/sticky-top-notification-banner-css/
Two sticky-banner behaviors over real scrolling content: a dismissible announcement bar whose position:sticky anatomy is visible live (scroll and watch it hold the top edge) and whose dismissal collapses smoothly instead of yanking the page up — and a zero-JS scroll-driven banner that condenses from a tall promo into a slim strip as you scroll, powered by animation-timeline: scroll().
## HTML
```html
<div class="ntf-15-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-15a" aria-label="Sticky announcement banner over scrolling content">
<div class="ntf-15a__page" id="ntf-15a-page">
<div class="ntf-15a__banner" id="ntf-15a-banner" role="region" aria-label="Announcement">
<div class="ntf-15a__clip">
<div class="ntf-15a__inner">
<span class="ntf-15a__tag" aria-hidden="true">NEW</span>
<p class="ntf-15a__msg"><b>Snippets 2.0 is live</b> — 30 fresh notification patterns, all copy-paste ready.</p>
<a class="ntf-15a__cta" href="#" onclick="return false">Browse</a>
<button class="ntf-15a__x" id="ntf-15a-x" type="button" aria-label="Dismiss announcement">✕</button>
</div>
</div>
</div>
<header class="ntf-15a__hero"><h2>The banner above is <em>sticky</em></h2><p>Scroll this panel — it holds the top edge. Dismiss it — the page glides, never jumps.</p></header>
<article class="ntf-15a__prose"><h3>Why sticky beats fixed</h3><p>A fixed banner floats over the document, so the page needs manual padding to avoid hiding content beneath it. A sticky banner occupies real layout space and only pins once its edge reaches the viewport.</p><h3>Dismissal without the yank</h3><p>Removing a 56px banner node scrolls everything up 56 pixels instantly. Collapsing grid-template-rows from 1fr to 0fr animates the same change over 400ms, and the reading position eases instead of teleporting.</p><h3>Filler paragraph</h3><p>Keep scrolling to feel the pin. The banner belongs to this scroll container, so it pins to the panel's top rather than the page's.</p><p>Sticky elements need every ancestor to keep visible overflow — one stray overflow:hidden and the pin silently dies.</p></article>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400..800&display=swap" rel="stylesheet">
<section class="ntf-15b" aria-label="Banner that condenses as you scroll, no JavaScript">
<div class="ntf-15b__page">
<div class="ntf-15b__banner" role="region" aria-label="Promotion">
<p class="ntf-15b__big" aria-hidden="true">EARLY ACCESS · <b>40% OFF</b> annual plans this week</p>
<p class="ntf-15b__small">40% off — <b>claim</b></p>
</div>
<article class="ntf-15b__prose">
<h2>Scroll me</h2>
<p>The banner's padding, size and even its message are keyframes mapped onto scroll position with <b>animation-timeline: scroll()</b>. No listeners, no throttling, runs off the main thread.</p>
<p>Scroll back up and it re-expands — the timeline plays in both directions because it IS the scrollbar.</p>
<p>The full promo line cross-fades into a compact one at the 60% mark of the 180px range; both live in the DOM, so screen readers always get the complete message.</p>
<p>Support: Chrome 115+, Edge 115+, Safari 26. The @supports guard keeps older browsers on the expanded banner permanently — nothing breaks, it just doesn't condense.</p>
<p>Filler to make the panel scroll. The pattern shines on marketing sites where the promo must stay present but shouldn't keep shouting once the visitor commits to reading.</p>
</article>
</div>
</section>
</div>
```
## CSS
```css
.ntf-15-group {
display: flex;
flex-direction: column;
width: 100%;
}
.ntf-15-group > section {
width: 100%;
}
.ntf-15a,
.ntf-15a *,
.ntf-15a *::before,
.ntf-15a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15a {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: radial-gradient(120% 100% at 50% 0%,#151a26,#0b0e15);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.ntf-15a__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #f5f6f9;
border: 1px solid rgba(165,190,230,.2);
box-shadow: 0 34px 80px -40px rgba(0,0,0,.9);
}
.ntf-15a__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .4s cubic-bezier(.4,0,.2,1),opacity .3s ease;
}
.ntf-15a__banner.is-dismissed {
grid-template-rows: 0fr;
opacity: 0;
}
.ntf-15a__clip {
overflow: hidden;
}
.ntf-15a__inner {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
background: linear-gradient(90deg,oklch(0.45 0.16 280),oklch(0.5 0.17 250));
color: #fff;
}
.ntf-15a__tag {
flex: none;
font-size: 10px;
font-weight: 700;
letter-spacing: .1em;
padding: 4px 8px;
border-radius: 6px;
background: rgb(255 255 255 / .18);
border: 1px solid rgb(255 255 255 / .3);
}
.ntf-15a__msg {
font-size: 13.5px;
line-height: 1.4;
min-width: 0;
}
.ntf-15a__msg b {
font-weight: 700;
}
.ntf-15a__cta {
margin-left: auto;
flex: none;
font-size: 12.5px;
font-weight: 700;
color: oklch(0.35 0.12 270);
background: #fff;
text-decoration: none;
padding: 7px 14px;
border-radius: 8px;
transition: scale .12s;
}
.ntf-15a__cta:hover {
scale: 1.04;
}
.ntf-15a__x {
flex: none;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
font-size: 12px;
color: rgb(255 255 255 / .8);
background: transparent;
cursor: pointer;
transition: background .15s;
}
.ntf-15a__x:hover {
background: rgb(255 255 255 / .15);
}
.ntf-15a__hero {
padding: 38px 30px 10px;
color: #1b1f2a;
}
.ntf-15a__hero h2 {
font-size: 26px;
font-weight: 600;
letter-spacing: -.01em;
}
.ntf-15a__hero h2 em {
font-style: normal;
color: oklch(0.5 0.17 260);
}
.ntf-15a__hero p {
margin-top: 8px;
font-size: 14px;
color: rgba(27,31,42,.6);
}
.ntf-15a__prose {
padding: 10px 30px 44px;
color: rgba(27,31,42,.75);
}
.ntf-15a__prose h3 {
margin-top: 26px;
font-size: 16px;
font-weight: 600;
color: #1b1f2a;
}
.ntf-15a__prose p {
margin-top: 8px;
font-size: 13.5px;
line-height: 1.75;
text-wrap: pretty;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15a__banner {
transition-duration: .01s;
}
}
.ntf-15b,
.ntf-15b *,
.ntf-15b *::before,
.ntf-15b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,#f6f2e9,#ece4d2);
font-family: 'Outfit','Segoe UI',system-ui,sans-serif;
}
.ntf-15b__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #fffdf8;
border: 1px solid rgba(35,31,24,.12);
box-shadow: 0 30px 70px -38px rgba(35,31,24,.55);
}
.ntf-15b__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
place-items: center;
background: #211d16;
color: #fdfaf2;
padding: 26px 20px;
}
.ntf-15b__big {
font-size: clamp(16px,2.6vw,21px);
font-weight: 500;
letter-spacing: .01em;
}
.ntf-15b__big b {
font-weight: 800;
color: oklch(0.85 0.14 85);
}
.ntf-15b__small {
position: absolute;
font-size: 13px;
font-weight: 600;
opacity: 0;
}
.ntf-15b__small b {
color: oklch(0.85 0.14 85);
}
@supports (animation-timeline: scroll()) {
.ntf-15b__banner {
animation: ntf-15b-shrink linear both;
animation-timeline: scroll();
animation-range: 0 180px;
}
.ntf-15b__big {
animation: ntf-15b-hide linear both;
animation-timeline: scroll();
animation-range: 0 140px;
}
.ntf-15b__small {
animation: ntf-15b-show linear both;
animation-timeline: scroll();
animation-range: 100px 180px;
}
@keyframes ntf-15b-shrink {
to {
padding: 9px 20px;
background: #181510;
}
}
@keyframes ntf-15b-hide {
to {
opacity: 0;
scale: .92;
}
}
@keyframes ntf-15b-show {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
}
.ntf-15b__prose {
padding: 34px 30px 46px;
color: rgba(35,31,24,.75);
}
.ntf-15b__prose h2 {
font-size: 24px;
font-weight: 700;
letter-spacing: -.01em;
color: #211d16;
}
.ntf-15b__prose p {
margin-top: 14px;
font-size: 14px;
line-height: 1.8;
text-wrap: pretty;
}
.ntf-15b__prose b {
color: #211d16;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15b__banner,
.ntf-15b__big,
.ntf-15b__small {
animation: none;
}
.ntf-15b__small {
position: static;
opacity: 0;
display: none;
}
}
```
## JavaScript
```js
(function(){
var banner=document.getElementById('ntf-15a-banner'); if(!banner) return;
document.getElementById('ntf-15a-x').addEventListener('click',function(){
banner.classList.add('is-dismissed');
banner.addEventListener('transitionend',function(e){
if(e.propertyName==='grid-template-rows') banner.remove();
/* In production: localStorage.setItem('banner-snippets-2', 'dismissed') before removing. */
});
});
})();
/* No JavaScript — animation-timeline:scroll() binds three keyframe sets to the panel's scroll position (banner shrinks, long message fades out, short one fades in); scrolling up plays everything backwards for free. */
```Here's a working CSS Notification from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Sticky Top Notification Banner CSS
Source: https://codefronts.com/snippets/css-notifications/sticky-top-notification-banner-css/
Two sticky-banner behaviors over real scrolling content: a dismissible announcement bar whose position:sticky anatomy is visible live (scroll and watch it hold the top edge) and whose dismissal collapses smoothly instead of yanking the page up — and a zero-JS scroll-driven banner that condenses from a tall promo into a slim strip as you scroll, powered by animation-timeline: scroll().
## HTML
```html
<div class="ntf-15-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-15a" aria-label="Sticky announcement banner over scrolling content">
<div class="ntf-15a__page" id="ntf-15a-page">
<div class="ntf-15a__banner" id="ntf-15a-banner" role="region" aria-label="Announcement">
<div class="ntf-15a__clip">
<div class="ntf-15a__inner">
<span class="ntf-15a__tag" aria-hidden="true">NEW</span>
<p class="ntf-15a__msg"><b>Snippets 2.0 is live</b> — 30 fresh notification patterns, all copy-paste ready.</p>
<a class="ntf-15a__cta" href="#" onclick="return false">Browse</a>
<button class="ntf-15a__x" id="ntf-15a-x" type="button" aria-label="Dismiss announcement">✕</button>
</div>
</div>
</div>
<header class="ntf-15a__hero"><h2>The banner above is <em>sticky</em></h2><p>Scroll this panel — it holds the top edge. Dismiss it — the page glides, never jumps.</p></header>
<article class="ntf-15a__prose"><h3>Why sticky beats fixed</h3><p>A fixed banner floats over the document, so the page needs manual padding to avoid hiding content beneath it. A sticky banner occupies real layout space and only pins once its edge reaches the viewport.</p><h3>Dismissal without the yank</h3><p>Removing a 56px banner node scrolls everything up 56 pixels instantly. Collapsing grid-template-rows from 1fr to 0fr animates the same change over 400ms, and the reading position eases instead of teleporting.</p><h3>Filler paragraph</h3><p>Keep scrolling to feel the pin. The banner belongs to this scroll container, so it pins to the panel's top rather than the page's.</p><p>Sticky elements need every ancestor to keep visible overflow — one stray overflow:hidden and the pin silently dies.</p></article>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400..800&display=swap" rel="stylesheet">
<section class="ntf-15b" aria-label="Banner that condenses as you scroll, no JavaScript">
<div class="ntf-15b__page">
<div class="ntf-15b__banner" role="region" aria-label="Promotion">
<p class="ntf-15b__big" aria-hidden="true">EARLY ACCESS · <b>40% OFF</b> annual plans this week</p>
<p class="ntf-15b__small">40% off — <b>claim</b></p>
</div>
<article class="ntf-15b__prose">
<h2>Scroll me</h2>
<p>The banner's padding, size and even its message are keyframes mapped onto scroll position with <b>animation-timeline: scroll()</b>. No listeners, no throttling, runs off the main thread.</p>
<p>Scroll back up and it re-expands — the timeline plays in both directions because it IS the scrollbar.</p>
<p>The full promo line cross-fades into a compact one at the 60% mark of the 180px range; both live in the DOM, so screen readers always get the complete message.</p>
<p>Support: Chrome 115+, Edge 115+, Safari 26. The @supports guard keeps older browsers on the expanded banner permanently — nothing breaks, it just doesn't condense.</p>
<p>Filler to make the panel scroll. The pattern shines on marketing sites where the promo must stay present but shouldn't keep shouting once the visitor commits to reading.</p>
</article>
</div>
</section>
</div>
```
## CSS
```css
.ntf-15-group {
display: flex;
flex-direction: column;
width: 100%;
}
.ntf-15-group > section {
width: 100%;
}
.ntf-15a,
.ntf-15a *,
.ntf-15a *::before,
.ntf-15a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15a {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: radial-gradient(120% 100% at 50% 0%,#151a26,#0b0e15);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.ntf-15a__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #f5f6f9;
border: 1px solid rgba(165,190,230,.2);
box-shadow: 0 34px 80px -40px rgba(0,0,0,.9);
}
.ntf-15a__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .4s cubic-bezier(.4,0,.2,1),opacity .3s ease;
}
.ntf-15a__banner.is-dismissed {
grid-template-rows: 0fr;
opacity: 0;
}
.ntf-15a__clip {
overflow: hidden;
}
.ntf-15a__inner {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
background: linear-gradient(90deg,oklch(0.45 0.16 280),oklch(0.5 0.17 250));
color: #fff;
}
.ntf-15a__tag {
flex: none;
font-size: 10px;
font-weight: 700;
letter-spacing: .1em;
padding: 4px 8px;
border-radius: 6px;
background: rgb(255 255 255 / .18);
border: 1px solid rgb(255 255 255 / .3);
}
.ntf-15a__msg {
font-size: 13.5px;
line-height: 1.4;
min-width: 0;
}
.ntf-15a__msg b {
font-weight: 700;
}
.ntf-15a__cta {
margin-left: auto;
flex: none;
font-size: 12.5px;
font-weight: 700;
color: oklch(0.35 0.12 270);
background: #fff;
text-decoration: none;
padding: 7px 14px;
border-radius: 8px;
transition: scale .12s;
}
.ntf-15a__cta:hover {
scale: 1.04;
}
.ntf-15a__x {
flex: none;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
font-size: 12px;
color: rgb(255 255 255 / .8);
background: transparent;
cursor: pointer;
transition: background .15s;
}
.ntf-15a__x:hover {
background: rgb(255 255 255 / .15);
}
.ntf-15a__hero {
padding: 38px 30px 10px;
color: #1b1f2a;
}
.ntf-15a__hero h2 {
font-size: 26px;
font-weight: 600;
letter-spacing: -.01em;
}
.ntf-15a__hero h2 em {
font-style: normal;
color: oklch(0.5 0.17 260);
}
.ntf-15a__hero p {
margin-top: 8px;
font-size: 14px;
color: rgba(27,31,42,.6);
}
.ntf-15a__prose {
padding: 10px 30px 44px;
color: rgba(27,31,42,.75);
}
.ntf-15a__prose h3 {
margin-top: 26px;
font-size: 16px;
font-weight: 600;
color: #1b1f2a;
}
.ntf-15a__prose p {
margin-top: 8px;
font-size: 13.5px;
line-height: 1.75;
text-wrap: pretty;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15a__banner {
transition-duration: .01s;
}
}
.ntf-15b,
.ntf-15b *,
.ntf-15b *::before,
.ntf-15b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ntf-15b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,#f6f2e9,#ece4d2);
font-family: 'Outfit','Segoe UI',system-ui,sans-serif;
}
.ntf-15b__page {
width: min(680px,100%);
height: min(72vh,560px);
overflow: auto;
border-radius: 18px;
background: #fffdf8;
border: 1px solid rgba(35,31,24,.12);
box-shadow: 0 30px 70px -38px rgba(35,31,24,.55);
}
.ntf-15b__banner {
position: sticky;
top: 0;
z-index: 5;
display: grid;
place-items: center;
background: #211d16;
color: #fdfaf2;
padding: 26px 20px;
}
.ntf-15b__big {
font-size: clamp(16px,2.6vw,21px);
font-weight: 500;
letter-spacing: .01em;
}
.ntf-15b__big b {
font-weight: 800;
color: oklch(0.85 0.14 85);
}
.ntf-15b__small {
position: absolute;
font-size: 13px;
font-weight: 600;
opacity: 0;
}
.ntf-15b__small b {
color: oklch(0.85 0.14 85);
}
@supports (animation-timeline: scroll()) {
.ntf-15b__banner {
animation: ntf-15b-shrink linear both;
animation-timeline: scroll();
animation-range: 0 180px;
}
.ntf-15b__big {
animation: ntf-15b-hide linear both;
animation-timeline: scroll();
animation-range: 0 140px;
}
.ntf-15b__small {
animation: ntf-15b-show linear both;
animation-timeline: scroll();
animation-range: 100px 180px;
}
@keyframes ntf-15b-shrink {
to {
padding: 9px 20px;
background: #181510;
}
}
@keyframes ntf-15b-hide {
to {
opacity: 0;
scale: .92;
}
}
@keyframes ntf-15b-show {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
}
.ntf-15b__prose {
padding: 34px 30px 46px;
color: rgba(35,31,24,.75);
}
.ntf-15b__prose h2 {
font-size: 24px;
font-weight: 700;
letter-spacing: -.01em;
color: #211d16;
}
.ntf-15b__prose p {
margin-top: 14px;
font-size: 14px;
line-height: 1.8;
text-wrap: pretty;
}
.ntf-15b__prose b {
color: #211d16;
}
@media (prefers-reduced-motion: reduce) {
.ntf-15b__banner,
.ntf-15b__big,
.ntf-15b__small {
animation: none;
}
.ntf-15b__small {
position: static;
opacity: 0;
display: none;
}
}
```
## JavaScript
```js
(function(){
var banner=document.getElementById('ntf-15a-banner'); if(!banner) return;
document.getElementById('ntf-15a-x').addEventListener('click',function(){
banner.classList.add('is-dismissed');
banner.addEventListener('transitionend',function(e){
if(e.propertyName==='grid-template-rows') banner.remove();
/* In production: localStorage.setItem('banner-snippets-2', 'dismissed') before removing. */
});
});
})();
/* No JavaScript — animation-timeline:scroll() binds three keyframe sets to the panel's scroll position (banner shrinks, long message fades out, short one fades in); scrolling up plays everything backwards for free. */
```How this works
position:sticky beats fixed for banners because it participates in layout — content starts BELOW the banner naturally, no padding-top hacks, no covered headlines:
.banner{ position:sticky; top:0; z-index:10 }The subtle part is dismissal. Removing the node teleports every element up by the banner's height. Collapse it instead — grid rows animate intrinsic height gracefully:
.banner{ display:grid; grid-template-rows:1fr;
transition:grid-template-rows .4s, opacity .3s }
.banner.is-dismissed{ grid-template-rows:0fr; opacity:0 }The condensing variant attaches its animation to scroll position instead of time — animation-timeline:scroll() maps keyframes onto the scroller's progress, so the banner's padding, font-size and message swap are driven directly by how far you've scrolled, with zero scroll listeners and zero jank:
.banner{ animation:condense linear both;
animation-timeline:scroll();
animation-range:0 180px }Make it yours
- animation-range:0 180px means 'finish condensing within the first 180px of scroll' — shorten it for snappier commitment.
- Sticky inside a scrollable panel sticks to THAT panel's top (this demo) — on a real page the nearest scroll container is the viewport.
- Keep dismissal state in localStorage keyed by campaign id (banner-v3) so re-runs of the same promo stay dismissed.
- Stack a nav under the banner? Give the nav position:sticky; top:0 too and the banner top:0 — they'll hand off naturally as the banner collapses.
Gotchas — read before shipping
- position:sticky silently fails when ANY ancestor has overflow:hidden — the #1 'sticky isn't sticking' bug; audit ancestors first.
- z-index: sticky elements form a stacking context but content with transforms can still paint over the banner — give it a real z-index.
- Removing the banner node on dismiss YANKS the page; collapse first (grid rows), remove on transitionend.
- Scroll-driven animations need Chrome 115+/Safari 26; wrap in @supports (animation-timeline:scroll()) and older browsers just keep the full-size banner.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 26+ | not yet | 115+ |
Floor set by oklch(), animation-timeline, text-wrap as this demo is written. The core technique itself goes back further — Chrome 107+.
Sticky + grid-rows collapse works everywhere evergreen (animatable grid rows: Chrome 107 / Safari 16 / Firefox 66). The condensing variant's animation-timeline:scroll() is Chrome 115 / Edge 115 / Safari 26, behind a flag in Firefox — guarded by @supports so others keep the expanded banner.