Mockup Spec — Whyso Design System
Version: 0.3.0 · hosted at https://whyso.design/mockup-spec
This document is the contract between a designer (human or AI) producing a client site mockup and the Whyso hub that ingests it. Mockups written against this spec drop straight into the platform: tokens become BrandSpec + bindings, blocks become BlockDefinitions, the site's pages become a StructureSpec stored on the project, reusable sections become Patterns, scoped CSS becomes ProjectBlockStyle overrides.
If you're a Claude instance being asked to build a client site mockup, follow this document. If you deviate, either declare the deviation in the manifest's deviations array OR be aware that a human will have to reconcile by hand.
How Mockup Spec relates to the other specs
Mockup Spec is design-first — it's how a finished visual mockup hands itself to the hub. On ingest it resolves into the four open standards:
| Manifest key | Resolves into | Standard |
|---|---|---|
brandspec + bindings |
The project's brand tokens | BrandSpec |
structure |
The project's sitemap + per-page block sequences | StructureSpec |
new_blocks |
Draft block definitions | BlockSpec |
The structure block is a StructureSpec document (same sitemap / archetypes shape, same block-reference grammar). Mockup Spec just lets you ship it inline alongside the brand tokens and visual HTML, instead of as a separate file. The hub persists it to Project.structure_spec on import.
0. The manifest — structured metadata in one block
Every mockup file ships with one embedded JSON manifest in a <script type="application/whyso-mockup">…</script> block at the top of the document. This is the structured contract; the visual HTML below renders the same composition for human review. The hub parses the manifest first, walks the visual DOM second, and reconciles the two.
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<title>Mockup — RCI Group</title>
<script type="application/whyso-mockup">
{
"version": "0.3",
"project": {
"name": "RCI Group",
"slug": "rci-group",
"client": "RCI Holdings Ltd",
"url": "https://rci.example.com"
},
"brandspec": {
"colours": [
{ "hex": "#1b1c26", "name": "Charcoal", "tags": ["primary"] },
{ "hex": "#3aa17e", "name": "Health green", "tags": ["health"] },
{ "hex": "#f28b3a", "name": "Care orange", "tags": ["care"] },
{ "hex": "#5b54c9", "name": "Justice indigo","tags": ["justice"] },
{ "hex": "#e94e8d", "name": "Insight pink", "tags": ["insight"] }
],
"typography": {
"heading": { "family": "Plus Jakarta Sans", "weight": 700 },
"body": { "family": "Inclusive Sans", "weight": 400 }
}
},
"bindings": {
"colours": [
{ "slot": "primary", "value": "#1b1c26" },
{ "slot": "health", "value": "#3aa17e" },
{ "slot": "care", "value": "#f28b3a" },
{ "slot": "justice", "value": "#5b54c9" },
{ "slot": "insight", "value": "#e94e8d" }
],
"fonts": [
{ "slot": "heading", "family": "Plus Jakarta Sans", "weight": 700 },
{ "slot": "body", "family": "Inclusive Sans" }
]
},
"structure": {
"sitemap": [
{
"slug": "home",
"title": "Home",
"blocks": [
{ "id": "b1", "block": "hero", "surface": "dark", "variant": "split-image", "theme": "health", "content": { "heading": "When care is urgent, we move fast.", "ctas": [{ "label": "Book a call", "url": "#book", "style": "primary" }] } },
{ "id": "b2", "block": "feature-row", "surface": "light", "theme": "health", "content": { "heading": "Sector experience" } },
{ "id": "b3", "block": "stats", "surface": "alt", "content": { "items": [{ "number": "12", "label": "Years" }] } },
{ "id": "b4", "block": "cta-strip", "surface": "dark", "variant": "centred", "theme": "health", "content": { "heading": "Talk to the team", "cta_primary_label": "Contact", "cta_primary_url": "#contact" } }
],
"children": [
{
"slug": "about",
"title": "About",
"archetype": "standard-page",
"blocks": [ /* … */ ]
}
]
}
],
"archetypes": [
{ "slug": "standard-page", "name": "Standard Page", "blocks": [ /* default block sequence pages of this type inherit */ ] }
]
},
"patterns": [
{
"slug": "sector-cta",
"title": "Sector CTA band",
"description": "Reusable CTA strip used across several pages — declared once, referenced by slug.",
"blocks": [
{ "block": "cta-strip", "surface": "dark", "theme": "health", "content": { "heading": "Talk to the team" } }
]
}
],
"new_blocks": [
{
"slug": "vertical-timeline",
"name": "Vertical Timeline",
"purpose": "Year-by-year company milestones, vertical layout.",
"fields": [
{ "name": "heading", "type": "text" },
{ "name": "items", "type": "repeater", "sub_fields": [
{ "name": "year", "type": "text" }, { "name": "label", "type": "text" }
]}
]
}
],
"overrides": [
{
"block_slug": "hero",
"css": ".hero__heading { letter-spacing: -0.04em; font-weight: 800; }"
}
],
"deviations": [
"Hero uses a custom heading weight not in the BrandSpec scale — applied as an override."
]
}
</script>
</head>
<body>
<!-- Visual HTML below, with each block wearing data-whyso-block-instance="b1" linking back to the manifest -->
</body>
</html>
Manifest field reference
| Key | Required? | Purpose |
|---|---|---|
version |
yes | Spec version. Match the doc's version (e.g. "0.3"). |
project |
yes | Project identity — name / slug / client / url. The slug becomes the hub's project slug. |
brandspec |
yes | Colours + typography. Structured the same as the Dashboard's BrandSpec export, so it round-trips without conversion. |
bindings |
optional | Token bindings — explicit slot → value mappings. Custom slots (e.g. RCI's sector colours) declared here become --brand-{slot} declarations + .theme-{slot} utility classes on deploy. |
structure |
yes (≥1 page) | The site's StructureSpec: sitemap (a tree of pages, each a sequence of block references) plus optional archetypes (named default block sequences pages can inherit). Persisted to Project.structure_spec. See the block-reference grammar below. |
patterns |
optional | Reusable sections only — a composition used on more than one page, declared once and referenced by slug. The hub creates one Pattern row per entry. (Do not list pages here; pages live in structure.) |
new_blocks |
optional | Block slugs the library doesn't have yet. Each entry seeds a draft BlockDefinition. The mockup's visual HTML for these blocks defines their shape. |
overrides |
optional | Per-block CSS escape hatch. Each entry becomes a ProjectBlockStyle row applied at deploy time. |
deviations |
optional | Free-text notes for the human reviewer. Surfaces in Design Import's briefing UI. |
Migrating from 0.2: in ≤0.2 each page was an entry in
patterns. As of 0.3, pages live instructure.sitemapandpatternsis reserved for genuinely-reusable sections. Back-compat: a 0.2 manifest with pages inpatternsstill imports — the hub folds those entries intostructure.sitemapautomatically (mappingblock_slug→block,field_values→content) and leavespatternsempty. New mockups should usestructuredirectly.
Block-reference grammar (shared with StructureSpec)
Each entry in a page's blocks array — and in a pattern's blocks array — is a block reference:
| Key | Required? | Purpose |
|---|---|---|
block |
yes | The block slug (library slug, or a new_blocks slug). (Legacy block_slug still accepted on import.) |
id |
optional | Stable instance id, matched to data-whyso-block-instance in the visual HTML. |
surface |
optional | Visual surface: light / alt / dark / image. Mirrors the section's data-surface. |
variant |
optional | Block layout variant (the block's data-layout value, e.g. split-image). |
layout |
optional | Container/layout hint where a block distinguishes it from variant. |
theme |
optional | Colour theme slot (e.g. a sector colour like health) → .theme-{slot}. |
content |
optional | Field values for this instance. (Legacy field_values still accepted on import.) |
Sitemap node shape
| Key | Required? | Purpose |
|---|---|---|
slug |
yes | Page slug (unique within the site). |
title |
optional | Human page title (defaults to slug). |
archetype |
optional | A structure.archetypes[].slug this page inherits its default block sequence from. |
blocks |
optional | This page's block references, in render order. A page may inherit from an archetype, override with its own blocks, or both. |
children |
optional | Nested child pages (same node shape, recursive). Expresses the navigation hierarchy. |
Block-instance linking
Every <section> in the visual HTML carries data-whyso-block-instance="<id>" matching a block reference's id in the manifest. The parser cross-references the two:
- Manifest says
"id": "b1","block": "hero","content": {…} - Visual HTML at
data-whyso-block-instance="b1"confirms the block's slug + lets the parser sanity-check field rendering
The manifest is authoritative. If the visual HTML's content drifts from the manifest's content (typo in copy, etc.), the manifest wins on import and a deviation note flags the divergence. This is by design — Claude can author a clean manifest first and let the visual HTML follow, or vice versa, but the structured data is what the hub stores.
What the manifest replaces
Section 10's HTML-comment metadata block is deprecated as of 0.2 (still parsed for back-compat). New mockups should use the manifest exclusively. As of 0.3, pages-as-patterns is also superseded by the structure block (legacy form still folded in on import). The manifest also supersedes:
:rootbrand-token declarations (Section 2a) — moved tomanifest.brandspec- New-block declaration comments (Section 5) — moved to
manifest.new_blocks - Per-block override styles in
<style>tags — moved tomanifest.overrides
The visual HTML still uses the data-attribute wrapper contract (Section 3) for rendering — manifest covers structure, HTML covers presentation. Both are required.
1. Architectural context — how the output is ingested
The hub expresses a design in three layers:
| Layer | Owns | What you (mockup author) produce |
|---|---|---|
| Structure | Blocks: their HTML shape, fields, variants | One <section> per block, with known slugs |
| Brand tokens | Colours, typography, spacing, radii | CSS custom properties in :root |
| Visual treatment | Per-client overrides on top of blocks | CSS rules scoped to .{block-slug} |
Your mockup's HTML + CSS gets parsed, mapped, and stored across these three layers. The cleaner it is, the less reconciliation the onboarding tool has to do.
2. Tokens — :root contract
Every mockup starts with a :root block declaring brand + semantic tokens. Two layers are required, because the hub separates brand identity from the design system proper.
2a. Brand layer (imported into BrandSpec)
Name these exactly. These are what per-project overrides touch.
:root {
/* Brand palette — 4 named slots, plus neutrals */
--brand-primary: #a46dff;
--brand-secondary: #1a1744;
--brand-accent: #eefa85;
--brand-highlight: #ede2ff;
--brand-black: #000000;
--brand-white: #ffffff;
/* Brand neutrals — nine-step greyscale (optional; the hub has a default) */
--brand-neutral-100: #f5f7fa;
--brand-neutral-200: #e8eaed;
/* ... through --brand-neutral-900 */
/* Typography — family + weights */
--brand-font-heading: 'Inter', system-ui, sans-serif;
--brand-font-body: 'Inter', system-ui, sans-serif;
--brand-font-heading-weight: 700;
--brand-font-body-weight: 400;
}
2b. Semantic layer (the hub generates this, but mockups may declare aliases)
If your mockup uses convenient names like --color-primary, alias them to the brand layer:
:root {
--color-primary: var(--brand-primary);
--color-primary-hover: color-mix(in srgb, var(--brand-primary) 85%, black);
--color-accent: var(--brand-accent);
--color-bg: var(--brand-white);
--color-bg-alt: var(--brand-neutral-100);
--color-bg-dark: var(--brand-neutral-800);
--color-text: var(--brand-neutral-900);
--color-text-muted: var(--brand-neutral-600);
--color-text-inverse: var(--brand-white);
--color-border: var(--brand-neutral-200);
}
2c. Fluid scales (the hub owns these — don't invent new ones)
The hub's TokenGenerator emits 11 type steps (--text-xs → --text-6xl) and 10 space steps (--space-3xs → --space-4xl), both clamped 320px → 1440px. Use these in your mockup — don't roll your own. If the design needs a step that doesn't exist, say so in a top-of-file comment.
Radii the hub ships: --radius-xs, --radius-sm, --radius-md, --radius-lg, --radius-xl, --radius-pill, --radius-circle.
Container widths: --container-narrow (640px), --container-standard (1200px), --container-wide (1440px), --container-max (1600px).
3. Block structure — the wrapper contract
Every block is a <section> following this exact shape:
<section class="section {slug}"
data-block="{slug}"
data-surface="{light|alt|dark|image}"
data-layout="{variant-value}"
data-container="{full|wide|standard|narrow}">
<div class="container container--{width}">
<div class="{slug}__inner">
<!-- content here, using .{slug}__* element classes -->
</div>
</div>
</section>
Notes:
data-blockis mandatory and MUST be the block slug. It's how the importer identifies the block.- The slug is kebab-case, singular (
cta-strip, notcta-strips). data-surfacesets the visual surface (background + text colour palette). Styling for each surface lives in the theme, so don't hand-roll colour rules — use these attribute values.data-layoutis the block-level layout variant. Its allowed values are block-specific; see the library reference below.data-containerpicks the inner content width. Match the content's natural reading/presentation width; the<section>itself is always full-bleed.- The inner div uses the
container container--{width}pair. Both classes must be present even though the modifier could technically stand alone. - Element classes follow BEM:
.{slug}__element, single underscore. Never use__element__sub. For sub-elements that live inside a repeating item, use.{slug}__item-{sub}(hyphen, not second underscore).
4. Library reference — known block slugs
These blocks already exist in the hub. Match your block to one of these slugs if the content fits, even approximately. The hub will take care of structure/fields; your job is just to get the shell right and declare which variants you're using.
| Slug | Purpose | Allowed data-layout |
|---|---|---|
hero |
Page-opening hero | centred, left, split-image |
feature-row |
Image + text side by side | — (use data-image-position instead: left, right, top) |
cta-strip |
Call-to-action band | centred, split |
testimonial |
Single quote or carousel | single, carousel |
stats |
Numeric highlights | — (uses data-columns: 1–6) |
faq-accordion |
Q&A list | single-column, two-column |
logo-bar |
Partner / client logos | — (uses data-size: sm, md, lg) |
rich-text |
Long-form prose | — (uses data-width: narrow, standard, wide) |
article-feed |
Recent posts query loop | — (uses data-columns: 1–6) |
grid |
Flexible card/icon grid | — (uses data-columns: 1–6) |
Surface values — universally supported on every block: light, alt, dark, image. When surface is image, also set an inline style style="--{slug}-bg-image: url(...)" so the importer can capture the background image.
5. New blocks — declaring something the library doesn't have
If your design calls for a block slug not in the table above, declare it at the top of the file with a comment:
<!--
NEW BLOCK: numbered-steps
Purpose: 3-up list of numbered steps with accent-circle number badge
Fields:
- heading (text)
- intro (textarea)
- items (repeater: number auto, title, description)
Variants:
- data-layout: columns-3, columns-2
- data-surface: light, alt, dark
-->
Then build the section using the same wrapper contract. The importer flags it, a human confirms the fields, and the hub adds a new BlockDefinition.
Keep new blocks generic where possible — if it's reusable across future clients, flag it for the global library (project_id = null). If it's truly one-off, it'll be scoped to the project.
6. CSS discipline
Scope everything to the block slug
/* Good — scoped */
.feature-row__image { ... }
.feature-row[data-image-position="right"] .feature-row__inner { flex-direction: row-reverse; }
/* Bad — global */
.image { ... }
.reverse { ... }
BEM — single underscore
/* Good */
.faq-accordion__item { ... }
.faq-accordion__item-question { ... } /* hyphen for sub-element inside item */
/* Bad */
.faq-accordion__item__question { ... } /* double underscore */
No sizing/colour values in media queries
One @media (min-width: 48rem) per block, structural only (layout shifts like grid columns, flex direction). All sizing comes from the fluid --text-* / --space-* tokens.
Surface colours come from data-surface → theme CSS
Don't write block-specific background-color / color. Use the surface attribute and let the theme's semantic layer handle it.
7. Images
- Real images:
<img src="..." alt="...">. Always includealt. For decorative,alt=""is correct. - Placeholders (demo mockups where the image is TBD):
<div class="img-placeholder">LABEL</div>. The importer treats these as "image slot, asset TBD" and leaves the corresponding ACF image field empty. - Background images: declared via inline CSS custom property on the section, e.g.
style="--hero-bg-image: url('...')". The block's own CSS picks that up.
8. Interactive elements
Use semantic HTML. The importer and generator both assume:
- Accordions:
<details><summary>— not<div>with JS - Buttons:
<button>when it triggers something,<a>when it navigates - Navigation:
<nav>with a<ul>inside - Forms: real
<form>, real<label>, real<input>
Don't roll your own disclosure widgets with Alpine/JS in the mockup. The hub's block generator will wire JS only for blocks that explicitly need it (flagged per-block in the BlockDefinition).
9. Example — a minimal, compliant Hero
<section class="section hero"
data-block="hero"
data-surface="image"
data-layout="left"
data-container="standard"
style="--hero-bg-image: url('/assets/hero.jpg')">
<div class="container container--standard">
<div class="hero__inner">
<p class="hero__eyebrow">Welcome</p>
<h1 class="hero__heading">When everything feels urgent, we bring clarity.</h1>
<p class="hero__subtext">Calm, experienced communications support for teams under pressure.</p>
<div class="hero__actions">
<a class="btn btn--primary" href="#">Book a call</a>
</div>
</div>
</div>
</section>
The importer will:
- Recognise
data-block="hero"as the library Hero. - Extract
data-surface,data-layout,data-container, and the inline--hero-bg-image. - Match visible content to known fields (
hero__eyebrow→eyebrow,hero__heading→heading, etc.). - If the mockup's CSS contains rules scoped to
.hero {...}that deviate from the library's base, stash them as a candidate ProjectBlockStyle override.
No human reconciliation needed.
10. Top-of-file metadata (deprecated 0.2 — use the manifest)
The HTML-comment metadata block is deprecated as of v0.2 in favour of the JSON manifest (Section 0). Still parsed for backwards compatibility, but new mockups should declare project / client / URL inside manifest.project instead. The hub's importer warns when a mockup uses the comment form and a manifest isn't present.
<!-- legacy 0.1 form — still parsed but discouraged in 0.2+ -->
<!--
PROJECT: Purpose Led Co
CLIENT: The Purpose Led Co. Ltd
URL: https://purposeled.co.uk
-->
Version history
- 0.3.0 — Pages moved out of
patternsinto a dedicatedstructureblock — a StructureSpec document (sitemap of pages, each a sequence of block references, plus optional archetypes) carried inline in the manifest. Block references now use the StructureSpec grammar (block/surface/variant/layout/theme/content) shared with the StructureSpec standard. The hub persistsstructuretoProject.structure_specon import, so styling/build can be scoped to exactly the blocks a site uses.patternsis reserved for genuinely-reusable sections. Back-compat: 0.2 manifests with pages inpatternsstill import — the hub folds them intostructure.sitemap(block_slug→block,field_values→content). - 0.2.0 — Mockup manifest contract introduced. Embedded
<script type="application/whyso-mockup">block at the top of the document carries the structured data (project, BrandSpec, bindings, patterns, new_blocks, overrides). Visual HTML still uses the data-attribute wrapper contract for rendering, with each<section>linked to a manifest entry viadata-whyso-block-instance="<id>". Section 10 (HTML-comment metadata) deprecated. Pattern composition extraction now end-to-end: a mockup with N pages lands as N Pattern rows in the project on import. - 0.1.1 — Hub canonical domain changed from
fortico.devtowhyso.design. No contract changes. - 0.1.0 — Initial spec. Covers tokens, block wrapper contract, library slugs, BEM, images, interactive elements.
This document is the canonical source of truth. If a human or an AI produces a mockup that violates it, we reconcile by hand — but the whole point of this spec is to make that unnecessary. Stick to it.