Skip to content

Snackbar

Snackbars inform users of a process that has or will be preformed. They shouldn't interrupt the user and you should be able to keep browsing without having to interact with it.

These Snackbar guidelines are a great way to get your head around the dos and don'ts.

Basics

The most common way to use a Snackbar is to position it in relation to the browser window. This solution leverages popover.

Popover text content

All changes saved

html
<!-- Button -->
<button class="button" popovertarget="snackbar1">Show snackbar 1</button>

<!-- Snackbar -->
<article popover="manual" id="snackbar1" class="snackbar" role="status">
  <div class="content">
    <p>Popover text content</p>
  </div>

  <div class="actions">
    <button class="button" popovertarget="snackbar1">Action</button>

    <button class="button" popovertarget="snackbar1">
      <span class="sr-only">Close</span>
      <svg></svg>
    </button>
  </div>
</article>
html
<!-- Button -->
<button class="button" popovertarget="snackbar2">Show snackbar 2</button>

<!-- Snackbar -->
<article id="snackbar2" popover="manual" class="snackbar" role="status">
  <div class="content">
    <p>All changes saved</p>
  </div>
</article>

Stacking Snackbars

Don't.

Position

Fixed

Position relative to the browser window. This is the default positioning behavior.

Use the positional classes in order to place the Snackbar. Default is .end-start.

All changes saved

Absolute

Work in progress!

This solution might get replaced with a cooler one using anchor-positioning.

In some edge-cases where the Snackbar might block or overlap other UI elements such as navigational elements it might be easier to absolute position the Snackbar instead of changing its inset values.

Differences from fixed Snackbar

  • Does not make use of popover.
  • Uses .visible class for visibility toggling.
  • Uses .absolute class for absolute positioning.
  • Needs Javascript to work.

Javascript is required

This solution does not leverage popover, so the Snackbar must be triggered with Javascript.

That's because when open, popover elements aren't influenced by a parents' position styling - popovers exist in the top layer.

All changes saved

html
<article class="snackbar absolute visible end-center" role="status">
  <div class="content">
    <p>All changes saved</p>
  </div>
</article>

Anatomy

  1. Container
  2. Content
  3. Action button (optional): may only contain a single action button
  4. Close button (optional)

Content

API

TypeModifiersDefaultDescription
Absolute&.absolute, &.visible-Optional classes needed for absolute positioning.
Actions& > .actions > <action button> / <icon button>-Optional action.
Container.snackbar-Container element.
Children& > .content, & > .actions-Optional child content.
Position.top-left, .top-center, .top-right, .bottom-left, .bottom-center, .bottom-right.bottom-leftDetermines where the Snackbar is positioned.

Browser compatibility

Installation

css
:where(.snackbar) {
  align-items: center;
  /* Inverse surface-filled */
  background-color: light-dark(var(--gray-14), var(--gray-1));
  border-radius: var(--surface-border-radius);
  box-shadow: var(--shadow-2);
  color: var(--text-color-1-contrast);
  display: flex;
  font-size: var(--font-size-sm);
  gap: var(--size-3);
  inset-block: auto var(--size-3);
  inset-inline: var(--size-3) auto;
  justify-content: space-between;
  min-inline-size: min(100%, 37ch);
  padding: var(--size-2) var(--size-3);
  position: fixed;
  inline-size: calc(100% - var(--size-6, 1.75rem));
  z-index: 100;

  &::backdrop {
    display: none;
  }

  * {
    word-break: break-word;
  }

  /* Global positioning (in relation to the window) */
  /*** Default (end-start) */
  inset-block: auto var(--size-3);
  inset-inline: 50% 0;
  translate: -50% 0;

  &.start-start,
  &.start-center,
  &.start-end {
    inset-block: var(--size-3) auto;
  }

  /* TODO use @custom-media instead? */
  @container (width > 480px) {
    /* Default (end-start) */
    inset-block: auto var(--size-7, 2rem);
    inset-inline: var(--size-7, 2rem) auto;
    translate: revert;
    inline-size: fit-content;

    &.end-start,
    &.end-end,
    &.start-start,
    &.start-end {
      translate: revert;
    }

    &.start-start {
      inset-block: var(--size-7, 2rem) auto;
      inset-inline: var(--size-7, 2rem) auto;
    }
    &.start-center {
      inset-block: var(--size-7, 2rem) auto;
      inset-inline: 50% 0;
      translate: -50% 0;
    }
    &.start-end {
      inset-block: var(--size-7, 2rem) auto;
      inset-inline: auto var(--size-7, 2rem);
    }
    &.end-start {
      inset-block-end: var(--size-7, 2rem);
      inset-inline: var(--size-7, 2rem) auto;
    }
    &.end-center {
      inset-block: auto var(--size-7, 2rem);
      inset-inline: 50% 0;
      translate: -50% 0;
    }
    &.end-end {
      inset-block: auto var(--size-7, 2rem);
      inset-inline: auto var(--size-7, 2rem);
    }
  }

  /* Absolute positioning */
  &.absolute {
    position: absolute;
  }

  /* Actions */
  .actions {
    align-items: center;
    display: flex;
    flex-shrink: 0;
    gap: var(--size-3);
    padding-inline: 0;

    button {
      /* Inverse hover and active backgrounds */
      &:where(:not(.disabled, [disabled])) {
        &:where(:not(:active):hover) {
          --_bg-color: light-dark(
            color-mix(in oklch, white 40%, black),
            color-mix(in oklch, white 85%, black)
          );
        }

        &:where(:hover:active) {
          --_bg-color: light-dark(
            color-mix(in oklch, white 45%, black),
            color-mix(in oklch, white 80%, black)
          );
        }
      }
    }

    button:not(:has(svg)) {
      border-radius: var(--surface-border-radius);
      font-size: inherit;
      max-block-size: var(--size-6, 1.5rem);
      padding: 1ex;
    }

    button:has(svg) {
      color: inherit;
      max-inline-size: var(--size-6, 1.75rem);
      margin: var(--size-00);
      padding: var(--size-1);
    }
  }

  /* Animations */
  opacity: 0;
  transition:
    display 0.075s allow-discrete,
    overlay 0.075s allow-discrete,
    opacity 0.075s var(--ease-out-1);

  &:popover-open,
  &:popover-open::backdrop,
  &.visible {
    opacity: 1;
    transition:
      display 0.25s allow-discrete,
      overlay 0.25s allow-discrete,
      opacity 0.25s var(--ease-out-1);

    @starting-style {
      opacity: 0;
    }
  }
}