Design Lounge
Install

Nº 513 of 520

Phone rating prompt

An 'Enjoying Brindle?' sheet after a delivered order. Yes hands off to a generic five-star system review alert; Not really opens a private feedback form; Not now backs off.

Preview

Open full size
390 × 844 · 17.8 KB · 241 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 "Phone rating prompt" (piece phone-rating-prompt) 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

252 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

An in-app rating prompt for “Brindle”, an invented grocery delivery app, shown right after a success moment: the order-delivered screen. A warm bottom sheet asks “Enjoying Brindle?” with two equal choices and a quiet “Not now”. Yes hands off to the platform’s own review prompt (mocked here as a generic five-star system alert, clearly tagged as drawn by the phone, with no store branding). Not really never reaches the store; the same sheet turns into a short private feedback form with reason chips and an optional note. iOS 26-ish language: grabber sheet, 28px top radii, spring-ish motion, butter and cocoa neutrals with persimmon as the one accent. The detail worth copying is the routing: happy people get one tap to the store, unhappy people get heard by the team, and nobody gets asked twice in a row.

Reference behaviour

  1. Initial state (hero): the delivered screen sits behind a cocoa scrim at 38%. The sheet is up: basket mascot tile, “Enjoying Brindle?”, “Your answer goes to the five of us who build it. One tap is plenty.”, buttons “Not really” (secondary) and “Yes, I like it” (primary, focused), and “Not now” below.
  2. Behind the sheet: “Brindle” wordmark, a close button, a 96px persimmon badge whose check draws in over 700ms (200ms delay), “Delivered”, “Left at your door at 18:24, six minutes early.”, and a receipt card for Order 4107 with four items and “Paid $20.40”.
  3. Not now, a scrim tap, or Esc: the sheet drops (280ms), a toast says “Got it. We will not ask again for a while.”, and a “Show the prompt again” link fades in under the receipt (demo replay only).
  4. Yes, I like it: the sheet and scrim leave; a darker system scrim (56%) fades in (220ms) and a 270px system-style alert scales from 1.12 to 1 (320ms). A small pill above it reads “System prompt, drawn by the phone”. Alert: app icon, “Enjoying Brindle?”, “Tap a star to rate it in the store.”, five outline stars, “Not Now”. Focus lands on star 1.
  5. Hover (mouse) previews a fill up to the hovered star; leaving restores the chosen rating. Tap or arrow keys choose a rating: stars fill blue with a 40ms stagger and a small overshoot pop. Choosing any rating reveals a bold “Submit” above “Not Now”.
  6. Submit: everything closes; toast “Thanks for the 4 stars” with a star icon. Not Now in the alert closes silently (the OS decides; the app does not comment).
  7. Not really: the sheet stays up and swaps content (320ms fade and 8px rise) to “Sorry it was not great / Tell us what got in the way. This goes to the team, not to a public review.” Six reason chips (Missing items, Late delivery, Bruised produce, App is slow, Hard to find things, Something else), a textarea “Anything else?” with a live “0 / 280” counter, and “Skip” / “Send”. Focus moves to the first chip.
  8. Send is aria-disabled until at least one chip is pressed or the note has text. Pressing it while disabled shakes it 4px and does nothing else.
  9. Send (enabled): the sheet swaps to “Thanks, Mira / Sent to the team. We read every one, and we will not ask you again for a while.” with a full-width “Back to my order”. Skip closes with the “not ask again” toast.
  10. Rate limiting is policy, not UI (see Implementation notes): never on first launch, never after an error or a failed order, at most three times in 365 days, never again after a rating or a feedback send, and a minimum gap after Not now.

Structure

