Notification List

Notifications 2 new

@use 'sprucecss/scss/spruce' as *;

.notification-list {
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  overflow: hidden;

  &__header {
    align-items: center;
    border-block-end: config('border-width-base', $display) solid color('border');
    display: flex;
    gap: spacer('s');
    justify-content: space-between;
    padding: spacer('s') spacer('m');
  }

  &__title {
    align-items: center;
    display: flex;
    font-size: font-size('h5');
    gap: spacer('xs');
    margin: 0;
  }

  &__count {
    background-color: color('primary');
    border-radius: radius('pill');
    color: color('primary-foreground', 'btn');
    font-family: config('font-family-base', $typography);
    font-size: config('font-size-sm', $typography);
    padding: 0 spacer('xs');
  }

  &__items {
    @include clear-list;
  }

  &__item {
    align-items: flex-start;
    display: flex;
    gap: spacer('s');
    margin: 0 !important;
    padding: spacer('s') spacer('m');
    position: relative;
    @include a11y-card-link('.notification-list__link');

    & + & {
      border-block-start: config('border-width-base', $display) solid color('border');
    }

    &:hover {
      background-color: color('surface-muted');
    }

    &--unread {
      background-color: color-alpha('primary', 0.06);

      .notification-list__link {
        font-weight: 700;
      }

      .notification-list__time::after {
        background-color: color('primary');
        block-size: 0.5rem;
        border-radius: 50%;
        content: '';
        display: inline-block;
        inline-size: 0.5rem;
        margin-inline-start: spacer('xs');
      }
    }
  }

  &__icon {
    --icon-color: #{color('info', 'alert')};
    background-color: color-mix(in srgb, var(--icon-color) 15%, transparent);
    block-size: 2.25rem;
    border-radius: radius('md');
    flex-shrink: 0;
    inline-size: 2.25rem;
    position: relative;

    &::after {
      background-color: var(--icon-color);
      block-size: 0.75rem;
      border-radius: 50%;
      content: '';
      inline-size: 0.75rem;
      inset: 0;
      margin: auto;
      position: absolute;
    }

    @each $type in ('success', 'warning', 'danger', 'info') {
      &--#{$type} {
        --icon-color: #{color($type, 'alert')};
      }
    }
  }

  &__content {
    flex-grow: 1;
    min-inline-size: 0;
  }

  &__link {
    color: color('heading');
    font-weight: 600;
    text-decoration: none;

    &:focus-visible {
      outline: none;

      &::before {
        outline: 2px solid color('focus-ring');
        outline-offset: -2px;
      }
    }
  }

  &__text {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
    margin: 0;
  }

  &__time {
    align-items: center;
    color: color('muted');
    display: flex;
    flex-shrink: 0;
    font-size: config('font-size-sm', $typography);
  }
}
<section class="notification-list" aria-labelledby="notifications-title">
    <header class="notification-list__header">
        <h2 class="notification-list__title" id="notifications-title">Notifications <span class="notification-list__count">2 new</span></h2>
        <button class="btn btn--ghost-primary btn--sm" type="button" data-mark-read>Mark all as read</button>
    </header>
    <ul class="notification-list__items" role="list">
        <li class="notification-list__item notification-list__item--unread">
            <span class="notification-list__icon notification-list__icon--success" aria-hidden="true"></span>
            <div class="notification-list__content">
                <a class="notification-list__link" href="#"><span class="sr-only">Unread: </span>Deployment finished</a>
                <p class="notification-list__text">spruce-docs was deployed to production.</p>
            </div>
            <time class="notification-list__time">5m</time>
        </li>
        <li class="notification-list__item notification-list__item--unread">
            <span class="notification-list__icon notification-list__icon--info" aria-hidden="true"></span>
            <div class="notification-list__content">
                <a class="notification-list__link" href="#"><span class="sr-only">Unread: </span>New comment</a>
                <p class="notification-list__text">Maya replied to “Design styles in v3”.</p>
            </div>
            <time class="notification-list__time">1h</time>
        </li>
        <li class="notification-list__item">
            <span class="notification-list__icon notification-list__icon--warning" aria-hidden="true"></span>
            <div class="notification-list__content">
                <a class="notification-list__link" href="#">Usage at 80%</a>
                <p class="notification-list__text">Your team used 8 of 10 GB storage.</p>
            </div>
            <time class="notification-list__time">3h</time>
        </li>
        <li class="notification-list__item">
            <span class="notification-list__icon notification-list__icon--danger" aria-hidden="true"></span>
            <div class="notification-list__content">
                <a class="notification-list__link" href="#">Build failed</a>
                <p class="notification-list__text">The preview build of #241 failed.</p>
            </div>
            <time class="notification-list__time">1d</time>
        </li>
    </ul>
</section>
(() => {
  document.querySelectorAll('.notification-list').forEach((list) => {
    list.querySelector('[data-mark-read]')?.addEventListener('click', () => {
      list.querySelectorAll('.notification-list__item--unread').forEach((item) => {
        item.classList.remove('notification-list__item--unread');
        item.querySelector('.sr-only')?.remove();
      });

      list.querySelector('.notification-list__count')?.remove();
    });
  });
})();

Notification List

On this page

A notification feed with unread markers, status icons, relative times and a "Mark all as read" action.

Technical Details

  • Each row is fully clickable through the a11y-card-link mixin; the focus ring is drawn on the whole row.
  • Unread items are bold and marked with a dot; screen readers get a hidden "Unread:" prefix (the dot is decorative).
  • "Mark all as read" removes the unread state and updates the counter.
  • The status colors come from the alert color group, mixed with color-mix() for the icon background.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.