Activity Log

Activity log

  1. Anna Smith changed the role of Ben King from Viewer to Admin

    Team event, 84.2.11.4 · Chrome on macOS

  2. Ben King refunded order #10478 ($129.00)

    Order event, 91.82.4.17 · Firefox on Windows

  3. Lea Moreau turned on two-factor authentication

    Security event, 188.6.2.90 · Safari on iOS

  1. Lea Moreau updated the price of Everyday Backpack from $95.00 to $89.00

    Product event, 188.6.2.90 · Safari on macOS

  2. Failed sign-in attempt for ben@acme.com

    Security event, 203.0.113.5 · Unknown device

  3. Jon Diaz fulfilled 12 orders

    Order event, 84.2.11.4 · Chrome on Windows

  1. Anna Smith invited jon.diaz@example.com as Viewer

    Team event, 84.2.11.4 · Chrome on macOS

Show older activity
@use 'sprucecss/scss/spruce' as *;

.activity-log {
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  padding: spacer('m');
  @include layout-stack('m');

  &__header {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
    justify-content: space-between;
  }

  &__title {
    font-size: font-size('h5');
    margin: 0;
  }

  &__filter {
    @include layout-cluster('xs');
  }

  &__days {
    @include layout-stack('m');
  }

  &__date {
    color: color('muted');
    font-family: inherit;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
  }

  &__items {
    list-style: none;
    padding: 0;
  }

  &__item {
    display: grid;
    gap: spacer('s');
    grid-template-columns: 2rem minmax(0, 1fr);
    margin: 0 !important;
    padding-block-end: spacer('s');
    position: relative;

    // The line that connects the icons.
    &:not(:last-child)::before {
      background-color: color('border');
      content: '';
      inline-size: 1px;
      inset-block: 2.25rem 0.25rem;
      inset-inline-start: calc(1rem - 0.5px);
      position: absolute;
    }
  }

  &__icon {
    align-items: center;
    aspect-ratio: 1;
    background-color: color('surface-muted');
    border: config('border-width-base', $display) solid color('border');
    border-radius: radius('pill');
    color: color('muted');
    display: inline-flex;
    inline-size: 2rem;
    justify-content: center;

    svg {
      block-size: 1rem;
      inline-size: 1rem;
    }

    &--order {
      color: color('primary');
    }

    &--security {
      color: color('danger', 'alert');
    }
  }

  &__day {
    @include layout-stack('xs');
  }

  &__body {
    padding-block-start: 0.2rem;
    @include layout-stack('xxs');
  }

  &__text {
    line-height: config('line-height-md', $typography);

    strong {
      color: color('heading');
    }
  }

  &__meta {
    color: color('muted');
    display: flex;
    flex-wrap: wrap;
    font-size: 0.8125rem;
    gap: 0 spacer('xs');

    time + span::before {
      content: '·';
      margin-inline-end: spacer('xs');
    }
  }

  &__empty {
    background-color: color('surface-muted');
    border-radius: radius('md');
    color: color('muted');
    padding: spacer('m');
    text-align: center;
  }
}
<section class="activity-log" aria-labelledby="activity-log-title" data-activity-log>
    <header class="activity-log__header">
        <h3 class="activity-log__title" id="activity-log-title">Activity log</h3>
        <form class="activity-log__filter" action="#" method="get">
            <label class="sr-only" for="activity-log-type">Show</label>
            <select class="form-control form-control--sm" id="activity-log-type" name="type" data-activity-filter>
                <option value="all">All activity</option>
                <option value="order">Orders</option>
                <option value="product">Products</option>
                <option value="team">Team</option>
                <option value="security">Security</option>
            </select>
            <button class="btn btn--outline-secondary btn--sm" type="submit" data-activity-submit>Filter</button>
        </form>
    </header>
    <p class="sr-only" aria-live="polite" data-activity-live></p>
    <div class="activity-log__days">
        <section class="activity-log__day" aria-labelledby="activity-2026-10-04" data-activity-day>
            <h4 class="activity-log__date" id="activity-2026-10-04"><time datetime="2026-10-04">Today</time></h4>
            <ol class="activity-log__items" role="list">
                <li class="activity-log__item" data-activity-type="team">
                    <span class="activity-log__icon activity-log__icon--team" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text"><strong>Anna Smith</strong> changed the role of <strong>Ben King</strong> from Viewer to Admin</p>
                        <p class="activity-log__meta"><span class="sr-only">Team event, </span><time datetime="2026-10-04T09:12">9:12 AM</time><span>84.2.11.4 · Chrome on macOS</span></p>
                    </div>
                </li>
                <li class="activity-log__item" data-activity-type="order">
                    <span class="activity-log__icon activity-log__icon--order" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text"><strong>Ben King</strong> refunded <a href="#">order #10478</a> ($129.00)</p>
                        <p class="activity-log__meta"><span class="sr-only">Order event, </span><time datetime="2026-10-04T08:47">8:47 AM</time><span>91.82.4.17 · Firefox on Windows</span></p>
                    </div>
                </li>
                <li class="activity-log__item" data-activity-type="security">
                    <span class="activity-log__icon activity-log__icon--security" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text"><strong>Lea Moreau</strong> turned on two-factor authentication</p>
                        <p class="activity-log__meta"><span class="sr-only">Security event, </span><time datetime="2026-10-04T07:30">7:30 AM</time><span>188.6.2.90 · Safari on iOS</span></p>
                    </div>
                </li>
            </ol>
        </section>
        <section class="activity-log__day" aria-labelledby="activity-2026-10-03" data-activity-day>
            <h4 class="activity-log__date" id="activity-2026-10-03"><time datetime="2026-10-03">Yesterday</time></h4>
            <ol class="activity-log__items" role="list">
                <li class="activity-log__item" data-activity-type="product">
                    <span class="activity-log__icon activity-log__icon--product" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text"><strong>Lea Moreau</strong> updated the price of <a href="#">Everyday Backpack</a> from $95.00 to $89.00</p>
                        <p class="activity-log__meta"><span class="sr-only">Product event, </span><time datetime="2026-10-03T16:05">4:05 PM</time><span>188.6.2.90 · Safari on macOS</span></p>
                    </div>
                </li>
                <li class="activity-log__item" data-activity-type="security">
                    <span class="activity-log__icon activity-log__icon--security" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text">Failed sign-in attempt for <strong>ben@acme.com</strong></p>
                        <p class="activity-log__meta"><span class="sr-only">Security event, </span><time datetime="2026-10-03T11:58">11:58 AM</time><span>203.0.113.5 · Unknown device</span></p>
                    </div>
                </li>
                <li class="activity-log__item" data-activity-type="order">
                    <span class="activity-log__icon activity-log__icon--order" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text"><strong>Jon Diaz</strong> fulfilled 12 orders</p>
                        <p class="activity-log__meta"><span class="sr-only">Order event, </span><time datetime="2026-10-03T10:20">10:20 AM</time><span>84.2.11.4 · Chrome on Windows</span></p>
                    </div>
                </li>
            </ol>
        </section>
        <section class="activity-log__day" aria-labelledby="activity-2026-09-30" data-activity-day>
            <h4 class="activity-log__date" id="activity-2026-09-30"><time datetime="2026-09-30">September 30</time></h4>
            <ol class="activity-log__items" role="list">
                <li class="activity-log__item" data-activity-type="team">
                    <span class="activity-log__icon activity-log__icon--team" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>
                    <div class="activity-log__body">
                        <p class="activity-log__text"><strong>Anna Smith</strong> invited <strong>jon.diaz@example.com</strong> as Viewer</p>
                        <p class="activity-log__meta"><span class="sr-only">Team event, </span><time datetime="2026-09-30T14:41">2:41 PM</time><span>84.2.11.4 · Chrome on macOS</span></p>
                    </div>
                </li>
            </ol>
        </section>
        <p class="activity-log__empty" data-activity-empty hidden>No activity of this type in this period.</p>
    </div>
    <a class="btn btn--ghost-secondary btn--sm btn--block" href="#">Show older activity</a>
