Theme components: small add-ons, many per site
A theme component is a small installable add-on — a dismissible banner, a sponsor strip in the forum's side column, a "back to top" button, banner images on your categories. Where a theme is a whole look, a component is one piece of furniture, and you can have as many as you like: install them once in your organization, switch each on for the spaces and sites that should wear it, in the order you want, and turn any one off without uninstalling it.
A component is four things, all optional except that it must put something on the page:
CSS — restyles the reader (and only the reader).
HTML in named outlets — markup placed into fixed, documented slots of the reader page: under the header, above a forum's topic list, after every post, on a member card.
Settings — typed values an owner tunes per install (a colour, a message, a link), read by the CSS as
var(--setting-name)and by the HTML as{{settings.name}}.Strings — the words its markup says, per language, as
{{strings.key}}.
The component file
{
"format": "forumulate.theme-component",
"formatVersion": 1,
"meta": {
"name": "Release banner",
"slug": "release-banner",
"description": "A strip under the header announcing the latest release.",
"author": { "name": "Acme Design", "url": "https://acme.example" },
"version": "1.0.0",
"license": "MIT",
"tags": ["banner"]
},
"settings": {
"message": { "type": "string", "default": "Version 4 is out", "maxLength": 120 },
"link_url": { "type": "url", "default": "" },
"tone": { "type": "enum", "default": "brand", "choices": ["brand", "quiet"] },
"background": { "type": "color", "default": "#1e3a5f", "label": "Background" }
},
"strings": {
"en": { "more": "What changed" },
"fr": { "more": "Les nouveautés" }
},
"outlets": {
"header-after": {
"position": "before",
"html": "<p class=\"rb rb--{{settings.tone}}\">{{settings.message}} <a href=\"{{settings.link_url}}\">{{strings.more}}</a></p>"
}
},
"css": ".rb{margin:0;padding:.5rem 1rem;text-align:center;background:var(--setting-background);color:#fff}\n.rb--quiet{background:transparent;color:inherit}\n.rb a[href=\"\"]{display:none}"
}The rules a file is held to — every one refused with a 422 that names the path:
Strict, all the way down. A key the format does not have is refused by name, never dropped.
Outlets come from the registry below. An outlet name that is not in it is refused (
outlets.sidebar-of-dreams).Every reference must resolve. A
{{settings.x}}orvar(--setting-x)that names no declared setting, or a{{strings.k}}that no locale defines, is refused where it is written — so a component never ships a placeholder that silently prints nothing.Defaults fit their declaration. An
enumdefault must be one of itschoices, anumberdefault insidemin/max, astringinsidemaxLength.At most 64 KB, CSS and each outlet's HTML at most 20,000 characters, 30 settings, 20 locales.
meta is the same metadata a theme bundle carries (slug is lowercase letters, digits and hyphens; version is major.minor.patch).
Outlets
An outlet is a named slot the reader draws. A component's HTML for an outlet goes into that slot on every page that has it, with position:
| Where |
|---|---|
| After whatever the reader draws in the slot (and after |
| Before it |
| Instead of the reader's own content — only on an outlet that wraps some; elsewhere, |
Several components in one outlet keep the order you switched them on in, with every before piece first.
An outlet costs nothing until it is used. A slot no component targets renders exactly what it rendered before components existed — no wrapper element, no attribute, no script. When a component fills one, the slot gets one wrapper and each component its own element, both with stable hooks:
Hook | The element |
|---|---|
| An outlet a component has filled |
| One component's markup inside it (by its slug) |
Both are part of the fx-1 hook contract and both are theme.motion.targets keys (outlet, component), so you can animate a component like any other part of the reader.
Some outlets offer facts about what they sit next to as placeholders — a member card offers {{user.website}}, a topic {{topic.title}}. They are escaped like every placeholder, empty when the reader does not know one, and only valid in the outlets that offer them.
Outlets on every type
Every space type has outlets of its own, on each kind of page it draws, besides the eight chrome outlets (header-before … footer-after, front-page-top, front-page-bottom) that every page has. An outlet's surface is the type it belongs to: a status-… outlet appears only on a status page, a legal-… one only on legal documents and the legal register.
Surface | Outlets | Pages with a slot | Wraps something |
|---|---|---|---|
| 16 | index, category, item, hub |
|
| 8 | index, item, hub | |
| 8 | item, category, hub | |
| 8 | index, item, hub | |
| 7 | index, item | |
| 7 | index, item, subpage | |
| 3 | index | |
| 11 | index, item, subpage |
|
| 16 | index, item, subpage, hub |
|
| 9 | index, category, item |
|
| 6 | index, item, subpage | |
| 5 | index, item |
|
A custom space has no surface of its own: its pages are drawn by the docs reader, so they offer the docs-page-…, docs-sidebar-… and docs-rail… outlets, and its /c/<slug> collections offer docs-hub-above-list and docs-hub-below-list. FAQ and careers collections offer those two as well.
Pages. Each outlet names the pages it is on, in one vocabulary for every type: index (the type's front door), item (one post, release, incident, role, document or contact topic), subpage (a page that belongs to an item or to the type but is not its index — an event's agenda, a status history page, a course quiz, a legal document's past version), hub (a listing of one slice: a tag, an author, an archive month, a course module), category (a /c/<slug> collection or a forum category) and every (the chrome). GET /theme-outlets returns each outlet's pages, so a tool can list exactly the slots one page offers.
Wraps. Only an outlet that wraps something the reader already draws (a category's header, the "message sent" confirmation, a locked lesson's card) honours position: "replace". Everywhere else there is nothing to replace, so replace reads as after.
Facts. The Offers column below lists the placeholders an outlet hands a component ({{incident.title}}, {{role.slug}}, {{document.version}}). An outlet on a sub-page that several pages share also offers {{subpage}}, the name of the page it is on (history, agenda, …), so one component can say something different on each.
Once per page, or once per row. An outlet that is not marked as repeating appears exactly once on a page, whatever the page's layout: the status board's status-above-board and the roadmap's roadmap-above-board sit beside the composed front page, not inside one of its bands, so removing a band does not remove your component and placing two bands does not draw it twice. A repeating outlet (forum-post-after-body, blog-card-footer, roadmap-card-footer, status-component-row-after, events-agenda-row-after, contact-topic-after and the other per-row slots) draws your markup once for every post, card or row, each with that row's facts. Write CSS for a repeating outlet with classes, never an id: the markup appears many times on one page.
Extensions in outlets
An installed extension can fill the same outlets as a theme component. It declares them in its manifest:
{
"outlets": [
{ "name": "forum-post-after-body", "position": "after", "minHeight": 48 },
{ "name": "docs-sidebar-end" }
]
}namemust be an outlet from the registry below. Each outlet can be listed once.positionisbeforeorafter(defaultafter). An extension can neverreplacethe reader's own content, so a page still works when the extension's server is down.minHeight(0 to 600 px, default 0) is the space the reader reserves before the extension answers, so the page does not jump when the content arrives.
While the extension is installed and enabled, every space of the organization shows it in those outlets. The surface rules still apply, so a forum outlet only appears on forum pages. The reader asks the extension through the signed proxy after the page loads. It sends a render request with block: "outlet:<name>", and context holds outlet plus the facts that outlet offers (topic.title, post.number, …), the same facts a component placeholder gets. The extension answers with declarative UI. Buttons in that UI send action requests the same way. If the extension does not answer, answers with an error, or answers with nothing, the slot is left empty and the reader sees no error. The console lists the outlets an extension fills on its card.
Extension pieces use the same wrapper ([data-fx-outlet]) as component pieces. Each one is in its own [data-extension-outlet='<slug>'] element, placed before the before component pieces or after the after ones. Turning the extension off or uninstalling it removes it from the reader on the next page view.
The registry
The fixed list, also served as JSON by GET /theme-outlets (no credential needed). A name here is never renamed or removed. Pages says which pages of the type mount the slot: index (the front door), item (one post, release, incident, role or document), subpage (an event's agenda, a status history page), hub (a tag, author or archive listing), category (a /c/<slug> collection or a forum category) and every (the chrome).
Outlet | Surface | Pages | Wraps | Offers | Where |
|---|---|---|---|---|---|
| all | every | Above the site header, full width, on every reader page | ||
| all | every | Directly below the site header, full width, on every reader page | ||
| all | every | The top of the main content area, inside the header and footer | ||
| all | every | The bottom of the main content area, above the footer | ||
| all | every | Directly above the site footer | ||
| all | every | Below the site footer — the very end of the page | ||
| all | index | A space's front page or a site's front door, above the document | ||
| all | index | A space's front page or a site's front door, below the document | ||
| forum | index, category | Forum index and category views, above the topic list | ||
| forum | index, category | Forum index and category views, below the topic list | ||
| forum | index, category |
| Each row of the forum topic list, after the title (both the table and the card list) | |
| forum | category | yes |
| Wraps the heading of a category view (its banner, logo and name) |
| forum | category | The forum’s /categories page, above the category layout | ||
| forum | index, item | The top of the forum side column (index and topic pages) — the column appears only when filled | ||
| forum | index, item | The bottom of the forum side column | ||
| forum | item |
| A topic page, above the title | |
| forum | item |
| A topic page, below the title and its byline, above the first post | |
| forum | item |
| A topic page, between the first post (and its polls) and the replies | |
| forum | item |
| After the body of every post in a topic | |
| forum | item |
| After the author name in every post header | |
| forum | item |
| A topic page, after the last post | |
| forum | hub | yes |
| Wraps the member card on a forum member page |
| blog | index | The blog index, above the list of posts | ||
| blog | item |
| A blog post, above its body | |
| blog | item |
| A blog post, after its body | |
| blog | item |
| A blog post, after suggested posts and the piece page, before the comments; as wide as | |
| docs | item |
| A documentation page, above the article | |
| docs | item |
| A documentation page, after the article | |
| docs | item | Top of the docs left rail, under the brand | ||
| docs | item | Foot of the docs left rail (sponsor walls, promos) | ||
| forum | hub | Forum hub pages (tags, a tag, members, badges, leaderboard, about, search), above the list | ||
| forum | hub | Forum hub pages, below the list | ||
| blog | index | The blog index, below the list of posts and its pager | ||
| blog | index, hub |
| The foot of every post card in a blog list (once per item) | |
| blog | hub | Blog hub pages (archive, an author, a series, a tag, search), above the list | ||
| blog | hub | Blog hub pages, below the list | ||
| docs | item |
| A documentation page, the top of the right rail (above "On this page") | |
| docs | item |
| A documentation page, the foot of the right rail | |
| docs | category, hub |
| A | |
| docs | category, hub |
| A | |
| changelog | index | The changelog index, above the releases | ||
| roadmap | index | The roadmap, above the board | ||
| status | index | The status page, above the board and its bands (once, whichever are placed) | ||
| faq | index | The FAQ, above the questions | ||
| events | index | The events index, above the list of events | ||
| events | item |
| An event page, between the details and the description | |
| events | item |
| An event page, under the register card (beside the description when it stands at the side) | |
| events | item |
| An event page, after the description and the register card, before "More events" | |
| events | index |
| The foot of every event card on the calendar (once per card) | |
| courses | index | The course index, above the lessons | ||
| courses | index | The course landing, under the hero (title, cover and start button) | ||
| courses | item | A lesson, between its title and its body | ||
| courses | item | A lesson, under its body and completion controls | ||
| courses | subpage | A completion certificate page, under the certificate | ||
| careers | index | The careers index, above the open roles | ||
| changelog | index | The changelog index, below the releases and the pager | ||
| changelog | index, hub |
| The foot of every release entry in a changelog list (once per item) | |
| changelog | item |
| A release page, under its title, date and facts, above the notes | |
| changelog | item |
| A release page, in the side column (or under the head when there is none) | |
| changelog | item |
| A release page, after the notes, before the previous/next links | |
| changelog | hub | Changelog hub pages (a tag, people, a person), above the list | ||
| changelog | hub | Changelog hub pages, below the list | ||
| roadmap | index | The roadmap, below the board | ||
| roadmap | index |
| Under the heading of every lane (column) of the board (once per item) | |
| roadmap | index |
| The foot of every item card on the board (once per item) | |
| roadmap | item |
| A roadmap item page, between its title and facts and its description | |
| roadmap | item |
| A roadmap item page, under the vote and status card | |
| roadmap | item |
| A roadmap item page, after the description, before the discussion | |
| status | index | The status page, below the monitors and incident history | ||
| status | index, subpage |
| Under every component row on the board and on a component page (once per item) | |
| status | subpage, item |
| Every status sub-page (history, uptime, maintenance, a component, an incident), under the back link | |
| status | subpage, item |
| Every status sub-page, at the end of its content | |
| status | item |
| An incident or maintenance page, under its title and impact, above the updates | |
| status | item |
| An incident or maintenance page, after the last update | |
| faq | index | The FAQ, below the questions (above the contact card) | ||
| faq | index |
| Under the answer of every question in the FAQ list (once per item) | |
| events | index | The events index, below the list (or calendar) of events | ||
| events | index | yes | Wraps the events index's empty state (no upcoming events). | |
| events | subpage |
| An event's sub-pages (agenda, speakers, a speaker, a session), under the event header | |
| events | subpage |
| An event's sub-pages, at the end of their content | |
| events | subpage |
| Under every session row of an event agenda (once per item) | |
| events | subpage |
| The foot of every speaker card on the speakers page (once per item) | |
| events | subpage |
| A session's page, after its description and stream link, before feedback and recordings | |
| events | subpage |
| A speaker's page, after the bio, before their sessions | |
| courses | index | The course landing, below the lessons (the syllabus) | ||
| courses | item | A lesson, the top of the course rail (the lesson list beside it) | ||
| courses | item | A lesson, the foot of the course rail | ||
| courses | item | yes | Wraps the card a locked lesson shows (enrol or buy to continue). | |
| courses | hub |
| A module page, above its lessons | |
| courses | hub |
| A module page, below its lessons | |
| courses | subpage |
| A module quiz, above the questions | |
| courses | subpage |
| A module quiz, after the questions and the result | |
| courses | subpage | The course checkout page (a learner back from Stripe or PayPal, or redeeming a gift), above the confirmation. The plans themselves are picked on Stripe | ||
| courses | subpage | The course checkout page, below the confirmation and the first-lesson link | ||
| courses | subpage | A learner's own course page ( | ||
| careers | index | The careers index, below the open roles | ||
| careers | index, category |
| Under every role row in a careers list (once per item) | |
| careers | item |
| A role page, under its title and facts, above the description | |
| careers | item |
| A role page, in the side column beside the description | |
| careers | item |
| A role page, after the description, above the application form | |
| careers | item |
| A role page, directly above the application form | |
| careers | item |
| A role page, directly below the application form | |
| careers | item | yes |
| Wraps the "application sent" message. |
| legal | index | An organization's legal register, above the list of documents | ||
| legal | index | The legal register, below the list of documents | ||
| legal | item, subpage |
| A legal document, in its header beside the title and dates | |
| legal | item, subpage |
| A legal document (and each of its past versions), under its title and dates, above the text | |
| legal | item, subpage |
| A legal document, in the side column under the section navigation | |
| legal | item, subpage |
| A legal document, after the text, above the version history | |
| contact | index | The contact page, above the form (and its topic picker) | ||
| contact | index | The contact page, below the form | ||
| contact | index | The contact page, in the side column beside the form (channels, hours, address) | ||
| contact | index, item |
| Under every topic card of the topic picker, and above the form once a topic is chosen (once per item) | |
| contact | index, item | yes |
| Wraps the "message sent" confirmation. |
Settings
| Value | As | Extra keys |
|---|---|---|---|
| Text | A quoted CSS string ( |
|
| A number | The number ( |
|
|
|
| |
| One of | The choice, as a bare word |
|
| A | The colour | |
| An |
| |
| A list of short texts | One quoted string, items joined by |
|
Every setting may carry a label and a description for the console's form.
In HTML, {{settings.x}} prints the value HTML-escaped — a " in a setting cannot close the attribute it was put in. An enum is a safe word, so class="rb--{{settings.tone}}" is the way to make a choice change the look.
Where a value comes from, last one wins: the component's declared default → the value your organization set on the install → an override on the space or site that switches it on. A value that no longer fits (a component upgraded to a narrower range) falls back to the layer below rather than rendering.
Each component's settings are published under its own custom-property names, so two components that both call a setting color — or a component and the theme's own --setting-* — never read each other's.
Strings
strings is locale → key → text. {{strings.key}} prints the reader's language, then its base language (pt for pt-BR), then English, then whichever locale has the key.
What is sanitised, and scripts
HTML goes through the same sanitiser as the header's HTML panels and the footer's HTML slot: no inline
<script>,<style>, form control oron…handler survives, on any host.CSS is sanitised on the way in and scoped to the reader on the way out, so a component can restyle the reader and nothing outside it. It is emitted once per page, after the theme's CSS, so a component wins a tie with the theme.
An external
<script src="https://…">in a component's HTML is treated exactly like one in a header HTML panel: taken out of the markup, loaded only on your own domain, after consent, with the page nonce, and its origin added to the address's script policy. Only a component authored in your organization may carry one. A component that arrives in a theme bundle or from the marketplace is data, like a theme: its scripts are stripped and named, and the marketplace refuses a listing that has any.
Installing and switching on
Installing puts a component in your organization; nothing changes on any page until you switch it on for a space or a site. Switching on is a draft write, like applying a theme: preview it at the reader address with a preview token, then publish the draft. A site's components show on every space under it, before the space's own.
Call | What it does |
|---|---|
| Every installed component, its values, its scripts, and where it is switched on |
| Author or import one: |
| One install |
|
|
| Uninstall. A theme that still names it simply stops rendering it |
| The component file, your values as its defaults, scripts stripped and named |
|
|
|
|
| The same for a site (its draft) |
Installing and editing need themes.manage; switching components on for a space or site needs space.update there, because it changes what its readers see. The list lives in the theme as componentInstalls, which is why it drafts, previews and publishes with the rest of the look.
In theme bundles
A theme bundle can carry its components: format 2 adds a components array of whole component files (format 1 has it empty). Exporting a space or site whose theme switches components on puts them in the bundle, with that address's values as their defaults. Importing or installing a bundle installs its components in your organization (reusing one you already have from the same author) and switches them on in the draft after the ones you already wear — applying a theme never takes a component away.
The marketplace
Components have their own listings beside themes, reviewed the same way: a submission is visible to nobody until a platform administrator approves it, a revision of a live listing waits while the approved version keeps serving, and only a live listing can be featured. Components are free.
Call | What it does |
|---|---|
| Browse without signing in: |
| One listing with its file |
| The component file |
| The same browse, marking what you have installed |
| Install (or upgrade, keeping your values); |
| Your listings / submit an install: |
| Take one down. Organizations that installed it keep their copy |
First-party components
Six, featured, free, and the worked examples for writing your own — none of them runs a line of script:
Slug | What it does |
|---|---|
| A strip under the header with a message and a link; readers dismiss it (a native |
| Turns the topic list's "new" dot into a labelled badge and recolours the unread bubble |
| A wide image banner above every category, with an overlay and the category's name |
| The member's website as a button under their card, then your community's social profiles |
| A floating "back to top" anchor that fades in as you scroll, where the browser supports it |
| Up to four sponsor logos at the top of the forum side column, hidden until one has a link |
In the console
Themes → Components lists what you have installed — with an on/off switch, a settings form drawn from each component's own settings, where it is switched on, and edit, download, publish and uninstall — and lets you switch components on for a space or site in order, as a draft you preview and publish. New component opens an editor with a code area for the CSS and for the HTML of each outlet, the settings and strings, and a live preview. Components → Marketplace browses and installs listings.
Docs blocks (wave D)
Every key is optional; unset is today's look. All are theme.components.* and apply to every space type.
hint.variants — one look per callout style (info, success, warning, danger) or author's word (note, tip, caution, important, check; a word beats the style it folds onto):
Key | Meaning |
|---|---|
| the wash |
| the leading bar and badge tone |
|
|
| body and title ink |
| a glyph name drawn as this style's badge (turns the badge on for it) |
| body px, 12–20 |
A default title per style (label) is not offered: the renderer has no theme.
table — rules (grid today / rows / none), stripe (false turns the zebra off), density (compact), headerBackground(Dark), headerInk(Dark), size (11–18 px), cellPadding {x, y}, border(Dark).
stepper — marker (disc / number / outline / box-mono), line (true or a colour), color(Dark), zeroPad, rule, titleSize.
card — arrow: none / corner / end.
kbd — setting it at all (even {}) draws key caps: background(Dark), border(Dark), ink(Dark), radius, font (mono/body), shadow.
tabs — overflow: "scroll" keeps a long tab strip on one scrolling line for every group that did not choose its own.
expandable gains borderDark, backgroundDark, inkDark.
Round 4: block keys, and setting them per block
Every key below is optional; unset is today's look, except the rows marked fix, which change what an existing page shows. The theme keys are theme.components.* (every space type) and theme.docs.blocks.* (DOCS spaces); each has a block attribute of the same name, listed in Authoring › Round 4 block attributes, with its markdown spelling in Markdown in Forumulate › Round 4.
Theme, page or block: where a value comes from
A block's look is read, nearest first:
the block — its own attribute (
"border": "tile"on onecardsblock);the page —
metadata.themePatch, a patch of theme keys for one page;the space theme —
theme.components.*,theme.docs.*;the look —
theme.docs.look/theme.forum.lookis only a PATCH of these same keys (a look never draws anything a key cannot);the built-in default — today's block.
So two blocks on one page can differ, a page can differ from its space, and any key works under any look. A value (a size, a colour) is published as --x-theme on the reader root and --x-block on the block, and the stylesheet reads var(--x-block, var(--x-theme, today)); a word (a border style, a placement) is drawn by the theme only for blocks that did not choose their own. Which stylesheet loads first never decides the winner.
Scope | Where you set it |
|---|---|
theme |
|
page | the page's |
block | the block's |
The new keys
Key (scope) | Shape | Default | Example |
|---|---|---|---|
| px 12–64 | the 1.35rem glyph (a 28px picture) |
|
| px 32–96 | 40 |
|
|
| 600 |
|
| px 12–24 | the body size |
|
| px 12–24 | 0.9em |
|
| as in round 3 | today's card | now also attributes of a |
|
| unset: no chip |
|
|
|
|
|
| boolean |
|
|
|
|
|
|
|
|
| now also a code block's / group's attribute |
|
|
|
|
|
| the outline / solid pill |
|
|
| the pill's ink |
|
|
|
|
|
|
|
|
|
|
| the brand ink and underline |
|
| a new value of |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| neither filled |
|
|
|
|
|
| 1–6 | 1, under |
|
What each one draws:
showLanguage—"auto"is the round-3 rule: a chip in the block's corner, but none inside a code group (its tab already names the sample) and none on a plain-text block.truedraws it everywhere —TEXTon a plain block, and inside a group (in the sample's bottom corner, clear of the tab strip). A block with a title never draws one (fix): the filename already says what it is.tabStyle: "pill"— the code group's tabs as grey pills in its header; code-onlytabsdraw as a code group, so they take it too. On atabsblock it also pills a plain tab strip.titleIcon— a file glyph before a code block's (or group's) title;titleFont: "ui"sets that title in the interface face.table.wrap—autokeeps headers on one line and words whole, the table scrolling in its frame;wraplets headers wrap and drops the 6ch cell floor;nowrapkeeps every cell on one line.table.more— the collapsed table's "Show more": its fill, ink, face and case, beside round 3'sstyle,alignandfade.tabs.active— the active tab's ink (the underline takes it too);underline: falsekeeps the ink and drops the underline.stepper.marker: "inline-number"— the number before the step's title, at the title's size, muted, with no marker column, so the body runs flush.badgePlacement: "inline"puts a step's badges on its title's line.prompt.phoneTargets— acompactprompt on a phone keeps Copy and this many "Open in …" links in its one row; the rest open from More (a keyboard opens it with Enter or Space; Escape closes it).
Fixes in this round (no key; every page): a one-line code block with no title puts its copy glyph inside the line, at its end, at every width; four described cards with no columns go four across only from a 1000px track (two by two below it); a wide table on a phone fades its end edge while it can still scroll, and a code chip cut by a line break keeps its corners; a picture icon in a border: "tile" card is tiled too, and iconAlign: "center" beside the icon centres the title on it.
Mix and match
None of these is a replica; each is a complete PUT /spaces/:id/theme body that validates as-is, built from the keys above and the ones before them.
A workbench docs site — tiled landing cards, inset code with pill tabs and a sans filename, hooked callouts, a black "SHOW MORE", green tabs with no underline, inline step numbers, bordered collapsibles:
{
"components": {
"card": {
"border": "tile",
"iconAlign": "center",
"tileSize": 48,
"iconSize": 22,
"titleWeight": 500,
"titleSize": 15,
"descriptionSize": 13
},
"code": {
"look": "inset",
"tabStyle": "pill",
"titleFont": "ui",
"titleIcon": true,
"showLanguage": "auto"
},
"hint": { "edge": "hook" },
"table": {
"wrap": "wrap",
"more": {
"style": "solid",
"background": "#111111",
"ink": "#ffffff",
"font": "mono",
"case": "upper"
}
},
"tabs": { "active": { "color": "#16a34a", "colorDark": "#4ade80", "underline": false } },
"stepper": { "marker": "inline-number", "badgePlacement": "inline", "titleSize": 18 },
"expandable": { "style": "bordered" }
}
}A quiet reference under paper — filled borderless cards, a chip on every block, one-line table cells, compact prompts that keep two assistants on a phone with "Open in …" filled:
{
"docs": {
"look": "paper",
"blocks": {
"prompt": { "variant": "compact", "phoneTargets": 2, "copyStyle": "icon", "primary": "open" }
}
},
"components": {
"card": {
"border": "none",
"background": "#f4f4f5",
"backgroundDark": "#18181b",
"titleWeight": 600
},
"code": { "showLanguage": true, "titleFont": "mono" },
"table": {
"wrap": "nowrap",
"rules": "rows",
"more": { "align": "center", "fade": true, "font": "ui", "case": "none" }
}
}
}The bold look, re-voiced — outlined cards with the icon at the start and the CTA at the row's end, flat code, badge callouts with a tight green tip, stacked prompts with a filled Copy:
{
"docs": {
"look": "bold",
"blocks": { "prompt": { "variant": "card", "layout": "stacked", "primary": "copy" } }
},
"components": {
"card": { "border": "card", "iconAlign": "start", "ctaPlacement": "end" },
"code": { "look": "flat", "tabStyle": "underline", "showLanguage": false },
"hint": { "icon": "badge", "variants": { "tip": { "barColor": "#16a34a", "titleGap": 4 } } }
}
}And on one page under any of them, two card grids that differ — the first takes the theme's card, the second names its own:
[
{
"type": "cards",
"attrs": { "columns": 4 },
"content": [
{ "type": "card", "attrs": { "title": "Database", "iconUrl": "https://example.com/db.svg" } }
]
},
{
"type": "cards",
"attrs": {
"variant": "horizontal",
"border": "card",
"titleWeight": 600,
"ctaPlacement": "end"
},
"content": [
{
"type": "card",
"attrs": {
"title": "Get started",
"description": "Five minutes to a running app.",
"cta": "Start"
}
}
]
}
]Round 5: block keys
The same three scopes as round 4 (block > page metadata.themePatch > space theme > look > today). Every key optional; unset is today's look, except the rows marked fix. "Layer" says whether the DOCS default layer sets it. The block attributes are listed in Authoring › Round 5 block attributes.
Key (scope) | Shape | Default | Layer | Example |
|---|---|---|---|---|
| px 160–480 | today's fixed column count |
|
|
| a space token ( | 0.75rem | no |
|
| boolean |
| no |
|
| px 0–120 | the 16:9 band | no |
|
|
|
| no |
|
|
|
| no |
|
|
| today's preview surface | no |
|
|
|
| no |
|
| a new value | — | no | fix: a theme-wide |
| a new value | — | no |
|
| px 10–20 | 0.9rem (14.4px) | no |
|
| 100–900 | the code weight (500) | no |
|
|
|
| no |
|
| 0–6 | all |
|
|
| a glyph | none | no |
|
|
|
| no |
|
| #hex | today's surface | no |
|
|
|
| no |
|
Fixes in this round (no key): an inline code chip never splits open — each piece of a broken chip keeps its frame, and one of 24 characters or fewer never breaks; the code language chip never covers the first line; a header: "tabs" prompt alone in a variant: "card" tab panel drops its own border, radius and fill (one frame, not two).
Mix and match (round 5)
Not a replica — a complete PUT /spaces/:id/theme body:
A component gallery — dense segmented tabs, cards that fold to one column under 220px with a tight 12px row pitch and banded panels with no rule, flat ink code in tables, previews framed with their source, compact prompts that keep one assistant on the desktop and fold with a pill:
{
"components": {
"card": { "minWidth": 220, "gap": { "lg": "sm", "sm": 12 }, "divider": false, "iconBand": 72 },
"tabs": { "style": "pill", "active": { "color": "#0f172a", "colorDark": "#e2e8f0" } },
"table": { "code": "ink", "rules": "rows" },
"preview": {
"border": "#e2e8f0",
"radius": 14,
"background": "#ffffff",
"backgroundDark": "#0f172a"
},
"code": { "showLanguage": "always" },
"apiRef": { "methodStyle": "text" }
},
"docs": {
"blocks": {
"prompt": {
"variant": "compact",
"openInDesktop": 1,
"collapseStyle": "pill",
"labelCase": "none",
"background": "#f8fafc",
"backgroundDark": "#0b1220"
}
}
}
}And one page where two tab groups differ — the theme's underline, then one block's own pill:
[
{
"type": "tabs",
"content": [{ "type": "tab", "attrs": { "title": "Overview" }, "content": [] }]
},
{
"type": "tabs",
"attrs": { "style": "pill" },
"content": [
{ "type": "tab", "attrs": { "title": "Monthly" }, "content": [] },
{ "type": "tab", "attrs": { "title": "Yearly" }, "content": [] }
]
}
]In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Public Theme Component Registry
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List theme components |
| |
| Get the theme component |
| |
| Get the package |
|
Theme Component component installs
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List component installs in an organization |
| |
| Create a component install in an organization |
| |
| Get the component install |
| |
| Update the component install |
| |
| Delete the component install |
| |
| Get the package in an organization |
|
Theme Component sites
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List components in an organization |
| |
| Replace components in an organization |
|
Theme Component theme
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Apply the theme |
| |
| List components in a space |
| |
| Replace components in a space |
|
Theme Component Market
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the component marketplace in an organization |
| |
| Get the component |
| |
| Install the component |
| |
| List listings in an organization (component marketplace) |
| |
| Create a listing in an organization (component marketplace) |
| |
| Delete the listing (component marketplace) |
|
Theme Outlets
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List theme outlets |
|