Alert banners
Four inline notices (info, success, warning and danger), each with a bold first phrase, one sentence, an optional action and a dismiss button.
New. yuki_h, kwesi.frames and 212 others liked your photo.
New. arash.t started following you.
leilani.k mentioned you: @rafal.makes is this the roof by the river?
henrik.sb started following you.
mireille.v liked your comment: The fog did the rest.
ximena.q started following you.
kwesi.frames mentioned you in their story.
Nothing here yet.
It is live: hover, focus and its own controls respond. Actions that would change data (approve, send, delete) are for your app to wire. The width is a real 390 px, 768 px or 1280 px box, shrunk to fit your screen.
<!-- Icons: Lucide (lucide.dev), ISC licence -->
<style>
.odl-activity-list { box-sizing: border-box; inline-size: 100%; max-inline-size: 34rem; margin-inline: auto; container-type: inline-size; color: var(--ds-color-text-primary); font: var(--ds-text-body-md); text-wrap: pretty; }
.odl-activity-list *, .odl-activity-list *::before, .odl-activity-list *::after { box-sizing: border-box; }
.odl-activity-list .odl-card { display: grid; gap: var(--ds-space-stack-lg); padding: var(--ds-space-inset-xl); background: var(--ds-color-surface-raised);
border: var(--ds-border-width-default) solid var(--ds-color-border-subtle); border-radius: var(--ds-radius-container); box-shadow: var(--ds-elevation-raised); }
.odl-activity-list .odl-head { display: grid; gap: var(--ds-space-stack-md); }
.odl-activity-list h3 { margin: 0; font: var(--ds-text-title-md); }
/* Filter chips: one scrolling row; the radio's hit area reaches past the chip's visible height to the touch floor. */
.odl-activity-list .odl-filter { display: flex; gap: var(--ds-space-inline-sm); margin: 0 calc(-1 * var(--ds-space-inset-xl)); padding: var(--ds-space-4) var(--ds-space-inset-xl); border: 0; min-inline-size: 0; overflow-x: auto; scrollbar-width: none; overscroll-behavior-x: contain;
-webkit-mask-image: linear-gradient(to right, var(--ds-color-black) calc(100% - var(--ds-space-32)), transparent); mask-image: linear-gradient(to right, var(--ds-color-black) calc(100% - var(--ds-space-32)), transparent); }
.odl-activity-list .odl-filter::-webkit-scrollbar { display: none; }
.odl-activity-list .odl-filter label { position: relative; flex: none; }
.odl-activity-list .odl-filter input { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; margin: 0; opacity: 0; cursor: pointer; }
.odl-activity-list .odl-filter span { display: inline-flex; align-items: center; min-block-size: var(--ds-size-control-md); padding: 0 var(--ds-space-inset-md); border-radius: var(--ds-radius-control);
border: var(--ds-border-width-default) solid var(--ds-color-border-default); color: var(--ds-color-text-secondary); font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-label); white-space: nowrap;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard), color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-activity-list .odl-filter label:hover span { background: var(--ds-color-bg-neutral-subtle); }
.odl-activity-list .odl-filter input:checked + span { border-color: transparent; background: var(--ds-color-bg-inverse); color: var(--ds-color-text-inverse); }
.odl-activity-list .odl-filter input:focus-visible + span { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
.odl-activity-list .odl-groups { display: grid; gap: var(--ds-space-stack-lg); }
.odl-activity-list .odl-group { display: grid; gap: var(--ds-space-stack-sm); }
.odl-activity-list .odl-group[hidden] { display: none; }
.odl-activity-list h4 { margin: 0; font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-heading); color: var(--ds-color-text-primary); }
.odl-activity-list ul { display: grid; gap: var(--ds-space-stack-md); margin: 0; padding: 0; list-style: none; }
.odl-activity-list .odl-item { position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--ds-space-inline-md);
min-block-size: var(--ds-size-row-lg); padding-block: var(--ds-space-stack-xs); }
.odl-activity-list .odl-item[hidden] { display: none; }
/* New: a small accent dot after the time, inside the row; the row itself stays neutral. */
.odl-activity-list .odl-item.is-new time::after { content: ""; display: inline-block; inline-size: var(--ds-space-8); block-size: var(--ds-space-8); margin-inline-start: var(--ds-space-6); border-radius: var(--ds-radius-full); background: var(--ds-color-bg-accent-bold); vertical-align: middle; }
.odl-activity-list .odl-who { position: relative; display: grid; inline-size: var(--ds-size-control-lg); block-size: var(--ds-size-control-lg); }
.odl-activity-list .odl-who img { display: block; inline-size: 100%; block-size: 100%; border-radius: var(--ds-radius-person); object-fit: cover; background: var(--ds-color-bg-neutral-subtle); }
/* Two people: two smaller photos, the front one cut out from the back one by a ring of the card's colour. */
.odl-activity-list .odl-who.is-pair img { position: absolute; inline-size: 68%; block-size: 68%; }
.odl-activity-list .odl-who.is-pair img:first-child { inset-block-start: 0; inset-inline-start: 0; }
.odl-activity-list .odl-who.is-pair img + img { inset-block-end: 0; inset-inline-end: 0; box-shadow: 0 0 0 var(--ds-border-width-emphasis) var(--ds-color-surface-raised); }
.odl-activity-list .odl-kind { position: absolute; inset-inline-end: calc(-1 * var(--ds-space-2)); inset-block-end: calc(-1 * var(--ds-space-2)); display: grid; place-items: center;
inline-size: var(--ds-size-icon-lg); block-size: var(--ds-size-icon-lg); border-radius: var(--ds-radius-full); border: var(--ds-border-width-emphasis) solid var(--ds-color-surface-raised);
color: var(--ds-color-text-inverse); background: var(--ds-color-bg-inverse); }
.odl-activity-list .odl-kind svg { inline-size: 58%; block-size: 58%; stroke-width: var(--ds-icon-stroke-lg); }
.odl-activity-list .odl-kind.is-like svg { fill: currentColor; }
.odl-activity-list .odl-text { margin: 0; color: var(--ds-color-text-primary); }
.odl-activity-list .odl-text b { font-weight: var(--ds-font-weight-heading); }
.odl-activity-list .odl-text q { color: var(--ds-color-text-secondary); quotes: "\201C" "\201D"; }
.odl-activity-list .odl-text time { color: var(--ds-color-text-tertiary); white-space: nowrap; font-variant-numeric: tabular-nums; }
.odl-activity-list .odl-thumb { display: block; inline-size: var(--ds-size-control-lg); block-size: var(--ds-size-control-lg); border-radius: var(--ds-radius-detail); object-fit: cover; background: var(--ds-color-bg-neutral-subtle); }
.odl-activity-list .odl-story { display: grid; inline-size: var(--ds-size-control-lg); block-size: var(--ds-size-control-lg); padding: var(--ds-border-width-emphasis); border-radius: var(--ds-radius-person);
border: var(--ds-border-width-emphasis) solid var(--ds-color-border-accent); }
.odl-activity-list .odl-story img { display: block; inline-size: 100%; block-size: 100%; border-radius: var(--ds-radius-person); object-fit: cover; }
/* Follow back: the accent fill; Following: a plain grey one. The hit area reaches the touch floor past the visible height. */
.odl-activity-list .odl-back { position: relative; display: inline-flex; align-items: center; justify-content: center; min-inline-size: calc(var(--ds-size-control-lg) * 2); min-block-size: var(--ds-size-control-sm);
padding: 0 var(--ds-space-inset-md); border: 0; border-radius: var(--ds-radius-control); background: var(--ds-color-bg-accent-bold); color: var(--ds-color-text-on-accent);
font: var(--ds-text-label-md); font-weight: var(--ds-font-weight-label); white-space: nowrap; cursor: pointer; transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-activity-list .odl-back::after { content: ""; position: absolute; inset: calc((var(--ds-size-control-sm) - var(--ds-size-target-touch)) / 2) 0; }
.odl-activity-list .odl-back:hover { background: var(--ds-color-bg-accent-bold-hover); }
.odl-activity-list .odl-back[aria-pressed="true"] { background: var(--ds-color-bg-neutral-subtle); color: var(--ds-color-text-primary); }
.odl-activity-list .odl-back[aria-pressed="true"]:hover { background: var(--ds-color-bg-neutral-subtle-hover); }
.odl-activity-list .odl-back:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
.odl-activity-list .odl-empty { margin: 0; padding: var(--ds-space-inset-xl) 0; color: var(--ds-color-text-secondary); text-align: center; }
.odl-activity-list .odl-sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@container (inline-size < 28rem) {
.odl-activity-list .odl-filter { margin-inline: calc(-1 * var(--ds-space-inset-lg)); padding-inline: var(--ds-space-inset-lg); }
.odl-activity-list .odl-filter span { padding-inline: var(--ds-space-inset-sm); }
.odl-activity-list .odl-back { min-inline-size: 0; padding-inline: var(--ds-space-inset-sm); }
.odl-activity-list .odl-card { padding: var(--ds-space-inset-lg); }
}
@media (prefers-reduced-motion: reduce) { .odl-activity-list, .odl-activity-list *, .odl-activity-list *::before { transition: none !important; animation: none !important; } }
</style>
<section class="odl-activity-list" aria-labelledby="odl-al-title">
<div class="odl-card">
<div class="odl-head">
<h3 id="odl-al-title">Activity</h3>
<fieldset class="odl-filter"><legend class="odl-sr">Show</legend>
<label><input type="radio" name="odl-al-kind" value="all" checked><span>All</span></label>
<label><input type="radio" name="odl-al-kind" value="like"><span>Likes</span></label>
<label><input type="radio" name="odl-al-kind" value="follow"><span>Follows</span></label>
<label><input type="radio" name="odl-al-kind" value="mention"><span>Mentions</span></label>
</fieldset>
</div>
<div class="odl-groups">
<section class="odl-group" aria-labelledby="odl-al-today"><h4 id="odl-al-today">Today</h4>
<ul>
<li class="odl-item is-new" data-kind="like"><span class="odl-who is-pair" aria-hidden="true"><img src="https://images.unsplash.com/photo-1684598273404-2d94d7a7ce2f?w=72&h=72&fit=crop&auto=format&q=70" alt="" width="36" height="36" loading="lazy" decoding="async"><img src="https://images.unsplash.com/photo-1596580817363-a4a8f67d4bc8?w=72&h=72&fit=crop&auto=format&q=70" alt="" width="36" height="36" loading="lazy" decoding="async"><span class="odl-kind is-like"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span></span>
<p class="odl-text"><span class="odl-sr">New. </span><b>yuki_h</b>, <b>kwesi.frames</b> and 212 others liked your photo. <time datetime="PT2H">2h</time></p><img class="odl-thumb" src="https://images.unsplash.com/photo-1512531123205-560f5974e686?w=96&h=96&fit=crop&auto=format&q=70" alt="Your rooftop sunset photo" width="48" height="48" loading="lazy" decoding="async"></li>
<li class="odl-item is-new" data-kind="follow"><span class="odl-who" aria-hidden="true"><img src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-kind"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/></svg></span></span>
<p class="odl-text"><span class="odl-sr">New. </span><b>arash.t</b> started following you. <time datetime="PT3H">3h</time></p><button type="button" class="odl-back" aria-pressed="false" aria-label="Follow back arash.t">Follow back</button></li>
<li class="odl-item" data-kind="mention"><span class="odl-who" aria-hidden="true"><img src="https://images.unsplash.com/photo-1534751516642-a1af1ef26a56?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-kind"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg></span></span>
<p class="odl-text"><b>leilani.k</b> mentioned you: <q>@rafal.makes is this the roof by the river?</q> <time datetime="PT5H">5h</time></p><img class="odl-thumb" src="https://images.unsplash.com/photo-1505490096310-204ef067fe6b?w=96&h=96&fit=crop&auto=format&q=70" alt="Your river photo" width="48" height="48" loading="lazy" decoding="async"></li>
</ul>
</section>
<section class="odl-group" aria-labelledby="odl-al-week"><h4 id="odl-al-week">This week</h4>
<ul>
<li class="odl-item" data-kind="follow"><span class="odl-who" aria-hidden="true"><img src="https://images.unsplash.com/photo-1600603406200-5b2a104684ac?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-kind"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/></svg></span></span>
<p class="odl-text"><b>henrik.sb</b> started following you. <time datetime="P2D">2d</time></p><button type="button" class="odl-back" aria-pressed="true" aria-label="Follow back henrik.sb">Following</button></li>
<li class="odl-item" data-kind="like"><span class="odl-who" aria-hidden="true"><img src="https://images.unsplash.com/photo-1607569708758-0270aa4651bd?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-kind is-like"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span></span>
<p class="odl-text"><b>mireille.v</b> liked your comment: <q>The fog did the rest.</q> <time datetime="P3D">3d</time></p><img class="odl-thumb" src="https://images.unsplash.com/photo-1578429827733-272eb6d7972e?w=96&h=96&fit=crop&auto=format&q=70" alt="The foggy road post you commented on" width="48" height="48" loading="lazy" decoding="async"></li>
<li class="odl-item" data-kind="follow"><span class="odl-who" aria-hidden="true"><img src="https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-kind"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/></svg></span></span>
<p class="odl-text"><b>ximena.q</b> started following you. <time datetime="P4D">4d</time></p><button type="button" class="odl-back" aria-pressed="false" aria-label="Follow back ximena.q">Follow back</button></li>
<li class="odl-item" data-kind="mention"><span class="odl-who" aria-hidden="true"><img src="https://images.unsplash.com/photo-1596580817363-a4a8f67d4bc8?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-kind"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg></span></span>
<p class="odl-text"><b>kwesi.frames</b> mentioned you in their story. <time datetime="P5D">5d</time></p><span class="odl-story"><img src="https://images.unsplash.com/photo-1609194246158-702022cc4f13?w=96&h=96&fit=crop&auto=format&q=70" alt="kwesi.frames's story: a city skyline at sunset" width="48" height="48" loading="lazy" decoding="async"></span></li>
</ul>
</section>
<p class="odl-empty" hidden>Nothing here yet.</p>
</div>
</div>
</section>
<script>
document.querySelectorAll(".odl-activity-list").forEach((root) => {
if (root.dataset.odlReady) return; // wire once, even if the file is pasted twice
root.dataset.odlReady = "1";
const items = root.querySelectorAll(".odl-item"), groups = root.querySelectorAll(".odl-group"), empty = root.querySelector(".odl-empty");
root.querySelectorAll(".odl-filter input").forEach((r) => r.addEventListener("change", () => {
items.forEach((li) => { li.hidden = r.value !== "all" && li.dataset.kind !== r.value; });
let any = false;
groups.forEach((g) => { const has = !!g.querySelector(".odl-item:not([hidden])"); g.hidden = !has; any = any || has; });
empty.hidden = any;
}));
root.querySelectorAll(".odl-back").forEach((b) => b.addEventListener("click", () => {
const on = b.getAttribute("aria-pressed") !== "true";
b.setAttribute("aria-pressed", String(on)); b.textContent = on ? "Following" : "Follow back";
}));
});
</script>
Four inline notices (info, success, warning and danger), each with a bold first phrase, one sentence, an optional action and a dismiss button.
What a list shows when there is nothing in it: a small drawing in the system's own colours, what it means, when that changes, and one thing to do.
Short notices that stack in a corner: done with an undo, in progress with a bar, and failed with a retry that keeps the person's work.