Specification · 0.3.0

Mockup Spec

The rulebook for mockup HTML consumed by the Whyso Design System — canonical class names, data attributes, surfaces, and container rules. Written for designers, developers, and AI mockup authors.

Public spec v0.3.0
Also available as raw Markdown at https://whyso.design/mockup-spec.md — intended for AI tooling and CLI importers.

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 slotvalue 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 in structure.sitemap and patterns is reserved for genuinely-reusable sections. Back-compat: a 0.2 manifest with pages in patterns still imports — the hub folds those entries into structure.sitemap automatically (mapping block_slugblock, field_valuescontent) and leaves patterns empty. New mockups should use structure directly.

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:

  • :root brand-token declarations (Section 2a) — moved to manifest.brandspec
  • New-block declaration comments (Section 5) — moved to manifest.new_blocks
  • Per-block override styles in <style> tags — moved to manifest.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-block is mandatory and MUST be the block slug. It's how the importer identifies the block.
  • The slug is kebab-case, singular (cta-strip, not cta-strips).
  • data-surface sets 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-layout is the block-level layout variant. Its allowed values are block-specific; see the library reference below.
  • data-container picks 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: 16)
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: 16)
grid Flexible card/icon grid — (uses data-columns: 16)

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 include alt. 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:

  1. Recognise data-block="hero" as the library Hero.
  2. Extract data-surface, data-layout, data-container, and the inline --hero-bg-image.
  3. Match visible content to known fields (hero__eyebroweyebrow, hero__headingheading, etc.).
  4. 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 patterns into a dedicated structure block — 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 persists structure to Project.structure_spec on import, so styling/build can be scoped to exactly the blocks a site uses. patterns is reserved for genuinely-reusable sections. Back-compat: 0.2 manifests with pages in patterns still import — the hub folds them into structure.sitemap (block_slugblock, field_valuescontent).
  • 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 via data-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.dev to whyso.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.