390 × 844
┌──────────────────────────────────────┐
│ (62px top padding, 54px clearance)   │
│ Brindle                         (×)  │ serif 20 · 44px close
│              ( ✓ )                   │ badge 96px + 10px halo
│            Delivered                 │ serif 40
│  Left at your door at 18:24, six …   │
│ ┌──────────────────────────────────┐ │
│ │ ORDER 4107              4 ITEMS  │ │ receipt, dashed rules
│ │ Sourdough loaf            $6.40  │ │
│ │ …                                │ │
│ │ Paid                     $20.40  │ │
│ └──────────────────────────────────┘ │
│       Show the prompt again          │ replay link, after dismiss
│ ░░░░░░░░░░ scrim 38% ░░░░░░░░░░░░░░░ │
│ ╭──────────────────────────────────╮ │ sheet, 28px top radii
│ │              ────                │ │ grabber 36×5
│ │ [bag]                            │ │ mascot 56px, 16px radius
│ │ Enjoying Brindle?                │ │ serif 26
│ │ Your answer goes to the five …   │ │
│ │ [ Not really ] [ Yes, I like it ]│ │ 52px, 10px gap, equal widths
│ │            Not now               │ │ 44px text button
│ │ (34px home clearance)            │ │
│ ╰──────────────────────────────────╯ │
└──────────────────────────────────────┘
 Yes →  centred 270px system alert over a 56% scrim
        [ SYSTEM PROMPT, DRAWN BY THE PHONE ]
        ┌────────────────────┐
        │      [icon]        │ 56px, 13px radius
        │ Enjoying Brindle?  │ 17/600 system font
        │ Tap a star to …    │ 13px
        │  ☆  ☆  ☆  ☆  ☆     │ 44px targets, 30px glyphs
        │──────────────────── │
        │      Submit        │ appears after a rating
        │      Not Now       │
        └────────────────────┘
  • main holds the success screen; it is inert while any prompt is open.
  • .sheet is section role="dialog" aria-modal="true"; its aria-labelledby points at the visible pane’s heading (ask, feedback, or thanks).
  • Panes: #pAsk (div), #pFb (form novalidate with a fieldset/legend of aria-pressed chips and a labelled textarea), #pDone (div, tabindex="-1", receives focus).
  • System mock: .sys overlay with an aria-hidden tag pill and div role="alertdialog" aria-modal="true"; stars are a role="radiogroup" of five role="radio" buttons with roving tabindex.
  • Toast: div role="status", pointer-events: none.

Tokens

