Making a style

Write a style by hand, from its folder, style.json, style.css and the kit, taste.md, and refs, to proving, checking, and packing it.

From an empty folder to a proved, checked zip. Every field is in package files; the built-in styles under styles/ (bubble, conspiracy, launch, poster, swiss, terminal) are complete examples.

1. The folder

my-style/
  manifest.json
  style.json
  style.css        the kit's classes, styled
  taste.md
  refs/            three to eight stills (optional, for reviewers)
  vectors/         the style's own marks, as SVG (optional)
  fonts/           the style's own faces, each folder with its licence (optional)
  components/      working scenes for the style's own pieces, to copy (optional)
  proof/           scene.html, and what rapid style proof renders from it

Allowed file types: .json, .css, .html, .md, .txt, .svg, .png, .jpg, .jpeg, .webp, .mp4, .wav, .woff2, .woff, .ttf, .otf. The packed zip is at most 25 MiB.

2. manifest.json

Start with the identity; leave files empty and let the check fill it.

{
  "kind": "style",
  "name": "my-style",
  "version": "0.1.0",
  "schema_version": 1,
  "description": "Big tight type on a warm field, one accent, exits off the edge.",
  "tags": ["editorial", "minimal"],
  "formats": ["reel", "square"],
  "fonts": [
    { "family": "Helvetica Neue", "weight": 700, "substitutes": ["Helvetica", "Arial"] }
  ],
  "files": []
}

A name is lowercase a-z, 0-9, and -, starting with a letter, 2 to 48 characters. A version is semantic (1.2.0). Every format in formats must also appear in style.json's formats.

3. style.json

styles/swiss/style.json:

{
  "schema_version": 1,
  "tokens": {
    "palette": {
      "field": "#F5F4F0",
      "paper": "#FFFFFF",
      "ink": "#0E0E0E",
      "accent": "#E3241B",
      "red": "#E3241B",
      "cool": "#C9C8C3",
      "screen": "#C9C8C3",
      "grey": "#6B6A66",
      "white": "#FFFFFF"
    },
    "fonts": {
      "display": { "family": "Helvetica Neue", "weight": 700, "substitutes": ["Helvetica"] },
      "ui": { "family": "Helvetica Neue", "weight": 400, "substitutes": ["Helvetica"] },
      "label": { "family": "Helvetica Neue", "weight": 500, "substitutes": ["Helvetica"] },
      "voice": { "family": "Helvetica Neue", "weight": 700, "substitutes": ["Helvetica"] }
    },
    "type_scale": { "hero": 360, "title": 120, "body": 38, "small": 32 },
    "leading": { "min": 0.8, "max": 1.27 },
    "margins": { "x": 72, "y": 72 }
  },
  "motion": {
    "exit": "off-edge",
    "easing": {
      "in": "outExpo",
      "settle": "outQuart",
      "out": "cubic-bezier(0.7, 0, 0.84, 0)",
      "drift": "linear"
    },
    "transitions": ["cut", "columns", "slide"],
    "pacing": { "line_hold": 0.7, "shot": 2.4, "words_per_second": 3.4, "chars_per_second": 36 }
  },
  "sound": {
    "cues": {
      "rule": "hat",
      "grid": "click",
      "word": "click",
      "land": "kick",
      "block": "kick",
      "number": "sub",
      "count": "click",
      "columns": "whoosh",
      "plate": "rim",
      "form": "kick",
      "horizon": "pad",
      "enter": "whoosh",
      "exit": "whoosh",
      "settle": "pad"
    },
    "colors": { "red": "kick", "accent": "kick", "ink": "rim", "cool": "keys", "screen": "keys", "grey": "keys", "paper": "room", "white": "room", "field": "room" },
    "tempo": 104
  },
  "components": {
    "rule": { "heavy": 16, "hair": 2 },
    "columns": { "stagger": 0.07, "dur": 0.55, "edge": 2 },
    "display": { "tracking": -0.05, "lead": 0.86 }
  },
  "formats": {
    "reel": {},
    "square": { "type_k": 0.8 },
    "wide": { "type_k": 0.8, "margins": { "x": 96, "y": 72 }, "grid": { "columns": 12, "gutter": 24, "baseline": 8 } }
  },
  "grid": { "columns": 6, "gutter": 24, "baseline": 8 },
  "video": { "steps": { "hero": 360, "number": 640, "title": 120, "caption": 52, "label": 32 }, "hero_ratio": 3 }
}
  • palette: colours by name. Name them by their job; the kit expects field, ink, accent, paper (a card's ground), and cool (a second colour), and you may add more. Scenes see each as --rapid-color-<name>, and a cue's color names one.
  • fonts: faces by role (display, voice, ui, and any more you name, such as label), spelled as the system names them or as your @font-face declares them (see fonts in a style); list only substitutes you'd accept in a finished video. Scenes see each as --rapid-font-<role>.
  • type_scale: pixels at the format's base width; the kit expects hero, title, body, and small, as --rapid-size-<step>. margins: the insets everything sits inside. leading: the range of line heights.
  • motion: how things leave, eases by role (as --rapid-ease-<role>), transitions, and the pacing every scene times its lines by.
  • sound: cue kinds to instruments, palette names to instruments, and an optional tempo. Cue kinds are what scenes emit; the instruments are kick, hat, pad, sub, keys, rim, room, click, and whoosh, and rapid style check refuses any other.
  • components: free-form parameters a scene's scripts may read from window.rapidStyle; most styles leave it empty and put the look in style.css.
  • formats: every format the style supports, with optional margins, type_k (a factor on the type scale), and grid. A format not listed isn't supported.
  • grid: columns between the margins, their gutter, and an optional baseline step, as --rapid-grid-*; the grid check holds key-frame text to it.
  • video: a type scale for video (steps, as --rapid-video-<step>) and hero_ratio, the least the hero may be over the body text, which the scale check holds key frames to (default 3).

4. style.css

Every style styles the kit: field, field-dark, display, title, body, small, voice, card, card-tile, card-name, card-sub, and accent. Style them from the custom properties, so the type scale and margins follow the format. Part of styles/terminal/style.css:

.field {
  background-color: var(--rapid-color-field);
  background-image: repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.22) 0 1px, transparent 1px 4px);
  color: var(--rapid-color-ink);
  font-variant-ligatures: none;
  font-feature-settings: "zero" 1, "tnum" 1;
  text-shadow: 0 0 10px color-mix(in oklab, currentColor var(--tc-glow), transparent);
}
.display {
  margin: 0;
  font-family: var(--rapid-font-display);
  font-weight: 800;
  font-size: var(--rapid-size-hero);
  line-height: 0.9;
  letter-spacing: -0.04em;
  white-space: pre;
  font-feature-settings: "zero" 1, "tnum" 1;
}
.voice::before { content: "$ "; color: var(--rapid-color-accent); font-weight: 700; }
.accent { background: var(--rapid-color-accent); color: var(--rapid-color-field); text-shadow: none; box-decoration-break: clone; -webkit-box-decoration-break: clone; }

