Navigation

v0.0.4
Feedback

Banner

One line across the top of a page for news everyone on it should have.

On this page

A banner is one line across the top of a page or a panel, for something everyone on it should know: a release, planned maintenance, an incident. It is a band of the tray tone with a hairline under it, quieter than an alert, which belongs to one task, and longer-lived than a toast, which reports one event.

Banner

<dialog open class="banner banner-dismissible" aria-label="Announcement">  <p class="banner-content">    <span class="banner-title">Atlas 4 is here.</span>Faster builds and a new dashboard.    <a class="banner-action" href="#">Read the notes</a>  </p>  <form method="dialog" class="banner-close">    <button class="btn btn-quiet btn-square btn-sm" aria-label="Dismiss">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </form></dialog><div class="p-lg text-sm fg-text-muted">The page continues under the banner.</div>
Dismiss the banner with its button. No script is involved.

#Dismissing

A banner that can be dismissed is a <dialog open> with banner-dismissible. A non-modal dialog opened by its attribute is part of the page from the first paint, and the button inside a <form method="dialog"> closes it without script. The band then folds away to nothing, where the browser can interpolate to an automatic height; elsewhere it leaves at once.

To remember the dismissal, listen for the dialog's close event and store it; leave the open attribute out when rendering the page next time.

A banner that stays is any element, such as a <div role="region"> with a label.

#Tones

A status tone tints the band and colors its icon. The words stay in ink and say what the hue means.

Tones

<div class="banner banner-info" role="status">  <p class="banner-content">    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">      <circle cx="12" cy="12" r="9" />      <path d="M12 11v5M12 8h.01" />    </svg>A new version is available.    <a class="banner-action" href="#">Reload</a>  </p></div><div class="banner banner-success" role="status">  <p class="banner-content">    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">      <circle cx="12" cy="12" r="9" />      <path d="m8.5 12.5 2.5 2.5 4.5-5.5" />    </svg>All systems are back to normal.</p></div><div class="banner banner-warning" role="status">  <p class="banner-content">    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">      <path d="M12 4 3 19h18zM12 10v4M12 17h.01" />    </svg>    <span class="banner-title">Scheduled maintenance</span>on Saturday from 02:00 to 04:00 UTC.</p></div><div class="banner banner-danger" role="alert">  <p class="banner-content">    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">      <circle cx="12" cy="12" r="9" />      <path d="M12 8v5M12 16h.01" />    </svg>Payments are failing for some customers.    <a class="banner-action" href="#">Status page</a>  </p></div>

#Inverse and brand

banner-inverse is a band of ink, for the one announcement of a release. banner-brand wears the brand color, which the system keeps for one moment per view: with a brand banner on the page, no other brand surface belongs on it.

Inverse and brand

<dialog open class="banner banner-dismissible banner-inverse" aria-label="Announcement">  <p class="banner-content">    <span class="banner-title">Atlas 4 is here.</span>Faster builds and a new dashboard.    <a class="banner-action" href="#">Read the notes</a>  </p>  <form method="dialog" class="banner-close">    <button class="btn btn-quiet btn-square btn-sm" aria-label="Dismiss">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </form></dialog><dialog open class="banner banner-dismissible banner-brand" aria-label="Offer">  <p class="banner-content">    <span class="banner-title">Two months free</span>on any yearly plan until Friday.    <a class="banner-action" href="#">See plans</a>  </p>  <form method="dialog" class="banner-close">    <button class="btn btn-quiet btn-square btn-sm" aria-label="Dismiss">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </form></dialog>

#Accessibility

  • Give a banner a role that matches its urgency: role="status" for news that can wait, role="alert" for an incident, a labelled role="region" or the dialog's own role for an announcement. A banner present at load is not announced; only one added later is.
  • Write one sentence and at most one link. A banner that wraps to three lines on a phone has become a paragraph; move it into the page.
  • The dismiss button has a name. Its target reaches the band's edge.
  • Text keeps its contrast on every tone: status tones tint the band by a few percent and leave the words in ink.

#Classes

ClassDescription
bannerThe band: the tray tone with a hairline under it. An icon inside takes the band's tone
banner-dismissibleOn a dialog with open: the band folds away to nothing when the dialog closes. Without open it is not shown. With banner
banner-infoA faint tint and icon in the info hue. With banner
banner-successA faint tint and icon in the success hue. With banner
banner-warningA faint tint and icon in the warning hue. With banner
banner-dangerA faint tint and icon in the danger hue. With banner
banner-inverseA band of ink, for the one announcement of a release. With banner
banner-brandA band of the brand color; with it on the page, no other brand surface belongs in the view. With banner
banner-contentThe message, centered in the band: an icon, the words and one action, wrapping as a group
banner-titleThe few strong words that carry the news
banner-actionThe one link out, underlined in the band's text color
banner-closeThe form with method="dialog", or the button, that dismisses the banner, at the far end of the band

#Custom properties

PropertyDefaultDescription
--banner-tonevar(--color-text-strong)The icon color, and the hue of a tint
--banner-colorvar(--color-surface-tray)The band's color

#Source

