ShortDrama Web — Design System v2

Scope of this version

Web version (desktop + mobile web) of the ShortDrama bootstrap. Tokens and layout rules below come from competitor research done 2026-09-23 (research/): real CSS fetched from 6 competitor sites, plus live-browser screenshots of ReelShort, DramaBox, GoodShort, NetShort at 1440×900 and 390×844. There is still no mood board of our own and no real brand name — "ShortDrama" is a placeholder. Every rule cites the research file it came from; anything the research did not observe is marked as our own choice.

Voice & Copy

Short, punchy English, imperative CTAs: "Play", "Continue EP 5", "Unlock for 50 coins", "Watch ad to unlock". No emoji in UI copy or controls — the coin is an SVG glyph, not 🪙. Meta always pairs a number with a unit: "1.2M views", "67 episodes". Episode labels are "EP 13", never a bare "13" outside the numbered grid tile. Price lines show the currency symbol and two decimals: "$19.99".

Colour

Two warm hues, strictly separated:

  • accent-600 (hot pink-red) = the brand and the action colour: Play, Unlock-with-coins CTA, Hot/New badge, current-episode tile, active-nav underline. This matches the category norm (5 of 7 competitors, research/visual-signals.md).
  • vip-gold = VIP and coins only: VIP plan cards, coin glyph and balance, VIP badge. Never on a Play/Unlock button. Every competitor keeps VIP gold separate from its brand red.

Don't use accent-600 for small text on surface-000 in dark theme (4.29:1, fails AA). The active nav item keeps its ink-900 label and shows a 2px accent underline. Build every page from surface-000 (page and player letterbox) → surface-100 (nav, panels, sheet) → surface-200 (chips, episode tiles, coin packs). Dark is the default theme. Light theme exists and passes WCAG, but no competitor ships a light product UI (DramaBox is light only on its SEO shell).

Typography

Plus Jakarta Sans for everything, with Noto Sans SC / PingFang SC as the fallback, because titles and actor names often come from Chinese-language sources (every competitor ships a CJK fallback). Never Inter or Roboto. display-xl is used only for the desktop hero title. title-m is for drama titles and section headers ("Trending Now", "New Release"). label is for anything tappable.

Layout

  • Desktop (≥1024px): top nav + centred content up to content-max (1280px). Home rails scroll horizontally, with poster-w 176px at 3:4.
  • Mobile web (<768px): same top nav condensed to hamburger + logo + search + coin/avatar. Rails at 132px. Gutter space-4.
  • Posters are always 3:4 (poster-aspect). v1 used 2:3, but every measured competitor sits at 0.72–0.77.
  • Never ship a non-responsive layout (GoodShort renders desktop DOM at 390px and forces horizontal scroll). That is the explicit anti-pattern.

Navigation

Top nav only; there is no bottom tab bar anywhere sitewide (0 of 4 live-checked competitors use one).

  • Desktop: logo · Home · Genres · (spacer) · search · history · language · coin pill · avatar/Login · "Top Up" CTA.
  • Mobile: hamburger · logo · (spacer) · search · coin pill.

The player is a pushed page with a back button. Language selection stays in the primary nav, as on every competitor.

Player

  • Desktop: 9:16 video centred in a full-height surface-000 letterbox column. A side-panel-w (416px) panel sits on the right with breadcrumb, "EP 1 · Title", synopsis with "More", tag chips, a like/save/share row, and the EpisodeGrid (ReelShort/DramaBox/NetShort, research/live-player-nav.md).
  • Mobile: video edge-to-edge full width. Below it: title, then a collapsed "All episodes" row that expands into the grid.

A persistent app-download nudge is allowed, but it must be non-blocking (a dismissible corner card). It never covers the video.

Paywall

Tapping a locked episode never hits a login wall first: free episodes play without an account (research/monetization.md). A locked episode opens UnlockSheet, a centred modal on desktop and a bottom sheet on mobile. It stacks, top to bottom:

1. Price + balance header.

2. Unlock for N coins (primary).

3. VIP plan cards (weekly / yearly) with a feature checklist.

4. Coin-pack grid.

5. Watch ad to unlock (secondary, capped per day).

6. Payment-method row (Apple Pay / Google Pay / Card).

Checkout happens on the web. Never "download the app to unlock" as the only path.

