Design Lounge
Install

Nº 206 of 520

Course landing with curriculum

An academic course page: navy serif hero, honest dated discount, a preview-lesson modal, a module accordion with lock and preview rows, FAQ, and a sticky enroll bar.

Preview

Open full size
1280 × 800 · 23.6 KB · 311 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 "Course landing with curriculum" (piece course-landing-curriculum) 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

259 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 sales page for one online course on a fictional learning site, Lectern. The course is “Data journalism: from spreadsheet to story” by Dr Amara Okonjo. It reads like a university press page: warm white paper, deep navy serif headings, 8px corners, 1px rules, and one green used only for checks, preview links and the free-lesson icon. The hero has a price card with a CSS-drawn video poster. Below it, a learning checklist, a six-module curriculum accordion, an FAQ, and an instructor card. Once the hero’s Enroll button scrolls away, a navy enroll bar slides up from the bottom. The detail worth copying is the honest discount: a dated price with a plain sentence about when it ends, and no countdown timer.

Reference behaviour

  1. First frame at 1280×800: header (64px), the hero with title, subtitle, instructor and rating row, two buttons, a four-fact strip, and the price card on the right. The top of “What you’ll learn” shows at the bottom edge.
  2. The price card shows $129, struck $189, a green Save $60 pill, and the sentence Autumn term price, the same for everyone until 31 October. After that it returns to $189.
  3. Click Preview lesson, the poster in the card, or any Preview link in the curriculum. A modal dialog opens with a 320ms rise (opacity 0 → 1, translateY 12px → 0). Its header reads Lesson preview and the lesson name with its length. Below is a 16:9 CSS-drawn video poster with a play disc, a green progress bar at 18%, 01:23 and the lesson length.
  4. Close the modal with the × button, Esc, or a click on the backdrop. Focus returns to the control that opened it.
  5. The curriculum header reads 6 modules · 31 lessons · 5 h and has an Expand all text button. These totals are computed from the lesson data, never typed.
  6. Module 01 is open at load. Every other module is closed.
  7. Click a module header. Its panel opens or closes over 320ms (grid rows 0fr → 1fr) and the chevron turns 180° over 280ms. Several modules may be open at once.
  8. When every module is open the text button reads Collapse all; clicking it closes all. Otherwise it reads Expand all and opens all.
  9. Each lesson row shows an icon, 1.2 Lesson name, a right-side action, and a tabular duration. Free lessons have a green play-circle icon and a green underlined Preview button. Locked lessons have a grey lock icon and the word Locked.
  10. The FAQ has four questions, all closed. Each opens and closes the same way as a module.
  11. Scroll until the hero’s Enroll for $129 button is above the viewport. The enroll bar slides up from the bottom over 340ms (expo out). Scroll back up until the button is visible and the bar slides away. While hidden, the bar is visibility: hidden so it cannot be tabbed into.
  12. The instructor card in the right rail is sticky at top: 24px on desktop.
  13. Reduced motion: no rise, no slide, no row animation. Panels and the bar switch instantly.

Structure

