Theming hooks: the selectors every type's pages share
A setting is always the first thing to reach for: it keeps working when our markup changes. When no setting does what you want, you write a rule yourself, in customCss or as an elementStyles entry. This page is the list of selectors you can write those rules against on any space type with reader pages, and what each one is wired to.
These names are part of the fx-1 hook contract described in Theming: the rest of it: a name here is never renamed or removed. We add names; if one ever has to break, it breaks as a new version published beside this one. Everything not listed — our utility classes, how elements are nested, the order of children — is an internal detail and does change. A rule written against .rounded-xl.border will break; one written against [data-fx-panel] will not.
Every hook sits inside .forumulate-reader, the reader's root element. Scope your rules with it.
The type stamp: what kind of page this is
Two places say which type and which page you are on.
The reader root, on every page of every type, carries the space's type:
.forumulate-reader[data-reader-type='status'] [data-fx-header] {
border-bottom: 0;
}That is enough to style the header, footer or anything else differently on one type without :has().
The page's own root — the first element the page draws inside the main area — carries the type again, plus what kind of page it is:
Attribute | Value | What it means |
|---|---|---|
|
| The space type, lower case |
|
| The kind of page |
| A short name: | Which sub-page or hub, when there is one |
| The item's id | The one item on an item page |
| The section's name | The collection or section the page belongs to |
| The section's id | The same section, by id |
The surfaces mean the same thing on every type:
Surface | The page |
|---|---|
| The type's front door or list root (the status board, the careers list, the legal register, the contact page) |
| One item: an incident, a legal document, a contact topic's page |
| A page of the type that is not its index: a status history or uptime page, a careers application page, the contact thank-you page |
| A list filtered by one facet: a tag, an author, an archive month |
| A |
| The reader's own pages |
| A results page |
| A view made to be framed (the status card, the careers and contact embeds) |
Today the page-root stamp is on the status, legal, careers and contact pages:
Type | Pages stamped |
|---|---|
Status | the board ( |
Legal | the register ( |
Careers | the list ( |
Contact | the contact page ( |
On the other types, scope by the reader root's data-reader-type and use the hooks below and the type's own hooks listed in Theming: the rest of it.
/* Every status sub-page's section headings, spaced out */
[data-reader-type='status'][data-reader-surface='sub'] [data-fx-section-title] {
letter-spacing: 0.06em;
}
/* Only the uptime page */
[data-reader-surface='sub'][data-reader-page='uptime'] [data-fx-panel] {
border-width: 2px;
}A framed copy of any page (?embed=1, see Embeds) has data-reader-embed on its root, so [data-reader-embed] … styles the embedded copy alone.
Settings that read the stamp
Four cross-type settings use the stamp, which is why they now work on more than the docs and blog readers:
Key | What it reads |
|---|---|
| The page title on an |
|
|
| Matches an id against |
| Draws behind an |
The shared parts
These hooks mean the same thing on every type, and the same theme key styles each of them wherever it appears.
Text
Hook | The element | Styled by |
|---|---|---|
| Every page's main heading |
|
| The title of an index, hub or collection page |
|
| The title of one entry in a list (a card, a row, a timeline entry) |
|
| A community topic's title |
|
| The standfirst or description under a title |
|
| A date, author, venue or facts row |
|
| A heading outside the article ("Past incidents", "Related topics") |
|
data-type-level says which level of the type scale an element plays: h1–h6, body, small or code. A section title carries it beside its hook (data-type-level="h2"), and so can any element that plays a level's part. The scale, the level's size, weight, colour, font and case reach it, and so do the heading-wide keys (headingTransform, headingWrap, variation.heading, features.heading), which now apply to every [data-type-level^="h"] and to the index, entry, topic and section titles, not only to headings inside an article. rule and spaceBefore on body, small and code reach a paragraph of the article, a caption (or [data-type-level="small"]) and a code block.
{
"typography": {
"levels": {
"entryTitle": { "weight": 600, "tracking": -0.01 },
"lede": { "color": "#52525b" }
}
},
"components": { "meta": { "font": "mono", "case": "upper", "tracking": 0.04 } }
}Structure
Hook | The element |
|---|---|
| A titled region of a page (a band of incidents, "Other ways to get help") |
| A list page's list |
| One row or card in it |
| A boxed card or panel |
| A cover or hero picture, or its rounded frame |
| An empty state ("No incidents") |
| Previous/next or page-number navigation |
| A filter or sort row above a list |
| A row of view tabs |
| An item's row of buttons (subscribe, apply, share) |
| Your ad (see below) |
Three theme keys reach these parts without any CSS from you:
components.radiusandcomponents.shadowreach every[data-fx-panel], beside our own cards. They are only emitted when you set them, so an unset theme keeps each panel's own rounding.components.image.radiusreaches every[data-fx-cover]and the picture inside it.components.expandablestyles everydetails[data-expandable]collapsible anywhere in the main area — an FAQ answer, a docs block, a front-page block — not only inside an article.
A floating menu or panel uses the class .popover-surface, painted by components.popover (the subscribe menu included). Buttons, text fields and cards are hooked by class, not attribute: .btn (.btn-primary, .btn-ghost), .input and .card / .surface.
Where the structure hooks are today
[data-fx-page-title] is on every type's pages. The title, lede and meta hooks are on the blog, changelog, status and docs pages and the front-page feed cards; [data-fx-index-title] on the blog, changelog, community, roadmap, status, events, FAQ and careers indexes. The structure hooks (section, entry-list, entry, panel, empty, pager, filters, tabs, actions) are on the status pages, and section titles on the status pages and front pages. More types join as their pages take the hooks; a rule written now will reach them then.
Each type's own parts: data-fx-<type>-<part>
Besides the shared parts, a type names the pieces only it has. The status page has the most:
Type | Hooks |
|---|---|
Status |
|
Legal |
|
FAQ |
|
Contact |
|
Courses |
|
Each is spelled as an attribute: status-banner is [data-fx-status-banner]. The docs, community and blog readers' parts (the docs drawer, the forum's new topics bar, a post's masthead, byline and author card, …) are in the main hook list in Theming: the rest of it.
.forumulate-reader [data-fx-faq-question] {
font-weight: 600;
}
.forumulate-reader [data-fx-contact-form] .input {
border-radius: 0;
}Click-to-key in the studio
The hook names are also how the studio's inspector finds the settings for what you double-clicked. Beside whatever the click opens (the page, the post, the block), the inspector offers This part's look: the theme keys that draw that part.
A type's own hook names its group.
[data-fx-<type>-<part>]openstheme.<type>.<part>when the type's theme has a group of that name:[data-fx-status-banner]openstheme.status.banner,[data-fx-faq-item]openstheme.faq.item,[data-fx-contact-form]openstheme.contact.form. A blog's hooks openpostgroups, and a custom space's opendocsgroups, because those are the namespaces those readers read. A hook whose name is no group simply opens nothing extra.A shared hook opens its token group, the same on every type: a page title opens
typography.levels.h1, an index titletypography.levels.indexTitle(andlayout.indexHeader), an entry titletypography.levels.entryTitle, a topic titletypography.levels.topicTitle, a ledetypography.levels.lede, a meta rowcomponents.meta, a section titletypography.levels.h2andh3, a covercomponents.image, a collapsiblecomponents.expandable, a popovercomponents.popover, and a panelcomponents.radiusandcomponents.shadow.An element with
data-type-levelopens that level,typography.levels.<level>.
The inspector looks a few elements up from where you clicked, so clicking the text inside a card's title still opens the title's level. A page root that carries the type stamp can be selected too; the visual editor names it by its stamp ([data-reader-type="status"][data-reader-surface="sub"][data-reader-page="history"]), so an element rule written there applies to that kind of page alone.
Palette slots and what they paint
Four palette slots that came from imported colour schemes now each paint one thing, on every type, in palette and (for dark mode) paletteDark. Each is only emitted when you set it.
Slot | What it paints |
|---|---|
| The ink of links in an article, when |
| The header's navigation items and the section row under the header; an item's open menu keeps the page's ink |
| The background of every highlight the reader draws: search-hit marks, a transcript's find-in-page, marked code lines |
| The liked heart and like count |
palette.paper (the page, surface and rule set in one choice) also works on every type now; see Themes.
{
"palette": { "tertiary": "#0f766e", "quaternary": "#334155", "highlight": "#fde68a", "love": "#e11d48" },
"paletteDark": { "highlight": "#854d0e" }
}The ad slot
When the theme's ads is set and enabled (see Theming: the rest of it), every reader page of every type shows the ad once, after the page's own content inside the main area, as [data-fx-ad-slot]. Ads count as marketing: nothing is drawn, not even an empty box, until the reader has granted marketing consent. Pages that must never show one — a checkout, an embedded frame — opt out on their own. On your own domain the ad's script runs in the page; on a Forumulate address it runs in an isolated frame.
.forumulate-reader [data-fx-ad-slot] {
margin-block: 3rem 1rem;
}Loading placeholders
While a part of a page is still loading, the reader draws a placeholder the size of what is coming. Every placeholder reads components.skeleton (style, radius, colours, first paint; see Themes), so one key restyles them all. A placeholder's box carries data-skeleton and aria-busy="true"; style it with the keys rather than CSS where you can, so light and dark mode and reduced motion keep working.
Writing rules against the hooks
In customCss, write ordinary CSS scoped by .forumulate-reader:
.forumulate-reader[data-reader-type='legal'] [data-fx-legal-facts] {
font-variant-numeric: tabular-nums;
}
[data-reader-type='careers'][data-reader-surface='index'] [data-fx-index-title] {
font-size: clamp(2rem, 5vw, 3.5rem);
}In elementStyles, each entry is a selector and its declarations, with an optional media (tablet, phone), state (hover, focus, active), scheme (light, dark) or page (one item's id). The rules are anchored inside the reader for you:
{
"elementStyles": [
{
"selector": "[data-reader-type=\"status\"] [data-fx-panel]",
"declarations": { "border-radius": "4px", "box-shadow": "none" }
},
{
"selector": "[data-fx-entry-title]",
"declarations": { "font-size": "18px" },
"media": "phone"
},
{
"selector": "[data-fx-status-banner]",
"declarations": { "background-color": "#052e16" },
"scheme": "dark"
}
]
}curl -sS -X PATCH \
-H "Authorization: Bearer $FORUMULATE_TOKEN" -H 'content-type: application/json' \
https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/theme \
-d '{ "elementStyles": [ { "selector": "[data-fx-faq-question]", "declarations": { "font-weight": "600" } } ] }'elementStyles accepts a narrowed set of properties and refuses anything that could move our chrome or cover the consent banner; the full rules are in Theming: the rest of it. Before writing either, check whether a key already does it: a radius on every panel is components.radius, not a rule; an entry title's size is typography.levels.entryTitle.size. A key keeps working when our markup changes, and it shows up in the studio for the next person.