Default free block: the first 8 episodes are free (our choice inside the observed 5–15 range; tune with data).

Iconography

Outline icons, 1.75–2px stroke. Lock glyph on locked episode tiles (top-right corner, episode number still visible). Play triangle on hero and poster hover. Coin = circle glyph filled vip-gold.

Imagery

Poster art is supplied by content partners and already has the title baked in (see the ReelShort rails). Never overlay our own title text on a poster in rails; the title goes below or appears on hover. A top gradient scrim (black 50% → transparent) is allowed only to keep a corner badge legible. Previews in this DS use flat colour blocks, never redrawn real show art.

States

  • Episode tiles: default (surface-200), current (accent-tinted fill + playing bars glyph), watched (ink-600 number), locked (lock glyph, number stays readable).
  • Insufficient coins: the Unlock CTA stays enabled but relabels to "Top up to unlock" and scrolls the sheet to the coin packs. A disabled grey button is a dead end.
  • Empty history is an illustration plus a "Browse trending" CTA, never "No data".

Known Gaps

  • Not designed: profile/settings, post-purchase success toast, empty-results search state, email-code entry step after "Email me a sign-in code".
  • Light theme: vip-gold was darkened to #946508 for AA text, so the coin glyph reads brown in light theme. If that's unwanted, split a non-text coin-fill token (glyphs don't need 4.5:1).
  • Unverified: logged-in purchase flow (no competitor login was performed during research).
  • No rank #1/#2/#3 badge. ReelShort's CSS has the classes, but no ranking badge was visible on any of the 4 sites checked live, so it's deferred until we see it rendered.

Actions

ButtonPrimary

Pill CTA. primary (accent fill) is the one main action per view; secondary (outline) is the free alternative.

When to use: primary: Play (hero), Unlock for N coins / Top up to unlock (UnlockSheet), Top Up (nav, sm). secondary: Watch ad to unlock.

Anatomy: Pill (height --cta-height, sm = 40px) · optional leading glyph (play / lock / ad) · .label.

Variants & states: primary · secondary · block (full width) · sm. Not enough coins → relabel "Top up to unlock", never disabled (a disabled button is a dead end in the money path).

Tokens used: --primary, --on-accent, --border-soft, --ink-900, --cta-height, --radius-pill.

Consumer provides: label, variant, icon?, onPress.