1280 × 800, content max 1280, side padding 56px, grid minmax(0,1fr) 380px, gap 56px
┌──────────────────────────────────────────────────────────────────────┐
│ [lectern] Lectern   Courses  Teachers  For teams              Sign in │ 64
├──────────────────────────────────────────────────────────────────────┤
│ JOURNALISM · INTERMEDIATE (green 12px)        ┌──────────────────────┐│
│ Data journalism: from                         │  poster 16:9 navy    ││
│ spreadsheet to story   (52px serif, 16ch)     │  grid + bars + play  ││
│ Six modules on finding… (21px serif, 44ch)    ├──────────────────────┤│
│ (AO) Dr Amara Okonjo  4.8 ★★★★★ 2,316 · 18,402│ $129 $189   Save $60 ││
│ [ Enroll for $129 ] [ ▷ Preview lesson ]      │ Autumn term price…   ││
│ ══════════════════════════════════ 2px navy   │ [   Enroll now    ]  ││
│ LEVEL      PACE       FORMAT        CAPTIONS  │ [  Add to my list ]  ││
│ Intermed.  4–6 weeks  Video and…    English…  │ ▢ 5 h of video, 31 … ││
│                                               └──────────────────────┘│
├──────────────────────────────────────────────────────────────────────┤
│ What you'll learn  [✓ ×6 in 2 columns]        ┌ instructor (sticky) ┐│
│ Curriculum   6 modules · 31 lessons · 5 h · Expand all  │ (AO) 72px   ││
│ ┌ 01  Finding the story      5 lessons · 42 min  ^ ┐   │ bio, 3 facts││
│ │     ▷ 1.1 Welcome…              Preview   04:10 │   └─────────────┘│
│ │     x 1.3 Asking…               Locked    09:05 │                   │
│ ├ 02  Cleaning without lying  6 lessons · 58 min v ┤                   │
│ Questions  (4 rows, 1px rules)                                        │
└──────────────────────────────────────────────────────────────────────┘
[ fixed bottom bar 72px navy: title + instructor · $129 $189 · [Enroll now] ]
  • header.top holds the logo link, nav aria-label="Primary", and a Sign in link.
  • The hero is a two-column grid. The left column holds the eyebrow p, the h1, the subtitle, the meta row, the two buttons, and a dl.glance of four facts. The right column is div.card: a button.poster then div.cbody.
  • The lower part is the same grid. main holds three sections labelled by their h2. aside.rail holds the instructor card.
  • Each module is div.mod > h3 > button.mbtn plus div.panel[role=region]. Lessons are an ol of li.les.
  • Each FAQ item is div.q > h3 > button.qb plus div.panel[role=region] > div > p.
  • The enroll bar is div.bar[role=region][aria-label=Enroll], position: fixed, bottom 0.
  • The preview is a native dialog opened with showModal().

Tokens

