Design Lounge
Install

Nº 031 of 520

M3 Expressive home feed

A warm-tonal Material 3 Expressive home: 36px greeting, morphing segmented group with a shape loader, 28px tonal cards, a floating 5-action toolbar and an extended FAB.

Preview

Open full size
390 × 844 · 11.2 KB · 139 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 "M3 Expressive home feed" (piece m3-expressive-home) 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

249 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 home screen of “Mira”, a daily planner, in the Material 3 Expressive language: warm tonal surfaces instead of hairlines, 28px card radii, a segmented “button group” whose selected segment grows into a full pill, a bottom floating toolbar (a detached pill with five icon actions) and an extended FAB stacked above it. The feeling is warm and physical: things squash when pressed, shapes morph rather than fade. The detail worth copying is the segmented group: the pressed segment takes 1.7× the flex share and rounds from 16px to a full pill over 500ms with emphasized easing, while a small morphing “loading shape” opens a 44px slot above the cards and the cards dip and return.

Reference behaviour

  1. Initial state: greeting “Morning, Noor.” (36px) with the date above it, a 48px avatar button at right. Below: segmented group with Today selected, then three tonal cards (primary-container hero, surface-high stats, tertiary-container habits). A floating toolbar pill sits centred 46px from the bottom; an extended FAB “New task” sits at right, 118px from the bottom.
  2. Tap Week or Month: the previously selected segment shrinks back to flex 1 and 16px radius; the tapped one grows to flex 1.7 and 999px radius, fills --primary. Both over 500ms cubic-bezier(.2,0,0,1).
  3. At the same instant a loader slot expands from 0 to 44px height (500ms, same easing) showing a 22px primary-coloured blob that morphs between three border-radius shapes while rotating 180° (1.2s loop). The card stack fades to opacity 0 and moves down 14px / scales to .98.
  4. After 700ms the hero and stats headlines swap to that range’s copy (“4 meetings, 2 free afternoons” / “Avg. sleep 6h 48m” for Week; “September: 11 focus days” / “Avg. sleep 7h 02m” for Month), the loader slot collapses and the cards return.
  5. Tap any habit row: the 26px checkbox fills --tertiary, morphs from 9px radius to a circle, a check scales in from .5; the label strikes through at 60% opacity. Tap again to undo.
  6. Tap a toolbar action: it becomes the pressed one — background --primary, icon white, radius eases from 999px to 16px (500ms). Only one pressed at a time. Inbox carries a “3” badge.
  7. Press-and-hold the FAB or the “Join call” pill: it scales to .95/.96 and its radius rounds further (18→28px, 999→14px respectively) over 160ms, then releases.
  8. The content scrolls beneath the toolbar and FAB; 150px of bottom padding keeps the last card reachable.

Structure

390 × 844
┌────────────────────────────────────┐
│ (54px status clearance)            │
│ Tuesday, 29 September      (NH)48  │  header
│ Morning, Noor.              avatar │  h1 36px
│ ┌──────────────┬────────┬────────┐ │
│ │   Today ●    │  Week  │ Month  │ │  segmented, 56px tall, 4px inset
│ └──────────────┴────────┴────────┘ │
│ [loader slot 0→44px]               │
│ ╭────────────────────────────────╮ │
│ │ NEXT UP · 10:30                │ │  hero card, primary-container
│ │ Design sync with Priya         │ │  28px radius
│ │ (Join call) pill 48px          │ │
│ ╰────────────────────────────────╯ │
│ ╭────────────────────────────────╮ │
│ │ BODY · You slept 7h 12m        │ │  surface-high card
│ │ [6,420][5 / 8][61] tiles 20px  │ │
│ ╰────────────────────────────────╯ │
│ ╭────────────────────────────────╮ │
│ │ HABITS · 1 OF 3  (3 checkboxes)│ │  tertiary-container card
│ ╰────────────────────────────────╯ │
│                   ┌──────────────┐ │
│                   │ + New task   │ │  extended FAB, bottom 118
│  ╭──────────────────────────────╮ │
│  │ ⌂  ▦  ✉3  ▥  ○              │ │  floating toolbar, 64px, bottom 46
│  ╰──────────────────────────────╯ │
│ (34px home-indicator clearance)    │
└────────────────────────────────────┘
  • <main> — full height, overflow-y:auto, padding 54px 16px 150px. Contains everything that scrolls.
    • <header> flex row, align-items:flex-end, 10px top padding; the segmented group has 16px above and 12px below. With these values the habits card headline sits just above the FAB on the first frame. <header>: <h1> with a <small> date line, plus <button class="avatar">.
    • <div class="seg" role="group"> with three <button aria-pressed>.
    • <div class="loader" aria-hidden> holding one .blob.
    • <section class="stack" aria-live="polite"> of three <article class="card">.
      • Hero: .eyebrow, <h2>, <p>, <button class="pill"> with a 20px icon.
      • Stats: .eyebrow, <h2>, .tiles grid of three .tile (value + label).
      • Habits: .eyebrow, <h2>, <ul class="habits"> of <button role="checkbox" aria-checked> rows.
  • <button class="fab"> — absolutely positioned on body, right:16px; bottom:118px.
  • <nav class="bar" aria-label="Primary"> — absolutely positioned, left:50%; bottom:46px; translateX(-50%), five <button aria-pressed>.