Do: One primary per view (the nav's Top Up sm is the sole exception).

Don't: Don't fill a button with vip-gold — gold marks VIP offers, not actions.

Commerce

AuthSheet

Sign-in sheet shown only at the first paid action — never before a free episode.

When to use: User taps Pay / Unlock / Subscribe / Watch ad while signed out (research/monetization.md §4: anonymous viewing of the free block, account at first monetized action). Also from the avatar in TopNav.

Anatomy: Title "Sign in to keep your coins" + close · one-line reason · "Continue with Google" · "Continue with Apple" · "or" divider · TextField email · primary "Email me a sign-in code" · Terms/Privacy line.

Variants & states: modal (440px, desktop) · bottom (mobile sheet). After success, return the user to the exact action they started (the pending unlock/purchase), not to Home.

Tokens used: .sd-sheet shell tokens, --ink-900/--surface-000 (light buttons), --primary, --border-soft.

Consumer provides: providers[], onProvider, onEmail, pendingAction.

Do: Text-only provider buttons until we have the official brand assets (never redraw Google/Apple marks from memory). Providers are our own choice — research did not capture any competitor login flow.

Don't: Don't ask for a password — email code keeps sign-up to one field. Don't gate free episodes behind this sheet.

UnlockSheet

The paywall: shown when the viewer opens a locked episode. Centered modal on desktop, bottom sheet on mobile.

When to use: Tap on a locked EpisodeGrid tile, or auto-next reaching a locked episode. Never before a free episode; no login wall in front of it.

Anatomy (top→bottom): price + balance header with close · "Unlock EP N to keep watching" · primary CTA · VIP plan cards (2, vip-surface, selected = gold ring, BEST VALUE badge) + perk checklist · divider · coin-pack grid (4 desktop / 2 mobile, bonus Badge) · secondary "Watch ad to unlock · N left today" · payment row (Apple Pay / Google Pay / Card).

Variants & states: modal (720px) · bottom (full width, max 92vh, scrolls). CTA: balance ≥ price → "Unlock for N coins" (lock glyph); balance < price → "Top up to unlock" (scrolls to packs) — never a disabled button.

Tokens used: --surface-100, --surface-200, --vip-gold, --vip-surface, --on-vip, --primary, --radius-md, --shadow-card.

Consumer provides: price, balance, plans[], packs[], adsLeft, paymentMethods[], onUnlock/onSubscribe/onBuyPack/onWatchAd.

Do: Keep VIP in gold and the Unlock CTA in pink-red — two different offers, two colours (visual-signals.md).

Don't: Don't make "Download the app" the only unlock path (GoodShort/DramaBox dead-ends, live-player-nav.md). Prices shown here are placeholders — set real ones per business decision (open-questions #6).

Content

EpisodeCard

Portrait 3:4 poster tile for rails and grids — the core content unit on Home and Genre pages.

When to use: Home rails (Continue Watching, Trending Now, New Release, genre rails), Genre grid.

Anatomy: Poster (--poster-aspect 3:4, width --poster-w 176px desktop / 132px mobile, radius-md, top scrim) · optional Badge top-left · hover play glyph · optional progress bar bottom (Continue Watching) · title (label, 2-line clamp) + caption meta below.

Variants & states: default · badged (HOT/NEW) · in-progress (accent progress bar + "EP 12 of 70") · hover (play glyph).

Tokens used: --poster-w, --poster-aspect, --radius-md, --accent-600, --space-2.

Consumer provides: posterSrc, title, meta, badge?, progress?, href.

Do: Use 3:4 — every measured competitor sits at 0.72–0.77 (research/visual-signals.md); v1's 2:3 is retired.

Don't: Don't overlay our own title on the art — partner posters already carry it; title goes under the poster.

EpisodeGrid

Numbered episode picker — the player's navigation and the visible map of what is free vs locked.

When to use: Desktop player side panel (6 columns in side-panel-w); mobile player inside the expandable "All episodes" row.

Anatomy: Range tabs (1 – 50, 51 – N) + "All episodes ›" · grid of 44px tiles, one per episode.

Variants & states (per tile): default (unwatched, free) · current (fill primary, playing-bars glyph) · watched (ink-600 number) · locked (lock glyph top-right, number stays readable). Clicking a locked tile opens UnlockSheet.

Tokens used: --episode-tile, --surface-200, --primary, --on-accent, --ink-600, --space-2.

Consumer provides: episodes[]{n, state}, current, range, onSelect.

Do: Show locked episodes greyed-with-lock, never hide them — universal across the 4 live-checked sites.

Don't: Don't assume "first N free" in code — ReelShort scatters free teaser episodes (live-player-nav.md); state is per episode from the API.

HeroBanner

Home's featured-title banner: full-bleed art, title, tags, 2-line synopsis, Play CTA, thumbnail strip to switch titles.

When to use: Top of Home only, 2–5 featured titles (web-screenflow.md §1 — 6/6 competitors open Home with this).

Anatomy: Art background + left→right and bottom scrims · body (display-xl, Badge HOT + static Chips, body synopsis clamped to 2 lines, caption meta, ButtonPrimary Play) · thumbnail strip bottom-right (3:4, selected = 2px ring).

Variants & states: desktop (overlay text on art) · mobile (art 4:5 on top, text below on surface-000, display-l).

Tokens used: --poster-aspect, --radius-md, --space-7, --primary, .display-xl/.display-l, .body, .caption.

Consumer provides: items[]{title, art, tags, synopsis, meta}, activeIndex, onPlay.

Do: Keep the scrim — hero text sits on arbitrary art; the text colour is fixed light, not theme ink.

Don't: Don't autoplay trailer audio in the hero.

Navigation

SearchField

Pill search input — opens the Search page from the nav search icon.

When to use: Top of WEB-SEARCH-01 (autofocused). Not inlined in the desktop nav (nav keeps an icon, like every competitor).

Anatomy: Search glyph · text input (body) · clear button (only when non-empty).

Variants & states: empty (placeholder ink-600) · focused (1.5px ink-900 ring) · with query (clear button).

Tokens used: --surface-200, --ink-900, --ink-600, --radius-pill, --space-4.

Consumer provides: value, onChange, onClear, onSubmit.

Do: Search titles, actors and genres from one field; show recent + trending searches before typing.

Don't: Don't use accent for the focus ring — focus is ink-900, accent is reserved for actions.

TopNav

Sitewide top navigation — the only navigation on web; there is no bottom tab bar.

When to use: Every page. Player uses it with no active link.

Anatomy: Desktop: logo · Home · Genres · spacer · search · history · language · CoinPill · avatar/Login · Top Up (sm primary). Mobile (<768): hamburger · logo · spacer · search · CoinPill.

Variants & states: desktop · mobile. Active link = ink-900 label + 2px accent-600 underline (never accent text — 4.29:1 on dark ground, see contrast.md).

Tokens used: --topnav-height, --content-max (inner edges align with page content), --surface-000, --border-soft, --ink-900, --ink-600, --accent-600, --icon-button-md.

Consumer provides: active, balance, user?, onTopUp, language.

Do: Keep language selector in the primary bar (every 12+ language competitor does — research/web-screenflow.md §7).

Don't: Don't add a bottom tab bar or a desktop sidebar — 0 of the sampled competitors use either (research/live-player-nav.md).

Selection

Chip

Small pill for a genre — selectable (Discover filter) or display-only (Drama detail tags).

When to use: Discover's filter row (tappable, single-select) and Drama detail's genre tag row (display-only, not tappable).

Anatomy: Pill container (height --chip-height) · centered .label, no icon.

Variants & states: selected (fill --primary, label --on-accent) · unselected (fill --surface-200, label --ink-900) · display-only (same visual as unselected, but no press state — used on Drama detail).

Tokens used: --primary, --surface-200, --ink-900, --on-accent, --chip-height, --radius-pill.

Consumer provides: label, selected?, interactive (bool).

Do: Use single-select for the Discover filter row — one chip selected at a time, "All" included as a real option.

Don't: Don't make the Drama-detail genre chips look pressable (no press-state styling) — they're metadata, not a filter there.

TextField

Labelled single-line input — currently only the email field on AuthSheet.

When to use: Email sign-in. Search has its own SearchField (pill, icon, clear).

Anatomy: label above · 48px input on surface-200, radius-sm · optional message line below.

Variants & states: default · focus (1.5px ink-900 ring) · error (1.5px accent-600 ring + message in ink-900 — accent text is 4.29:1 on dark, below AA).

Tokens used: --surface-200, --ink-900, --ink-600, --accent-600, --radius-sm.

Consumer provides: label, value, placeholder, error?, type.

Do: Keep the label visible above the field (placeholder is an example, not the label).

Don't: Don't colour the error message red text — the ring carries the colour, the message carries the words.

Status

Badge

Tiny status pill overlaid on a poster or CTA — functional signal, not decoration.

When to use: Hot/New on a poster corner (Discover, Drama-detail episode grid) · coin-cost badge on a locked episode tile.

Anatomy: Small pill (height --badge-height) · label only (Hot, New, or a coin count like 12).

Variants & states: status (HOT/NEW, bonus +25% on coin packs — fill --accent-600, label --on-accent) · vip (VIP, BEST VALUE — fill --vip-gold, label --on-vip).

Tokens used: --accent-600, --on-accent, --vip-gold, --on-vip, --badge-height, --radius-pill.

Consumer provides: kind (hot|new|coin), value? (coin count).

Do: Keep this the only place besides ButtonPrimary that fills solid accent-600 — it must stay rare enough to read as a signal.

Don't: Don't add a third badge colour (e.g. green "Free today") without a documented reason; no rank #1/#2/#3 badge until observed live (README §Known Gaps).

CoinPill

Coin balance pill — keeps the balance visible before any unlock moment.

When to use: TopNav (desktop + mobile), UnlockSheet header (price/balance).

Anatomy: Gold coin glyph (vip-gold) + balance (label) on surface-200 pill.

Variants & states: nav (height --coin-pill-height) · zero (shows 0, still visible — never hide the pill at zero).

Tokens used: --vip-gold, --surface-200, --ink-900, --coin-pill-height, --radius-pill.

Consumer provides: balance, onPress (opens coin store — WEB-STORE-01, not yet designed).

Do: Coin glyph is always vip-gold (v1 used accent — changed after research/visual-signals.md).

Don't: Don't render a bare number without the glyph.