:root {
  --bg: #faf7f1;        /* warm white page */
  --surface: #fffdf8;   /* cards, accordion, dialog */
  --navy: #15234a;      /* headings, text, primary button, bar */
  --navy-2: #24356a;    /* primary hover */
  --ink-2: #4a5068;     /* body copy */
  --ink-3: #6b7086;     /* meta, durations, lock icon */
  --line: #e3ddd0;      /* 1px rules and borders */
  --green: #2e6b4e;     /* the one accent: checks, preview, eyebrow, focus */
  --green-t: #e3efe7;   /* Save pill */
  --gold: #b7862c;      /* rating stars only */
  --poster-hi: #8fc2a6; /* highlighted bar and progress on navy */

  --serif: "Newsreader", Georgia, serif;
  --sans: "Public Sans", system-ui, sans-serif;

  --fs-11: 11px; --fs-12: 12px; --fs-13: 13px; --fs-14: 14px; --fs-15: 15px;
  --fs-17: 17px; --fs-18: 18px; --fs-19: 19px; --fs-21: 21px; --fs-22: 22px;
  --fs-32: 32px; --fs-40: 40px; --fs-52: 52px;

  --s-8: 8px; --s-12: 12px; --s-16: 16px; --s-24: 24px; --s-36: 36px;
  --s-48: 48px; --s-56: 56px;

  --r: 8px;
  --shadow: none;
  --t-micro: 160ms; --t-chev: 280ms; --t-panel: 320ms; --t-bar: 340ms;
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --out: cubic-bezier(0.16, 1, 0.3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
LogoNewsreader22px60010Title
h1Newsreader52px5001.04-0.015emSentence, max 16ch
SubtitleNewsreader21px5001.450Sentence, max 44ch
h2Newsreader32px5001.150Sentence
Module titleNewsreader19px6001.30Sentence
FAQ questionNewsreader18px5001.350Sentence
PriceNewsreader40px60010$129
Glance valueNewsreader17px5001.30Sentence
EyebrowPublic Sans12px6001.40.14emUPPER, green
LabelsPublic Sans11px6001.40.14emUPPER
BodyPublic Sans15px4001.60Sentence
Lesson rowPublic Sans14px4001.50Sentence
ButtonsPublic Sans15px60010Sentence

Durations use font-variant-numeric: tabular-nums so the column lines up.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Accordion panelheader clickgrid-template-rows0fr → 1fr320ms--easeinstant
Chevronheader clicktransform0 → rotate(180deg)280ms--easeinstant
Dialogopenopacity, transform0, translateY(12px) → 1, none320ms--outnone
Enroll barhero button leaves toptransformtranslateY(100%) → none340ms--outinstant
Play discposter hovertransformscale(1) → scale(1.06)200ms--out1ms
Buttonshoverbackgroundbase → hover160ms--ease1ms

The bar hides with visibility delayed by 340ms so it finishes sliding before it leaves the tab order.

States

  • Primary button: navy fill, warm-white text. Hover --navy-2.
  • Ghost button: 1px navy border, no fill. Hover --surface.
  • Module header hover: background --bg. Open: aria-expanded="true", chevron up.
  • Lesson free: green play icon, green underlined Preview button. Locked: grey lock, grey Locked label. Locked rows are not clickable.
  • Expand all text toggles to Collapse all only when every module is open.
  • Enroll bar hidden: translateY(100%), visibility: hidden. Shown: class on.
  • Focus-visible: 2px solid --green, offset 2px. Inside the navy bar, the ring is #8fc2a6.
  • Closed panels have inert on their inner wrapper so hidden Preview buttons cannot be tabbed into.
  • There is no loading or error state on this frame. The price is static.

Accessibility

  • One h1. Section headings are h2. Module and FAQ headers are h3 elements that wrap a button, per the accordion pattern.
  • Each header button has aria-expanded and aria-controls. Each panel has role="region" and aria-labelledby pointing back to its button.
  • Enter and Space toggle because the headers are real buttons. Tab moves header to header, and into an open panel’s Preview buttons.
  • The rating stars are aria-hidden; the visible 4.8 and 2,316 ratings carry the meaning.
  • The card poster is a button with aria-label="Preview lesson 1.2, Finding the story in a column of numbers, 7 minutes".
  • The dialog is native, labelled by its heading. showModal() traps focus and Esc closes it. The poster inside is role="img" with a short label.
  • The enroll bar is a labelled region. It is removed from the tab order while hidden.
  • Contrast: navy on warm white is about 15:1. #4a5068 passes 7:1. Green #2e6b4e on #fffdf8 passes 6:1. Warm white on navy passes 14:1.
  • Hit targets: module headers 64px, FAQ rows 56px, buttons 48px, close button 40px.

Responsive rules

  • ≥1280: as drawn. Padding 56px. Grid minmax(0,1fr) 380px, gap 56px.
  • 1024 (max-width 1100px): padding 36px. Right column 340px, gap 36px. h1 46px.
  • 768 (max-width 860px): one column. Header links hide. The price card follows the hero text, max 520px wide. The instructor card follows the FAQ and is not sticky.
  • <640: padding 18px. h1 36px, subtitle 18px. Glance strip 2×2. Learn list one column. Module header becomes title over meta, with the chevron on the right; the module number hides. Lesson rows drop the duration column; the action stays. The bar hides its title and shows price on the left and Enroll on the right.
  • Never scroll sideways at 390px. Every grid column uses minmax(0, …).
  • Keep 120px of bottom padding on the page so the fixed bar never covers the last FAQ row.

Acceptance checklist

Always

  • The discount states a plain end date and the full price after it. No countdown, no “only 3 left”.
  • One accent colour (green), used only for checks, preview links, free icons, the eyebrow and focus.
  • Module and FAQ headers are buttons inside headings, with aria-expanded and aria-controls.
  • Module meta shows lesson count and total duration, computed from the lesson list.
  • Every lesson row shows either a free-preview action or a lock, never both.
  • Preview opens a modal dialog that closes on Esc, × and backdrop, and returns focus.
  • The enroll bar appears only after the hero’s primary button scrolls above the viewport, and cannot be tabbed into while hidden.
  • Closed panels are inert.
  • All corners 8px; regions separated by 1px rules, no drop shadows.
  • No horizontal scroll at 390px.

This demo

  • Title Data journalism: from spreadsheet to story, instructor Dr Amara Okonjo, 4.8 from 2,316 ratings, 18,402 students.
  • Price $129, struck $189, Save $60, ends 31 October.
  • Curriculum summary reads 6 modules · 31 lessons · 5 h; module 01 is open with 5 lessons · 42 min.
  • Free previews are 1.1, 1.2 and 2.2.
  • FAQ has four questions, including Is the discount real?.

Implementation notes

1. Height animation without measuring. Animate grid rows from 0fr to 1fr. The child needs overflow: hidden. Put inert on the child when closed.

.panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .32s var(--ease); }
.panel > div { overflow: hidden; }
.panel.open { grid-template-rows: 1fr; }
.mbtn[aria-expanded="true"] .chev { transform: rotate(180deg); }
function toggle(btn, open) {
  const panel = document.getElementById(btn.getAttribute('aria-controls'));
  open = open ?? btn.getAttribute('aria-expanded') !== 'true';
  btn.setAttribute('aria-expanded', open);
  panel.classList.toggle('open', open);
  panel.firstElementChild.inert = !open;
}

2. Totals from data. Store lessons as [name, "mm:ss", isFree]. Sum seconds, then round to whole minutes before splitting into hours, or you get 4 h 60 min.

const sec = t => { const [m, s] = t.split(':'); return +m * 60 + +s; };
const fmt = s => {
  const t = Math.round(s / 60), h = Math.floor(t / 60), m = t % 60;
  return h ? h + ' h' + (m ? ' ' + m + ' min' : '') : m + ' min';
};

3. Enroll bar trigger. Observe the hero’s primary button. Show the bar only when it is out of view above the top, not when the page first loads with it below the fold.

new IntersectionObserver(([e]) => {
  bar.classList.toggle('on', !e.isIntersecting && e.boundingClientRect.top < 0);
}).observe(document.getElementById('enrollTop'));
.bar { position: fixed; inset: auto 0 0; transform: translateY(100%); visibility: hidden;
       transition: transform .34s var(--out), visibility 0s .34s; }
.bar.on { transform: none; visibility: visible; transition: transform .34s var(--out); }

The poster is drawn, not an image: a navy box, a grid from two repeating-linear-gradients at 14% white, six flex bars at 28% white with one green bar at 96% height, and a 64px warm-white play disc.

Common mistakes:

  • A countdown timer or a fake “price goes up in 02:14:09”. The honest sentence is the point of this piece.
  • Using <details> for the curriculum and then adding a second set of ARIA. Pick buttons with aria-expanded and stay consistent.
  • Leaving Preview buttons focusable inside closed panels.
  • A sticky bar that is visible on the first frame, covering the hero button it duplicates.
  • Typing module durations by hand. They drift from the lessons.
  • A purple gradient hero or a video thumbnail with a glow. The page is paper and ink.
  • Making the right card sticky as well as showing the bar. Use one sticky purchase surface at a time; here the rail holds the instructor.

Rebuild order:

  1. Set tokens, header, and the two-column grid.
  2. Build the hero text, meta row, buttons and glance strip.
  3. Build the price card with the drawn poster.
  4. Add the learn checklist.
  5. Generate the curriculum from data and wire the accordion.
  6. Add the FAQ with the same toggle.
  7. Add the preview dialog and wire every preview trigger.
  8. Add the enroll bar and its observer.
  9. Check 1024, 768 and 390.

Details

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

Studied

Haunted Boulder City

Pin the screen and let scroll turn the pages. Only the line you are on gets to shout.

All sources