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), andcool(a second colour), and you may add more. Scenes see each as--rapid-color-<name>, and a cue'scolornames one. - fonts: faces by role (
display,voice,ui, and any more you name, such aslabel), spelled as the system names them or as your@font-facedeclares 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, andsmall, 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, andwhoosh, andrapid style checkrefuses any other. - components: free-form parameters a scene's scripts may read from
window.rapidStyle; most styles leave it empty and put the look instyle.css. - formats: every format the style supports, with optional
margins,type_k(a factor on the type scale), andgrid. A format not listed isn't supported. - grid: columns between the margins, their gutter, and an optional baseline step, as
--rapid-grid-*; thegridcheck holds key-frame text to it. - video: a type scale for video (
steps, as--rapid-video-<step>) andhero_ratio, the least the hero may be over the body text, which thescalecheck 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(orLICENSE.txt,LICENCE.txt,LICENSE.md,LICENCE.md,UFL.txt) in the same folder as the font.rapid style checkrefuses 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.jsonas the@font-facenames it, with its weight and style, and in the manifest'sfonts. - Keep it light: subset to the characters videos use and ship
.woff2. The built-inpostercarries 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.