Sample content per range (the two swapped headlines are #c1 in the hero card and #c2 in the stats card):

RangeHero h2Stats h2
TodayDesign sync with PriyaYou slept 7h 12m
Week4 meetings, 2 free afternoonsAvg. sleep 6h 48m
MonthSeptember: 11 focus daysAvg. sleep 7h 02m

Fixed copy: date “Tuesday, 29 September”; greeting “Morning, Noor.”; hero eyebrow “Next up · 10:30”, body “Room 4B · 3 joined · agenda: onboarding v3.” (one line), pill “Join call”; stats eyebrow “Body”, tiles 6,420 steps / 5 / 8 glasses / 61 resting bpm; habits eyebrow “Habits · 1 of 3”, h2 “14-day streak” (kept short so it clears the FAB’s left edge at x≈241), rows “10 minutes of reading” (checked), “Walk after lunch”, “Lights out by 23:00”; FAB “New task”; toolbar labels Home, Calendar, Inbox (badge 3), Stats, Profile.

Tokens

:root {
  /* tonal surfaces — warm bias */
  --surface: #fff8f3;
  --surface-low: #fdf1e8;
  --surface-c: #f7e9dd;          /* segmented track */
  --surface-high: #f1e2d3;       /* stats card */
  --surface-highest: #ebdac9;    /* floating toolbar */
  --on-surface: #211b16;
  --on-surface-v: #564840;       /* secondary text, unselected icons */
  --outline: #89766a;
  --outline-v: #dcc9ba;

  /* primary — terracotta */
  --primary: #8a4a1d;
  --on-primary: #ffffff;
  --primary-c: #ffdbc8;          /* hero card, FAB */
  --on-primary-c: #341100;

  /* secondary / tertiary */
  --secondary-c: #f5dfc9;
  --on-secondary-c: #4a2e17;
  --tertiary: #4c6339;           /* checkbox fill, badge */
  --tertiary-c: #dfe9c8;         /* habits card */
  --on-tertiary-c: #152005;

  /* type */
  --font: "Rubik", system-ui, sans-serif;
  --fs-h1: 36px; --fs-h2: 24px; --fs-stat: 22px; --fs-body: 15px; --fs-small: 14px; --fs-eyebrow: 12px;

  /* shape */
  --r-card: 28px;
  --r-pill: 999px;
  --r-seg: 16px;                 /* unselected segment / pressed toolbar item */
  --r-tile: 20px;
  --r-fab: 18px;
  --r-chk: 9px;

  /* spacing (4px base) */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 20px; --s-6: 24px;

  /* elevation */
  --shadow-fab: 0 6px 18px rgba(52,17,0,.18), 0 1px 3px rgba(52,17,0,.12);
  --shadow-bar: 0 8px 24px rgba(52,17,0,.14);

  /* motion — M3 Expressive */
  --t-micro: 160ms;
  --t-big: 500ms;
  --t-swap: 700ms;               /* JS: time before copy swaps */
  --ease-emph: cubic-bezier(.2, 0, 0, 1);
  --ease-exit: cubic-bezier(.3, 0, .8, .15);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Greeting h1Rubik36px5001.05−0.02emsentence
Date lineRubik14px4001.40sentence
Segment labelRubik14px50010sentence
Card eyebrowRubik12px5001.3+0.06emUPPERCASE
Card h2Rubik24px5001.15−0.01emsentence
Card bodyRubik14px4001.450sentence
Stat tile valueRubik22px5001.1−0.02emnumerals
Tile labelRubik12px4001.30lowercase
Pill / FABRubik15px50010sentence
BadgeRubik10px60016px0numerals

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
.seg buttonselectflex, border-radius1, 16px → 1.7, 999px500ms--ease-emphbackground/colour swap in 160ms
.loaderselectheight0 → 44px, then back500ms--ease-emphoverflow:hidden
.blobwhile loadingborder-radius, rotatecircle → blob A → blob B → circle, 0→180°1200ms loop--ease-emph3 keyframes
.stack .cardselectopacity, transform1, none → 0, translateY(14px) scale(.98)500ms--ease-emphreversed after 700ms
.chktogglebackground, radiustransparent, 9px → tertiary, 50%160ms--ease-emph
.chk svgtoggleopacity, scale0, .5 → 1, 1160ms--ease-emph
.bar buttonselectborder-radius999px → 16px500ms--ease-emphcolour 160ms
.fab, .pill:activescale, border-radius1, 18px → .95, 28px160ms--ease-emphpill: 999→14px, .96

Reduced motion: all transitions and animations to 1ms; the card stack does not dip (opacity stays 1, transform none). The loader slot still opens and closes so the copy swap has a visible cause.

States

  • Segment selected: aria-pressed="true", fill --primary, text --on-primary, flex 1.7, full pill.
  • Segment pressed (:active): scale .97.
  • Toolbar action pressed: aria-pressed="true", fill --primary, icon white, 16px radius. Others: transparent, --on-surface-v.
  • Habit checked: aria-checked="true", checkbox filled circle, label line-through at 60% opacity.
  • Habit row active: background rgba(21,32,5,.08) (state layer on the tertiary container).
  • Focus-visible (all buttons): 3px --primary outline, 2px offset.
  • Loading: .loader.on and .stack.swap present together for 700ms.

Accessibility

  • Segmented group: role="group" aria-label="Range", each segment a <button aria-pressed>. Do not use radio semantics unless you also implement arrow-key movement.
  • Toolbar: <nav aria-label="Primary">, each action has an aria-label; the badge count is folded into the label (“Inbox, 3 unread”).
  • Habits: <button role="checkbox" aria-checked>; Space/Enter toggles natively.
  • The card stack is aria-live="polite" so the copy swap is announced once.
  • The loader is aria-hidden; loading is implicit from the live region update.
  • Contrast: --on-surface-v on --surface-high 7.3:1; --on-primary-c on --primary-c 12:1; white on --primary 7.4:1.
  • Hit targets: segments 48px, toolbar actions 56×48, habit rows ≥44px, avatar 48px, FAB 56px.

Responsive rules

  • 390 wide: as specified.
  • 360 wide: toolbar shrinks its actions to 48×48 (total 264px); FAB keeps 56px height; h1 drops to 32px; tiles stay 3-up.
  • ≥ 600 (tablet or large phone): cap content at 480px centred; the toolbar stays centred; the FAB aligns to the content’s right edge, not the viewport’s.
  • Landscape/short heights: reduce top clearance to env(safe-area-inset-top) and allow the toolbar to overlap the last card (padding-bottom stays 150px).

Acceptance checklist

  • Selected segment has flex 1.7 and 999px radius; unselected have flex 1 and 16px; the change eases over 500ms cubic-bezier(.2,0,0,1).
  • Selecting a range opens a 44px loader slot with a morphing 22px blob and swaps the two headlines after 700ms.
  • Cards use 28px radii and are separated only by tonal surface changes (no borders, no shadows).
  • Hero card is #FFDBC8 with #341100 text; habits card is #DFE9C8 with #152005 text.
  • Floating toolbar is a detached 64px pill, centred, bottom edge 46px from the viewport bottom, casting 0 8px 24px rgba(52,17,0,.14).
  • Extended FAB sits at right:16px; bottom:118px, 56px tall, 18px radius, and rounds to 28px while pressed.
  • Only one toolbar action is aria-pressed="true" at a time; the pressed one is #8A4A1D with a 16px radius.
  • Habit checkbox morphs from a 9px-radius square to a filled circle on check.
  • Every button shows a 3px terracotta focus ring on keyboard focus.
  • Nothing fixed sits within the top 54px or bottom 34px.
  • prefers-reduced-motion removes the blob animation and the card dip while keeping the copy swap.
  • Content scrolls under the toolbar with 150px bottom padding.

Implementation notes

Grow the selected segment with flex, not width. Width transitions fight the container; flex-basis transitions are smooth and keep the group edge-to-edge:

.seg { display:flex; gap:4px; padding:4px; border-radius:999px; background:var(--surface-c); }
.seg button { flex:1; height:48px; border-radius:16px;
  transition: flex 500ms var(--ease-emph), border-radius 500ms var(--ease-emph),
              background 160ms, color 160ms; }
.seg button[aria-pressed="true"] { flex:1.7; border-radius:999px;
  background:var(--primary); color:var(--on-primary); }

The loading shape is pure CSS — three border-radius keyframes plus a half-turn:

.blob { width:22px; height:22px; background:var(--primary);
  animation: morph 1.2s var(--ease-emph) infinite; }
@keyframes morph {
  0%   { border-radius:50%; transform:rotate(0) }
  33%  { border-radius:30% 70% 60% 40% / 50% 40% 60% 50% }
  66%  { border-radius:60% 40% 30% 70% / 60% 30% 70% 40% }
  100% { border-radius:50%; transform:rotate(180deg) }
}

Swap copy once, after the dip. Clear the previous timer so fast taps don’t stack swaps:

let timer = null;
seg.addEventListener('click', (e) => {
  const b = e.target.closest('button');
  if (!b || b.getAttribute('aria-pressed') === 'true') return;
  seg.querySelectorAll('button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
  loader.classList.add('on'); stack.classList.add('swap'); clearTimeout(timer);
  timer = setTimeout(() => { /* write new headlines */ loader.classList.remove('on'); stack.classList.remove('swap'); }, 700);
});

Common mistakes: drawing the toolbar as a full-width bottom bar (it must float, with surface visible on both sides); giving cards a border; using ease instead of the emphasized curve, which makes the flex growth feel like a slide rather than a settle.

Details

Palette
Type
Rubik
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Don't Board Me

A control. It studies this site for quiet, not for a show.

All sources