Design Lounge
Install

Nº 056 of 520

PWA news reader

A paper-toned news front with serif headlines: scrolling section chips, a lead card with a CSS-illustrated landscape, a story list with reading times and saved-for-offline marks, and a bottom nav.

Preview

Open full size
390 × 844 · 11.7 KB · 116 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "PWA news reader" (piece pwa-news-reader) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

232 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The front page of “Meridian”, an installed news PWA. Cream paper, a 30px Newsreader masthead, a horizontally scrolling row of section chips, a lead card whose image area is a CSS-drawn landscape (sky gradient, sun disc, two hills), and a hairline-separated list of six stories with 17px serif headlines, reading time and a green “saved” mark for anything cached for offline. Chips filter the list with a 280ms dip; bookmark buttons toggle the saved state and update the badge on the Saved tab. The detail worth copying is the offline affordance: saved stories say so inline in the meta line, in the same colour as the bookmark, not with a separate icon column.

Reference behaviour

  1. Initial state: “Top stories” chip pressed; lead card (Climate) with “saved”; six stories, two of them saved; Saved tab badge reads 3 (lead + 2).
  2. Swipe the chip row: it scrolls horizontally with the scrollbar hidden and proximity snap on each chip.
  3. Tap a chip (e.g. “Technology”): the previously pressed chip returns to outline; the tapped one fills --ink with paper text (160ms). The list dips to opacity 0 / 6px down over 280ms. At 280ms, stories not in that section are hidden, the section label changes to “Technology · 1 story”, and the list returns.
  4. Tap “Top stories” to show all six again; label returns to “Latest”.
  5. Tap a story’s bookmark (40px round button, right column): aria-pressed flips; the icon fills --saved green; a ”· saved” fragment with a download-check icon is appended to (or removed from) the story’s meta line; the Saved badge recounts.
  6. Tap a bottom-nav item: it becomes aria-current="page" (ink colour); others stay --ink-3. Only visual in this piece.
  7. The lead card is a single link; tapping it would open the article (out of scope).

Structure