The kit sets the look: colour, face, weight, size, leading, tracking, case, corners, borders. Scenes set layout, so never position anything here. A url() names one of the style's own files, which is inlined; @import and outside addresses are refused. rapid style check refuses a style.css that leaves a kit class unstyled.

Fonts in a style

A style may carry its own faces, so it renders the same on every machine. Put each family's files in its own folder under fonts/, with the full text of its licence beside them, and declare each face in style.css with url("rapid:style/<path>"):

fonts/
  display/
    display-semibold.woff2
    OFL.txt
@font-face {
  font-family: "Display Serif";
  font-weight: 600;
  src: url("rapid:style/fonts/display/display-semibold.woff2") format("woff2");
}
  • Only free licences. The SIL Open Font License, Apache 2.0, or the Ubuntu Font Licence, in full: OFL.txt (or LICENSE.txt, LICENCE.txt, LICENSE.md, LICENCE.md, UFL.txt) in the same folder as the font. rapid style check refuses a font with no licence beside it, or one whose licence file is not a free licence's full text. A face you may not redistribute stays on your machine and out of the style.
  • Declared, not installed. The page inlines each file as a data: address, so nothing is fetched. Every face is loaded before frame 0, and the fonts check counts it as loaded like an installed one.
  • Name it in style.json as the @font-face names it, with its weight and style, and in the manifest's fonts.
  • Keep it light: subset to the characters videos use and ship .woff2. The built-in poster carries six faces in under 140 KB.

5. taste.md

The judgment rules, in plain words. Reviewers read it beside the contact sheet; builders read it before they draw. The opening of the swiss style's:

# swiss: taste

International Typographic Style as printed in the eighties and nineties: cultural posters,
annual reports, exhibition and signage systems. Its look is a method: the grid is decided before
anything is drawn, then every block is dropped onto grid lines, so each position looks like the
only possible one. When a frame looks retro, the surface was copied and the method was not.