@master/ui/components/banner.css
/* * Banner: one line across the top of a page or a panel, for something everyone on it should know. * It is a band of the tray tone with a hairline under it, quieter than an alert, which belongs to one task. * As a <dialog open> it can be dismissed without script: its close button submits a dialog form, and the band * folds away to nothing where the browser can interpolate to an automatic height. */@layer components {  .banner {    --banner-tone: var(--color-text-strong);    --banner-color: var(--color-surface-tray);    --icon-size: 1rem;    --icon-color: var(--banner-tone);    position: relative;    inset: auto;    display: grid;    grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);    align-items: center;    width: auto;    max-width: none;    height: auto;    max-height: none;    min-height: 2.5rem;    margin: 0;    padding: .5rem .75rem;    border: 0;    background: var(--banner-color);    color: var(--color-text-body);    box-shadow: inset 0 -.5px 0 var(--color-line-contour);    font-size: .8125rem;    line-height: 1.5;    /* Status tones tint the band and color its icon; the words stay in ink and say what the hue means. */    &.banner-info { --banner-tone: var(--color-info); }    &.banner-success { --banner-tone: var(--color-success); }    &.banner-warning { --banner-tone: var(--color-warning); }    &.banner-danger { --banner-tone: var(--color-danger); }    &:is(.banner-info, .banner-success, .banner-warning, .banner-danger) {      --banner-color: color-mix(in oklab, var(--banner-tone) 8%, var(--color-surface-base));      box-shadow: inset 0 -.5px 0 color-mix(in oklab, var(--banner-tone) 28%, transparent);    }    /* Inverse: a band of ink, for the one announcement of a release. */    &.banner-inverse {      --surface-color: var(--color-surface-inverse);      @apply --on-ink;      --banner-tone: var(--color-text-inverse);      --banner-color: var(--color-surface-inverse);      color: var(--color-text-inverse);      box-shadow: none;    }    /* Brand: the brand's face, kept for the moment the brand speaks. */    &.banner-brand {      --surface-color: var(--color-brand);      @apply --on-ink;      --banner-tone: var(--color-brand-ink);      --banner-color: var(--color-brand);      background: linear-gradient(180deg, color-mix(in oklab, var(--color-brand), white 10%), var(--color-brand)) var(--color-brand);      color: var(--color-brand-ink);      box-shadow: inset 0 -.5px 0 color-mix(in oklab, var(--color-brand), black 30%);    }    /*     * A dismissible banner is a <dialog open>. Closing it folds the band: height and padding run to zero while     * display waits for them. Without interpolate-size it leaves at once.     */    &.banner-dismissible {      overflow: clip;      interpolate-size: allow-keywords;      transition-property: height, min-height, padding-block, opacity, display;      transition-duration: var(--duration-base);      transition-timing-function: var(--easing-glide);      transition-behavior: allow-discrete;      &:not([open]) {        display: none;        height: 0;        min-height: 0;        padding-block: 0;        opacity: 0;      }      @media (prefers-reduced-motion: reduce) {        transition-property: opacity, display;        transition-duration: var(--duration-quick);      }    }    @media (forced-colors: active) {      border-bottom: 1px solid CanvasText;    }  }  /* The message, centered in the band: an icon, the words and one action, wrapping as a group. */  .banner-content {    grid-row: 1;    grid-column: 2;    display: flex;    flex-wrap: wrap;    align-items: center;    justify-content: center;    gap: .25rem .625rem;    min-width: 0;    margin: 0;    text-align: center;    text-wrap: balance;    overflow-wrap: anywhere;  }  /* The few words that carry the news. */  .banner-title {    color: var(--color-text-strong);    font-weight: var(--font-weight-control);    text-wrap: balance;  }  .banner-inverse .banner-title {    color: var(--color-text-inverse);  }  .banner-brand .banner-title {    color: var(--color-brand-ink);  }  /* One link out. It takes the band's text color and keeps a fine underline, so it reads on every tone. */  .banner-action {    border-radius: calc((var(--radius-chip) - 2px) * var(--corner-scale));    corner-shape: var(--corner-shape);    color: inherit;    font-weight: var(--font-weight-control);    white-space: nowrap;    text-decoration: underline;    text-decoration-thickness: .5px;    text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);    text-underline-offset: .22em;    transition: text-decoration-color var(--duration-quick) var(--easing-glide);    @media (hover: hover) {      &:hover {        text-decoration-color: currentColor;      }    }    /* Pressed: the full line, as under the pointer, so a tap is answered too. */    &:active {      text-decoration-color: currentColor;    }    &:focus-visible {      outline: 2px solid currentColor;      outline-offset: 2px;    }  }  .banner:not(.banner-inverse, .banner-brand) .banner-action {    color: var(--color-text-strong);  }  /* The dismiss form or button, at the far end of the band. Its button takes the band's ink. */  .banner-close {    --icon-color: currentColor;    grid-row: 1;    grid-column: 3;    justify-self: end;    display: flex;    margin-block: -.25rem;    margin-inline: .5rem -.25rem;    color: inherit;  }  .banner:is(.banner-inverse, .banner-brand) .banner-close .btn {    color: inherit;    @media (hover: hover) {      &:hover:not(:disabled) {        background: color-mix(in oklab, currentColor 14%, transparent);      }    }    &:focus-visible {      outline-color: currentColor;    }  }}