390 × 844
┌────────────────────────────────────┐
│ (54px clearance)                   │
│ Meridian        Tuesday 29 Sep · … │  masthead serif 30
│ (Top stories)(World)(Climate)(Tec… │  chips 36px, horizontal scroll
│ ╭────────────────────────────────╮ │
│ │  sky gradient · sun · hills    │ │  art 190px, CSS only
│ │────────────────────────────────│ │
│ │ CLIMATE                        │ │  kicker 11px red
│ │ The valley that moved its      │ │  h2 serif 26px
│ │ river back                     │ │
│ │ Forty years after it was…      │ │  dek 14px
│ │ Ines Kalder · 9 min read · ⇩saved │
│ ╰────────────────────────────────╯ │
│ LATEST                             │
│ ───────────────────────────────── │
│ [thumb] Nord coalition talks…   ▮  │  story: 72px thumb, serif 17px, bookmark
│         World · 4 min · ⇩saved     │
│ ───────────────────────────────── │
│ [thumb] Orbital's tiny chip…    ▯  │
│         Technology · 6 min         │
│ ───────────────────────────────── │
│  ... (4 more)                      │
│ ▤ Today   ≡ Sections   ▮ Saved³   ○ Me │  nav 64px
│ (34px clearance)                   │
└────────────────────────────────────┘
  • <main> — absolute, inset: 0 0 98px, scrolls; padding-top:54px.
    • <header class="mast"> — <h1> + date span.
    • .chips (role="group" aria-label="Sections") — six <button aria-pressed data-cat>.
    • <a class="lead"> — .art (with one <i> for the second hill) + .txt (kicker, <h2>, dek, .meta).
    • <p class="sec"> section label.
    • <ul class="list"> of <li class="story" data-cat>: .thumb (three custom properties --c1/--c2/--c3), .body (<h3>, .meta), <button class="save" aria-pressed>.
  • <nav class="nav" aria-label="Primary"> — four buttons; the Saved one holds a .n badge.

Sample content:

  • Masthead: “Meridian” / “Tuesday 29 Sep · 08:14”. Chips: Top stories (all), World, Climate, Technology, Culture, Sport.
  • Lead: kicker “Climate”; headline “The valley that moved its river back”; dek “Forty years after it was straightened, the Tessel is being allowed to meander again — and the floods have stopped.”; meta “Ines Kalder · 9 min read · saved”.
StorySectionReadSavedThumb --c1/--c2/--c3
Nord coalition talks stall over fisheries quotaWorld4 minyes#D9DFE6 / #7B8EA3 / #3F5468
Orbital’s tiny chip promises a week of battery — with caveatsTechnology6 minno#E6DFD2 / #B89D6C / #A83A2A
Marrow Theatre reopens with a play performed in the darkCulture5 minyes#E9D6D2 / #C0716A / #5C2B26
Halden win the derby on a goal nobody saw comingSport3 minno#D6E4D7 / #5F9A6A / #F0C33C
Why September’s heat felt different this yearClimate7 minno#DFE6D3 / #8A9A6A / #E2A04A
Fjord Bank to stop cash handling at 40 branchesWorld4 minno#E5DCD0 / #A58A68 / #2F6B4F

Thumb drawing: --c1 fill; ::before a --c2 half-ellipse rising from the bottom (inset −20% sides, −30% bottom, 70% tall); ::after a 16px --c3 disc at top-right (12px inset).

Tokens

:root {
  /* paper neutrals */
  --paper: #faf6ee;
  --paper-2: #f1ebdf;      /* lead card */
  --line: #e3dccd;         /* hairlines, chip borders */
  --ink: #1c1a17;
  --ink-2: #5e5850;        /* dek, chip text */
  --ink-3: #948c80;        /* meta, inactive nav, bookmark idle */

  /* accents */
  --red: #a83a2a;          /* kicker, badge, focus */
  --red-soft: #f3ddd6;
  --saved: #2f6b4f;        /* saved mark, filled bookmark */

  /* lead illustration */
  --sky: #e9d9c0;
  --sky-2: #f4e4cd;
  --sun: #e2a04a;
  --hill: #8a9a6a;
  --hill-2: #5e6f48;

  /* type */
  --serif: "Newsreader", Georgia, serif;     /* opsz 6..72, wght 500/600 */
  --sans: "Inter", system-ui, sans-serif;
  --fs-mast: 30px; --fs-lead: 26px; --fs-story: 17px; --fs-dek: 14px; --fs-chip: 13px; --fs-meta: 12px; --fs-kicker: 11px; --fs-nav: 11px;

  /* layout */
  --nav-h: 64px;
  --bottom-clear: 34px;
  --gutter: 20px;
  --art-h: 190px;
  --thumb: 72px;
  --r-card: 18px;
  --r-thumb: 10px;
  --r-chip: 999px;

  /* motion */
  --t-micro: 160ms;
  --t-swap: 280ms;
  --ease-std: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
MastheadNewsreader30px6001−0.01emsentence
Lead headlineNewsreader26px6001.15−0.01emsentence
Story headlineNewsreader17px6001.250sentence
DekInter14px4001.450sentence
Kicker / section labelInter11px6001.3+0.08emUPPERCASE
ChipInter13px50010sentence
Meta lineInter12px5001.40sentence
DateInter12px5001.40sentence
Nav labelInter11px50010sentence
BadgeInter10px60016px0numerals

Set the serif with font-optical-sizing:auto so 17px and 26–30px get different optical sizes.

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
.chips buttonselectbackground, color, border-coloroutline → ink fill160mslinear
.list.swap .storychip selectopacity, translateY1, 0 → 0, 6px280ms--ease-std / --ease-outreversed after the filter
.savetogglecolor, fillink-3 → saved0—instant
.chipsswipescroll-snap—native—scroll-snap-type:x proximity
nav itemselectcolorink-3 → ink0—instant

Reduced motion: the list swap dip is removed (opacity:1; transform:none), the filter still applies after 280ms; all transitions 1ms.

States

  • Chip pressed: aria-pressed="true", fill --ink, text --paper, border --ink.
  • Chip idle: transparent, 1px --line border, text --ink-2.
  • Story saved: bookmark aria-pressed="true", icon filled --saved; meta gains ”· saved” with a 14px download-check icon in --saved.
  • Story hidden (filtered): hidden attribute → display:none.
  • Nav current: aria-current="page", colour --ink.
  • Focus-visible: 3px --red outline, 2px offset on chips, bookmarks, nav and the lead link.
  • Empty section: not reachable with the sample data; if a section has zero stories, show the label “Technology · 0 stories” and leave the list empty.

Accessibility

  • Chips: role="group" aria-label="Sections" with aria-pressed buttons (toggle semantics, not tabs, because the list below is filtered rather than replaced).
  • Bookmarks: <button aria-pressed aria-label="Save for offline">; the visible “saved” text in the meta line duplicates the state for sighted users.
  • The lead card is one <a> with heading, dek and meta inside, so its accessible name is the headline (put aria-labelledby on it pointing at the <h2> if your framework flattens link names).
  • Thumbnails and the landscape are aria-hidden decorative CSS.
  • Badge count is inside the “Saved” button so its name reads “Saved 3”.
  • Contrast: --ink-2 on --paper 7.0:1; --ink-3 on --paper 3.6:1 — used only for 11–12px meta at weight 500, so bump to #7d756a (4.6:1) if AA on meta is required; --red on --paper 5.9:1; --saved on --paper 5.7:1.
  • Hit targets: chips 36px tall with 8px gaps (allow 44px row), bookmarks 40px, nav items ≥ 88×52.

Responsive rules

  • 390 wide: as specified.
  • 360 wide: gutter 16px; lead headline 24px; thumbs 64px.
  • ≥ 600 wide: two-column story grid under a full-width lead; chips stay a single scrolling row.
  • ≥ 840 wide (tablet): lead card and list side by side (lead 5/12, list 7/12); bottom nav becomes a left rail.

Acceptance checklist

  • Background is #FAF6EE; headlines are Newsreader 600 at 30 / 26 / 17px; UI text is Inter.
  • Chip row scrolls horizontally with no visible scrollbar and proximity snapping; pressed chip is #1C1A17 with paper text.
  • Selecting a chip dips the list 6px / opacity 0 over 280ms, filters, then restores; the section label reports “Section · N stories”.
  • Lead card is #F1EBDF, 18px radius, with a 190px CSS-drawn landscape (gradient sky, 64px sun, two hills) — no image files.
  • Each story row has a 72px thumb, a serif headline, a meta line with section and reading time, and a 40px bookmark button.
  • Saved stories show ”· saved” in #2F6B4F with a download-check icon; the bookmark is filled.
  • Toggling a bookmark updates the meta line and the Saved tab badge count.
  • Stories are separated by 1px #E3DCCD hairlines, not cards.
  • Bottom nav is 64px plus 34px clearance; current item is ink-coloured with aria-current="page".
  • Focus rings visible on chips, lead link, bookmarks and nav.
  • Reduced motion removes the list dip but keeps filtering.
  • No fixed control inside the top 54px.

Implementation notes

Landscape in three pseudo-elements. Sun and hills are border-radius shapes; the second hill’s shadow gives a third band:

.art { height:190px; position:relative; overflow:hidden;
  background: linear-gradient(180deg, var(--sky) 0%, var(--sky-2) 100%); }
.art::before { content:""; position:absolute; left:58%; top:34px; width:64px; height:64px; border-radius:50%; background:var(--sun); }
.art::after  { content:""; position:absolute; left:-10%; right:-10%; bottom:-40px; height:120px;
  border-radius:50% 50% 0 0 / 100% 100% 0 0; background:var(--hill); box-shadow: 0 26px 0 12px var(--hill-2); }
.art i { position:absolute; left:20%; bottom:30px; width:150%; height:80px;
  border-radius:60% 60% 0 0 / 100% 100% 0 0; background:var(--hill-2); opacity:.85; }

Filter after the dip, not during. One timer, cleared on rapid taps:

list.classList.add('swap'); clearTimeout(t);
t = setTimeout(() => {
  const c = chip.dataset.cat; let n = 0;
  stories.forEach(s => { const show = c === 'all' || s.dataset.cat === c; s.hidden = !show; if (show) n++; });
  label.textContent = c === 'all' ? 'Latest' : `${chip.textContent} · ${n} ${n === 1 ? 'story' : 'stories'}`;
  list.classList.remove('swap');
}, 280);

Saved mark lives in the meta line. Append/remove the fragment next to the reading time rather than toggling a hidden span, so the DOM matches what’s announced:

if (on && !sv) meta.insertAdjacentHTML('beforeend', '<span>·</span><span class="sv">…saved</span>');
else if (!on && sv) { sv.previousElementSibling.remove(); sv.remove(); }

Common mistakes: using <img> placeholders for thumbs (the brief wants CSS colour blocks driven by custom properties); making chips tabs with role="tablist" (the content is filtered, not swapped); letting the chip row wrap at 360px.

Details

Palette
Type
Newsreader · Inter
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Mendo

Paper, type, and the object. The shop stays quiet.

All sources