30 CSS Notifications16 / 30

Pure CSSMIT licensed

CSS Notification Banner With Close Button

The same dismissible banner built in two eras: a 2027 version where :has() lets the banner react to its OWN close-checkbox and exit through display:none via allow-discrete, and the classic checkbox-hack (input + sibling combinator) that has worked since 2011 — styled identically so you can diff the two mechanisms line by line.

Published

Live Demo
Try it

The code

<div class="ntf-16-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-16a" aria-label="Banner dismissed by :has(:checked)">
  <div class="ntf-16a__frame">
    <p class="ntf-16a__era">2027 · :has() looks at its own checkbox</p>
    <div class="ntf-16a__banner" role="region" aria-label="Storage notice">
      <div class="ntf-16a__clip">
        <div class="ntf-16a__inner">
          <span class="ntf-16a__ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12H16l-2 3h-4l-2-3H2"/><path d="M5.5 5.1L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.5-6.9A2 2 0 0 0 16.7 4H7.3a2 2 0 0 0-1.8 1.1z"/></svg></span>
          <p class="ntf-16a__tx"><b>Workspace storage 90% full.</b> Older exports will be archived in 7 days — download anything you need to keep.</p>
          <label class="ntf-16a__x" aria-label="Dismiss storage notice"><input class="ntf-16a__box" type="checkbox">&#10005;</label>
        </div>
      </div>
    </div>
    <p class="ntf-16a__after">Content below the banner glides up as the row collapses, then the banner leaves the layout through <b>display:none</b>.</p>
  </div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-16b" aria-label="Banner dismissed by the classic checkbox hack">
  <div class="ntf-16b__frame">
    <p class="ntf-16b__era">2011 · input:checked ~ sibling — still ships</p>
    <input class="ntf-16b__box" type="checkbox" id="ntf-16b-close" aria-label="Dismiss update notice">
    <div class="ntf-16b__banner" role="region" aria-label="Update notice">
      <div class="ntf-16b__clip">
        <div class="ntf-16b__inner">
          <span class="ntf-16b__ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg></span>
          <p class="ntf-16b__tx"><b>Version 3.2 available.</b> Restart the app to apply the update.</p>
          <label class="ntf-16b__x" for="ntf-16b-close" aria-hidden="true">&#10005;</label>
        </div>
      </div>
    </div>
    <p class="ntf-16b__after">The input sits <b>before</b> the banner in the DOM — <b>#close:checked ~ .banner</b> is the whole mechanism. Fragile ordering, heroic browser support.</p>
  </div>
</section>
</div>
.ntf-16-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.ntf-16-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.ntf-16a,
.ntf-16a *,
.ntf-16a *::before,
.ntf-16a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-16a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(120% 100% at 50% 0%,#141a26,#0b0e15);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.ntf-16a__frame {
  width: min(560px,100%);
}

.ntf-16a__era {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  font-weight: 600;
  color: oklch(0.75 0.13 200);
  margin-bottom: 12px;
}

.ntf-16a__banner {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows .45s cubic-bezier(.4,0,.2,1),opacity .35s ease,display .45s allow-discrete;
}

.ntf-16a__banner:has(.ntf-16a__box:checked) {
  grid-template-rows: 0fr;
  opacity: 0;
  display: none;
}

.ntf-16a__clip {
  overflow: hidden;
}

.ntf-16a__inner {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 15px 16px;
  border-radius: 14px;
  background: oklch(0.82 0.13 85 / .1);
  border: 1px solid oklch(0.82 0.13 85 / .35);
}

.ntf-16a__ico {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: #171308;
  background: oklch(0.82 0.13 85);
}

.ntf-16a__ico svg {
  width: 18px;
  height: 18px;
}

.ntf-16a__tx {
  font-size: 13px;
  line-height: 1.55;
  color: rgba(235,228,205,.85);
  min-width: 0;
}

.ntf-16a__tx b {
  color: #f4eed9;
  font-weight: 600;
}

.ntf-16a__x {
  position: relative;
  margin-left: auto;
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  font-size: 12px;
  color: rgba(235,228,205,.7);
  cursor: pointer;
  transition: background .15s,color .15s;
}

.ntf-16a__x:hover {
  background: oklch(0.82 0.13 85 / .18);
  color: #f4eed9;
}