:root {
  /* warm neutrals */
  --bg: #fff3dc;           /* page, chip and textarea fill */
  --paper: #fffaf0;        /* receipt, sheet */
  --ink: #2a1a12;          /* cocoa text, pressed chip, toast */
  --ink-2: #6b5244;        /* secondary text */
  --ink-3: #a08878;        /* placeholder only */
  --line: #efdcbc;         /* borders, grabber */
  --dash: #e3caa2;         /* receipt dashed rules */

  /* accent */
  --accent: #bd421c;       /* persimmon: primary button, badge, focus ring */
  --accent-ink: #fff6ec;
  --accent-soft: #fbe0cf;  /* badge halo, mascot tile */
  --star: #f0a92e;         /* toast star */

  /* system mock (deliberately not the app palette) */
  --sys: #f4f1ec; --sys-line: #dcd6cd; --sys-blue: #2463c7;

  --scrim: rgba(42, 26, 18, .38);
  --sys-scrim: rgba(20, 14, 10, .56);

  --serif: "Young Serif", Georgia, serif;
  --sans: "Figtree", system-ui, sans-serif;

  --r-sheet: 28px; --r-card: 20px; --r-btn: 16px; --r-chip: 12px; --r-alert: 14px;
  --sheet-x: 24px; --home: 34px;

  --t-micro: 160ms; --t-layout: 320ms; --t-sheet: 480ms; --t-exit: 280ms; --t-toast: 2800ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --spring: cubic-bezier(.32, .72, 0, 1);
  --pop: cubic-bezier(.34, 1.56, .64, 1);
  --exit: cubic-bezier(.4, 0, 1, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingNotes
Success titleYoung Serif40px4001.05-0.02em
WordmarkYoung Serif20px4001-0.01em
Sheet titleYoung Serif26px4001.15-0.01em
Sheet subFigtree15px4001.40--ink-2
ButtonsFigtree16px60010
Not now, SkipFigtree15px / 16px60010--ink-2 / ink
ChipsFigtree15px50010
Legend, labelFigtree15px6001.40
CounterFigtree13px40010tabular numerals
TextareaFigtree16px4001.4016px avoids iOS zoom
Receipt headerFigtree12px60010.08emuppercase
Receipt rowsFigtree15px400, total 7001.40prices tabular
System alertplatform system font17px / 13px600 / 4001.350do not use the app fonts
Tag pillFigtree10px60010.1emuppercase

Motion

ElementTriggerPropertyFrom → ToDurationEasingDelay
Badge checkloadstroke-dashoffset30 → 0700ms--ease200ms
SheetopentranslateY110% → 0480ms--spring0
SheetclosetranslateY0 → 110%280ms--exit0
Scrimopen / closeopacity0 ↔ 1480ms--ease0
Pane swapNot really / Sendopacity, translateY0, 8px → 1, 0320ms--ease0
System scrimYesopacity0 → 1220ms--ease0
System alertYesscale1.12 → 1320ms--spring0
Starsrate / hoverfill, scaleoutline, 1 → filled, 1.1220ms--pop40ms × index
Chip, button:activescale1 → .96 / .97160ms--ease0
Send (disabled)submit attempttranslateX-4, 4, 0260ms--ease0
Toastshow / hidetranslateY, opacity24px, 0 ↔ 0, 1260ms--springhides after 2800ms
Replay linkafter closeopacity0 → 1320ms--ease300ms

Reduced motion: all transitions 1ms, check draw, pane fade, star stagger and shake removed. Focus moves happen immediately instead of after the sheet settles.

States

  • Ask: body.ask. Primary focused. Both answers equal width so neither is a dark pattern.
  • Feedback: body.fb. Send disabled (40% opacity, aria-disabled="true") until a chip or text exists.
  • Chip pressed: cocoa fill, paper text, check icon shown. Unpressed: --bg fill, 1.5px --line border.
  • Textarea focus: 2.5px persimmon outline, offset 0. Counter updates on every input; hard stop at 280 via maxlength.
  • Thanks: pane with check tile and one full-width button.
  • System alert, no rating: outline stars, only “Not Now”. Rated: stars filled to the rating, “Submit” visible and bold.
  • Dismissed: success screen interactive, replay link visible, toast for 2800ms.
  • Error: the prompt is never shown on an error or a failed delivery screen. If sending feedback fails, keep the form filled, show “Could not send. Try again.” under the buttons, and keep Send enabled.
  • Focus-visible: 2.5px --accent ring, 2px offset, 12px radius; system stars use a 2px blue ring inset 4px.

Accessibility

  • The sheet is a modal dialog; main is inert behind it. Tab cycles inside the sheet or the alert. Esc dismisses (counts as Not now).
  • Initial focus: “Yes, I like it” on ask, the first chip on feedback, the thanks pane itself, star 1 in the alert. Focus returns to the replay link after closing.
  • Answer buttons are plain words. No smiley-only buttons.
  • Chips are aria-pressed toggles inside a fieldset with legend “What went wrong?”. The textarea has a real label; the counter is aria-live="polite".
  • Send uses aria-disabled rather than disabled so it stays focusable and the shake explains itself.
  • Stars: role="radiogroup" labelled “Rating”; each star role="radio" with aria-label="3 stars", aria-checked, roving tabindex. Arrow keys move and select, Home/End jump to 1 and 5.
  • Toast is role="status" and click-through.
  • Contrast: --ink on --paper 16.1:1; --ink-2 on --paper 6.9:1; --accent-ink on --accent 5.0:1 (16px/600 buttons); --ink on --bg 15.2:1; system blue on the alert 5.1:1. Do not lighten the persimmon: at #c9471f the button text drops to 4.5:1.
  • Hit targets: answer buttons 52px, chips, Not now, close and stars 44px.

Responsive rules

  • 390 × 844 frame. The sheet hugs the bottom with 34px home clearance; content starts 62px from the top (54px clearance plus 8px).
  • 360 wide: chips wrap to three rows; answer buttons stay side by side (“Yes, I like it” fits at 16px in 151px).
  • Largest text size: rows grow, never clip. The two answer buttons stack, primary first; “Not now” stays below. Chips wrap to more rows. The sheet gets max-height: calc(100% - 54px) and scrolls its content while the buttons stay pinned at the bottom.
  • Tablet: the sheet becomes a centred 420px card with all corners rounded. The system prompt is whatever the OS draws.
  • Do not draw the status bar or the home indicator.

Acceptance checklist

Always

  • The prompt only follows a success moment, never first launch, never after an error.
  • Two equal-width answers plus a separate “Not now”; no pre-checked stars, no incentive copy.
  • Yes calls the platform review API; the app never draws its own fake store stars in production. The mock is tagged as system UI.
  • Not really routes to a private form, not the store; Send needs a reason or text.
  • Any dismissal, rating or send records a timestamp and suppresses the prompt per the rate rules below.
  • Modal focus management: initial focus, trap, Esc, focus return.
  • All targets at least 44px; stars are a keyboard-operable radiogroup.
  • Reduced motion keeps every path complete.

This demo

  • Sheet title “Enjoying Brindle?” in Young Serif 26px on #FFFAF0, primary #BD421C.
  • Background: “Delivered”, “Left at your door at 18:24, six minutes early.”, Order 4107, Paid $20.40.
  • System alert 270px wide, “Tap a star to rate it in the store.”, Submit appears after a rating.
  • Six reason chips in the order listed; counter “0 / 280”.
  • Toasts: “Got it. We will not ask again for a while.” and “Thanks for the 4 stars”.

Implementation notes

Rate limiting lives in one gate function. Check it at the success moment; if it says no, show nothing and do not log an impression.

const DAY = 864e5;
function shouldAsk(s, now = Date.now()) {
  if (s.launches < 3 || s.completedOrders < 2) return false;      // never on first launch
  if (s.errorThisSession || s.lastOrderFailed) return false;      // never after an error
  if (s.rated || s.feedbackSent) return false;                    // never after an answer
  const year = s.asks.filter(t => now - t < 365 * DAY);
  if (year.length >= 3) return false;                             // at most 3 per year
  if (s.lastDismiss && now - s.lastDismiss < 60 * DAY) return false; // gap after Not now
  if (s.appVersionAsked === s.appVersion) return false;           // once per version
  return true;
}

The platform review API applies its own cap on top (iOS shows the system prompt at most three times in 365 days and may show nothing at all). Never assume the system alert appeared; do not show a “thanks for rating” message unless your own UI collected the rating. In this demo the Submit toast exists because the mock collects the star count.

Roving tabindex for the stars.

function rate(n, focus) {
  stars.forEach((s, i) => {
    s.setAttribute('aria-checked', i === n - 1);
    s.tabIndex = i === n - 1 ? 0 : -1;
    s.classList.toggle('on', i < n);
  });
  if (focus) stars[n - 1].focus();
  submit.hidden = false;
}
group.addEventListener('keydown', e => {
  const d = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 }[e.key];
  if (d) { e.preventDefault(); rate(Math.max(1, Math.min(5, (rating || 0) + d || 1)), true); }
});

One sheet, three panes. Keep the sheet mounted and swap panes so the scrim never flickers between ask and feedback; update the dialog’s aria-labelledby on each swap and move focus into the new pane.

Common mistakes:

  • Asking for stars in the app first and only sending 4 and 5 star people to the store. Store rules forbid gating reviews on sentiment; the yes/no question routes the conversation, it does not filter ratings.
  • Showing the prompt on launch, on the checkout screen, or after a refund.
  • A close ”×” that is tiny and grey next to a huge Yes. Not now is a real 44px button.
  • Drawing store logos or the store’s name in the mock.
  • Toast or overlay elements that still capture taps after they fade out.

Details

Palette
Type
Young Serif · Figtree
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

60fps

Show one small moment at a time, and let the tags do the sorting.

All sources