</section>
(() => {
  document.querySelectorAll('[data-activity-log]').forEach((log) => {
    const filter = log.querySelector('[data-activity-filter]');
    const items = [...log.querySelectorAll('[data-activity-type]')];
    const days = [...log.querySelectorAll('[data-activity-day]')];
    const empty = log.querySelector('[data-activity-empty]');
    const live = log.querySelector('[data-activity-live]');

    // The filter applies instantly, so the submit button (the no-JavaScript fallback) is not needed.
    log.querySelector('[data-activity-submit]').hidden = true;

    filter.addEventListener('change', () => {
      const type = filter.value;
      let visible = 0;

      items.forEach((item) => {
        item.hidden = type !== 'all' && item.dataset.activityType !== type;
        visible += item.hidden ? 0 : 1;
      });

      // Hide the days without a visible event.
      days.forEach((day) => {
        day.hidden = !day.querySelector('[data-activity-type]:not([hidden])');
      });

      empty.hidden = visible > 0;
      live.textContent = `${visible} ${visible === 1 ? 'event' : 'events'} shown.`;
    });
  });
})();

Activity Log

An audit log of who did what and when, grouped by day, with the type of the event, the IP address and the device, and an instant filter.

Anatomy

  • .activity-log__filter: a small GET form with the event type select.
  • .activity-log__day: a section per day, labelled by its date, with an ordered list of __items.
  • .activity-log__item: the type __icon and the __body with the __text (actor, action, object) and the __meta (time, IP address, device).

Accessibility and Usability

  • The days are labelled sections with a time element, so screen reader users can jump between them, and machines can read the dates.
  • The type of each event is hidden text ("Security event, 7:30 AM"), because the icon is decorative.
  • The filter result is announced in a live region ("2 events shown"); when nothing matches, an empty message is shown instead of a blank area.
  • Write the entries in plain language with the names of the people and objects, and link the objects (orders, products) to their pages.

Technical Details

  • Progressive enhancement: without JavaScript, the filter form submits (?type=security) and the server returns the filtered log. With JavaScript, the "Filter" button is hidden and the list filters instantly; the days without a matching event are hidden.
  • "Show older activity" is a link to the next page; replace it with a "load more" request if you prefer.
  • Keep the security events (failed sign-ins, two-factor changes) visible to the account owners; they are the reason audit logs exist.

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.