.ntf-16a__box {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.ntf-16a__x:has(.ntf-16a__box:focus-visible) {
  outline: 2px solid oklch(0.82 0.13 85);
  outline-offset: 2px;
}

.ntf-16a__after {
  margin-top: 14px;
  font-size: 13px;
  line-height: 1.65;
  color: rgba(212,224,246,.5);
  text-wrap: pretty;
}

.ntf-16a__after b {
  color: rgba(237,242,252,.8);
}

@media (prefers-reduced-motion: reduce) {
  .ntf-16a__banner {
    transition-duration: .01s;
  }
}

.ntf-16b,
.ntf-16b *,
.ntf-16b *::before,
.ntf-16b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-16b {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(120% 100% at 50% 100%,#101722,#0a0d13);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.ntf-16b__frame {
  position: relative;
  width: min(560px,100%);
}

.ntf-16b__era {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  font-weight: 600;
  color: oklch(0.72 0.14 250);
  margin-bottom: 12px;
}

.ntf-16b__box {
  position: absolute;
  width: 30px;
  height: 30px;
  opacity: 0;
  z-index: 2;
  inset-inline-end: 8px;
  margin-top: 44px;
  cursor: pointer;
}

.ntf-16b__banner {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows .45s cubic-bezier(.4,0,.2,1),opacity .35s ease;
}

.ntf-16b__box:checked ~ .ntf-16b__banner {
  grid-template-rows: 0fr;
  opacity: 0;
  pointer-events: none;
}

.ntf-16b__clip {
  overflow: hidden;
}

.ntf-16b__inner {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 15px 16px;
  border-radius: 14px;
  background: oklch(0.72 0.14 250 / .1);
  border: 1px solid oklch(0.72 0.14 250 / .35);
}

.ntf-16b__ico {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: #0d1420;
  background: oklch(0.72 0.14 250);
}

.ntf-16b__ico svg {
  width: 18px;
  height: 18px;
}

.ntf-16b__tx {
  font-size: 13px;
  line-height: 1.55;
  color: rgba(210,224,246,.85);
  min-width: 0;
}

.ntf-16b__tx b {
  color: #e9f0fc;
  font-weight: 600;
}

.ntf-16b__x {
  margin-left: auto;
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  font-size: 12px;
  color: rgba(210,224,246,.7);
  cursor: pointer;
  transition: background .15s,color .15s;
}

.ntf-16b__x:hover {
  background: oklch(0.72 0.14 250 / .18);
  color: #e9f0fc;
}

.ntf-16b__box:focus-visible ~ .ntf-16b__banner .ntf-16b__x {
  outline: 2px solid oklch(0.72 0.14 250);
  outline-offset: 2px;
}

.ntf-16b__after {
  margin-top: 14px;
  font-size: 13px;
  line-height: 1.65;
  color: rgba(210,224,246,.5);
  text-wrap: pretty;
}

.ntf-16b__after b {
  color: rgba(233,240,252,.85);
  font-family: ui-monospace,Menlo,monospace;
  font-size: .9em;
}

@media (prefers-reduced-motion: reduce) {
  .ntf-16b__banner {
    transition-duration: .01s;
  }
}
/* No JavaScript — the banner watches its own checkbox with :has(:checked), collapses its grid row, fades, and finally transitions display itself away thanks to allow-discrete. */

/* No JavaScript — the pre-:has() classic: a checkbox BEFORE the banner plus the general sibling combinator. The input is absolutely positioned over the visible ✕ so pointer, keyboard and screen-reader users all operate the same control. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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: CSS Notification Banner With Close Button
Source: https://codefronts.com/snippets/css-notifications/css-notification-banner-with-close-button/

The same dismissible banner built in two eras: a 2027 version where :has() lets the banner react to its OWN close-checkbox and exit through display:none via allow-discrete, and the classic checkbox-hack (input + sibling combinator) that has worked since 2011 — styled identically so you can diff the two mechanisms line by line.
## HTML
```html
<div class="ntf-16-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-16a" aria-label="Banner dismissed by :has(:checked)">
  <div class="ntf-16a__frame">
    <p class="ntf-16a__era">2027 · :has() looks at its own checkbox</p>
    <div class="ntf-16a__banner" role="region" aria-label="Storage notice">
      <div class="ntf-16a__clip">
        <div class="ntf-16a__inner">
          <span class="ntf-16a__ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12H16l-2 3h-4l-2-3H2"/><path d="M5.5 5.1L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.5-6.9A2 2 0 0 0 16.7 4H7.3a2 2 0 0 0-1.8 1.1z"/></svg></span>
          <p class="ntf-16a__tx"><b>Workspace storage 90% full.</b> Older exports will be archived in 7 days — download anything you need to keep.</p>
          <label class="ntf-16a__x" aria-label="Dismiss storage notice"><input class="ntf-16a__box" type="checkbox">&#10005;</label>
        </div>
      </div>
    </div>
    <p class="ntf-16a__after">Content below the banner glides up as the row collapses, then the banner leaves the layout through <b>display:none</b>.</p>
  </div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-16b" aria-label="Banner dismissed by the classic checkbox hack">
  <div class="ntf-16b__frame">
    <p class="ntf-16b__era">2011 · input:checked ~ sibling — still ships</p>
    <input class="ntf-16b__box" type="checkbox" id="ntf-16b-close" aria-label="Dismiss update notice">
    <div class="ntf-16b__banner" role="region" aria-label="Update notice">
      <div class="ntf-16b__clip">
        <div class="ntf-16b__inner">
          <span class="ntf-16b__ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg></span>
          <p class="ntf-16b__tx"><b>Version 3.2 available.</b> Restart the app to apply the update.</p>
          <label class="ntf-16b__x" for="ntf-16b-close" aria-hidden="true">&#10005;</label>
        </div>
      </div>
    </div>
    <p class="ntf-16b__after">The input sits <b>before</b> the banner in the DOM — <b>#close:checked ~ .banner</b> is the whole mechanism. Fragile ordering, heroic browser support.</p>
  </div>
</section>
</div>
```
## CSS
```css
.ntf-16-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.ntf-16-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.ntf-16a,
.ntf-16a *,
.ntf-16a *::before,
.ntf-16a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-16a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(120% 100% at 50% 0%,#141a26,#0b0e15);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.ntf-16a__frame {
  width: min(560px,100%);
}

.ntf-16a__era {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  font-weight: 600;
  color: oklch(0.75 0.13 200);
  margin-bottom: 12px;
}

.ntf-16a__banner {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows .45s cubic-bezier(.4,0,.2,1),opacity .35s ease,display .45s allow-discrete;
}

.ntf-16a__banner:has(.ntf-16a__box:checked) {
  grid-template-rows: 0fr;
  opacity: 0;
  display: none;
}

.ntf-16a__clip {
  overflow: hidden;
}

.ntf-16a__inner {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 15px 16px;
  border-radius: 14px;
  background: oklch(0.82 0.13 85 / .1);
  border: 1px solid oklch(0.82 0.13 85 / .35);
}

.ntf-16a__ico {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: #171308;
  background: oklch(0.82 0.13 85);
}

.ntf-16a__ico svg {
  width: 18px;
  height: 18px;
}

.ntf-16a__tx {
  font-size: 13px;
  line-height: 1.55;
  color: rgba(235,228,205,.85);
  min-width: 0;
}

.ntf-16a__tx b {
  color: #f4eed9;
  font-weight: 600;
}

.ntf-16a__x {
  position: relative;
  margin-left: auto;
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  font-size: 12px;
  color: rgba(235,228,205,.7);
  cursor: pointer;
  transition: background .15s,color .15s;
}

.ntf-16a__x:hover {
  background: oklch(0.82 0.13 85 / .18);
  color: #f4eed9;
}

.ntf-16a__box {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.ntf-16a__x:has(.ntf-16a__box:focus-visible) {
  outline: 2px solid oklch(0.82 0.13 85);
  outline-offset: 2px;
}

.ntf-16a__after {
  margin-top: 14px;
  font-size: 13px;
  line-height: 1.65;
  color: rgba(212,224,246,.5);
  text-wrap: pretty;
}

.ntf-16a__after b {
  color: rgba(237,242,252,.8);
}

@media (prefers-reduced-motion: reduce) {
  .ntf-16a__banner {
    transition-duration: .01s;
  }
}

.ntf-16b,
.ntf-16b *,
.ntf-16b *::before,
.ntf-16b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-16b {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(120% 100% at 50% 100%,#101722,#0a0d13);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.ntf-16b__frame {
  position: relative;
  width: min(560px,100%);
}

.ntf-16b__era {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  font-weight: 600;
  color: oklch(0.72 0.14 250);
  margin-bottom: 12px;
}

.ntf-16b__box {
  position: absolute;
  width: 30px;
  height: 30px;
  opacity: 0;
  z-index: 2;
  inset-inline-end: 8px;
  margin-top: 44px;
  cursor: pointer;
}

.ntf-16b__banner {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows .45s cubic-bezier(.4,0,.2,1),opacity .35s ease;
}

.ntf-16b__box:checked ~ .ntf-16b__banner {
  grid-template-rows: 0fr;
  opacity: 0;
  pointer-events: none;
}

.ntf-16b__clip {
  overflow: hidden;
}

.ntf-16b__inner {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 15px 16px;
  border-radius: 14px;
  background: oklch(0.72 0.14 250 / .1);
  border: 1px solid oklch(0.72 0.14 250 / .35);
}

.ntf-16b__ico {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: #0d1420;
  background: oklch(0.72 0.14 250);
}

.ntf-16b__ico svg {
  width: 18px;
  height: 18px;
}

.ntf-16b__tx {
  font-size: 13px;
  line-height: 1.55;
  color: rgba(210,224,246,.85);
  min-width: 0;
}

.ntf-16b__tx b {
  color: #e9f0fc;
  font-weight: 600;
}

.ntf-16b__x {
  margin-left: auto;
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  font-size: 12px;
  color: rgba(210,224,246,.7);
  cursor: pointer;
  transition: background .15s,color .15s;
}

.ntf-16b__x:hover {
  background: oklch(0.72 0.14 250 / .18);
  color: #e9f0fc;
}

.ntf-16b__box:focus-visible ~ .ntf-16b__banner .ntf-16b__x {
  outline: 2px solid oklch(0.72 0.14 250);
  outline-offset: 2px;
}

.ntf-16b__after {
  margin-top: 14px;
  font-size: 13px;
  line-height: 1.65;
  color: rgba(210,224,246,.5);
  text-wrap: pretty;
}

.ntf-16b__after b {
  color: rgba(233,240,252,.85);
  font-family: ui-monospace,Menlo,monospace;
  font-size: .9em;
}

@media (prefers-reduced-motion: reduce) {
  .ntf-16b__banner {
    transition-duration: .01s;
  }
}
```

## JavaScript
```js
/* No JavaScript — the banner watches its own checkbox with :has(:checked), collapses its grid row, fades, and finally transitions display itself away thanks to allow-discrete. */

/* No JavaScript — the pre-:has() classic: a checkbox BEFORE the banner plus the general sibling combinator. The input is absolutely positioned over the visible ✕ so pointer, keyboard and screen-reader users all operate the same control. */
```

How this works

Pure-CSS dismissal is a checkbox wearing a close-button costume. The modern wiring reads inside-out — the banner watches its own descendant:

.banner:has(.close-box:checked){
  grid-template-rows:0fr; opacity:0;
  /* then leave the layout entirely: */
  transition:… , display .45s allow-discrete;
  display:none;   /* in the dismissed state */
}

The historic wiring needs the input OUTSIDE, before the banner, because CSS could only look sideways-and-down:

#close:checked ~ .banner{ … hidden styles … }

Both hide the banner; the differences are anatomy. :has() keeps the checkbox inside the component (portable, no id collisions, works repeated on a page); the sibling hack demands a specific DOM order and a unique id per instance. The demo styles both banners identically so the mechanism — not the paint — is the lesson. Both label-buttons are keyboard-real: the input takes focus, space toggles it, and :focus-visible draws the ring on the visible costume.

Make it yours

  • Add @starting-style entry styles and the banner also animates IN when rendered — same transition list, no extra work.
  • Re-show for testing: wrap in a form and add a label for a reset — or just uncheck in DevTools; production re-shows are a JS/localStorage decision.
  • The collapse duration (.45s) should scale with banner height — tall cookie banners read better at .6s.
  • Recolor per severity by swapping the two accent custom properties; the close mechanics don't care.

Gotchas — read before shipping

  • CSS-only dismissal FORGETS on reload — that's fine for session noise (promos), wrong for legal notices; persist real dismissals in JS.
  • The checkbox must stay focusable: opacity:0 + position:absolute keeps it in the tab order, display:none would remove keyboard dismissal entirely.
  • Screen readers announce the label's text — 'Dismiss announcement', never a bare ✕; both demos carry the aria-label.
  • The sibling version breaks the moment a wrapper div lands between input and banner — the fragility is WHY :has() replaced it.

Browser support

ChromeSafariFirefoxEdge
117+17.4+129+117+

:has() is Chrome 105 / Safari 15.4 / Firefox 121; the display exit needs allow-discrete (Chrome 117 / Safari 17.4 / Firefox 129) — without it the banner still collapses, it just keeps zero-height layout. The checkbox-hack variant works back to IE9-era browsers.

Techniques used in this demo

Search CodeFronts

Loading…