Three moves make it:
1. **One enormous thing and a small exact column of text,** tied by a shared grid line, with the
   white space between them as the third element.
2. **Type as image.** A numeral or a word so big it is a shape: cropped by the sheet's edge,
   or turned upright to run the height of the columns.
3. **The visible system.** A heavy rule opens the sheet, a colophon names it, sections and
   sheets are numbered (`03 / 06`), and on information frames the grid itself is drawn.

This is a starting point, not a cage. The pieces in `components/` (display type set by its ink,
the bleed numeral, the column wipe, the ruled table, block chart, plate, flat form, horizon,
colophon, end bar) are meant to be copied, retuned, and recombined to fit what a video has to
say. Keep the method; vary the compositions.

## Checkable
- One family, Helvetica Neue: bold for display, regular for text, medium for labels and
  figures. No italic, no second family.
- Flush left, ragged right. Never centred, never justified.

Write rules a reviewer can see broken in a still. "Feels premium" isn't a rule; "type sits on a field of one colour, never on a gradient" is. Say what the style never does as plainly as what it does, and keep it to a page.

6. components/

A look usually needs pieces the kit doesn't have: red string between pins, a wave that floods the frame, a lid that opens, a word built from block cells. A style can't carry scripts, so each piece ships as a working scene, components/<name>.html: it links rapid:style, shows the piece alone, and keeps its behaviour in one marked <script> block that starts with a SETTINGS object. A builder copies that block into a scene and changes the settings, or writes a variation. Its look lives in style.css (classes with a short prefix of the style's) and its pictures in the style's folders, loaded as rapid:style/<path>. List them in components/README.md, one line each. Write them as starting points, not a cage: the style's rules in taste.md are what a variation must keep.

7. refs/

A handful of stills that show the look at its best: a type frame, a busy one, a quiet one, a transition mid-way. Reviewers compare contact sheets against them. Use frames you made or have the rights to.

8. Prove it

The proof is the style's sample and its regression test. A style needs one to be shared in rapid's repository; installing and using a style don't.

Write proof/scene.html: an HTML scene, as in writing scenes, that links rapid:style and uses every kit class. Copy styles/swiss/proof/scene.html to start, or a piece from a built-in style's components/. Then render it:

rapid style proof ./my-style

It renders the scene in the style, in the first format the manifest lists (--format picks another), and writes:

proof/example.mp4     the example, as a phone copy capped at 4 MB
proof/poster.png      the poster: the scene's last frame
proof/golden-1.png    golden frames at a quarter, half, three quarters, and 95% of the scene
proof/golden-2.png    (--times 0.5,1.2 picks the seconds instead)
…

Then it records them in the manifest's proof and refreshes its file list:

"proof": {
  "scene": "proof/scene.html",
  "video": "proof/example.mp4",
  "poster": "proof/poster.png",
  "golden": [{ "t": 1, "path": "proof/golden-1.png" }, { "t": 2, "path": "proof/golden-2.png" }]
}

To see whether the style still draws the same, render the golden frames again and compare:

rapid style proof ./my-style --check

It prints a line per frame, ok proof/golden-1.png at 1s: 0.00, with the mean difference per channel from 0 to 255, and changes nothing. A frame that differs by more than 1.5 is DIFF, and the command exits 1. Run it after an engine update, when nothing should change. After you change style.css or style.json on purpose, run rapid style proof again to make new golden frames.

9. Check it

rapid style check ./my-style --write

--write recomputes the manifest's files list (every file's path, sha256, and size), then checks the package: the manifest and style.json against their schemas, style.css styling the kit, every file listed and matching its hash, no file of a type that isn't allowed, no stylesheet reaching outside the package, the size limit, and the formats agreeing. It prints my-style@0.1.0 ok <sha256>, or every reason it failed and exits 1. Run it again after any edit.

To check as a pull request is checked, with the proof required:

rapid style check ./my-style --publish

10. Pack, install, use

rapid style pack ./my-style                  # writes my-style-0.1.0.zip
rapid style add ./my-style                   # installs it in ~/.rapid/styles/my-style/0.1.0/
rapid style use my-style --project ~/videos/launch   # pins it in the project's rapid.lock
rapid video ~/videos/launch --style my-style

A version, once installed, never changes: to change the style, raise its version. To share it, see sharing styles.

Formats

A format is packaged the same way, with kind: "format" in its manifest and a format.json (see Format), through rapid format check, pack, add, and use. A format has no stylesheet and needs no proof. The built-in ones in formats/ are complete examples.