Theming hooks: the selectors every type's pages share

Embed
Preview of the card
<a href="https://forumulate.com/guides/theming-hooks"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/2237232b-27e3-4548-b613-117104cfeda7/social-card.svg?v=3e7ae33eec8b" alt="Theming hooks: the selectors every type's pages share" width="600" height="315"></a>

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

data-reader-type

docs, forum, blog, roadmap, changelog, status, legal, faq, careers, contact, events, courses, custom

The space type, lower case

data-reader-surface

index, item, sub, hub, category, account, search, embed

The kind of page

data-reader-page

A short name: history, uptime, apply, thanks, a legal document's kind…

Which sub-page or hub, when there is one

data-reader-item

The item's id

The one item on an item page

data-reader-section

The section's name

The collection or section the page belongs to

data-reader-section-id

The section's id

The same section, by id

The surfaces mean the same thing on every type:

Surface

The page

index

The type's front door or list root (the status board, the careers list, the legal register, the contact page)

item

One item: an incident, a legal document, a contact topic's page

sub

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

hub

A list filtered by one facet: a tag, an author, an archive month

category

A /c/<slug> collection

account

The reader's own pages

search

A results page

embed

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 (index); history, uptime, maintenance, component, subscription, alerts (sub); an incident or maintenance window (item, page incident, with its id); the status card (embed)

Legal

the register (index, page trust-centre when it shows the trust centre alone); a document (item, page = its kind); a collection (category)

Careers

the list (index, section = the team when filtered); apply and application-status (sub); a collection (category); the jobs embed (embed)

Contact

the contact page (index), or a topic's own page (item, the topic as the item); thanks (sub); the embedded form (embed)

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

chrome.header.scrollTitle

The page title on an item page ([data-reader-surface="item"] [data-fx-page-title]) when the page has no post title

chrome.header.sectionCrumb

data-reader-section-id / data-reader-section

announcement.pages

Matches an id against data-reader-item as well as a docs page id

layout.heroDecor

Draws behind an index, hub or category page root as well as an index frame

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

[data-fx-page-title]

Every page's main heading

typography.levels.h1

[data-fx-index-title]

The title of an index, hub or collection page

typography.levels.indexTitle

[data-fx-entry-title]

The title of one entry in a list (a card, a row, a timeline entry)

typography.levels.entryTitle

[data-fx-topic-title]

A community topic's title

typography.levels.topicTitle

[data-fx-lede]

The standfirst or description under a title

typography.levels.lede

[data-fx-meta]

A date, author, venue or facts row

components.meta

[data-fx-section-title]

A heading outside the article ("Past incidents", "Related topics")

typography.levels.h2 / h3, by its data-type-level

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

[data-fx-section]

A titled region of a page (a band of incidents, "Other ways to get help")

[data-fx-entry-list]

A list page's list

[data-fx-entry]

One row or card in it

[data-fx-panel]

A boxed card or panel

[data-fx-cover]

A cover or hero picture, or its rounded frame

[data-fx-empty]

An empty state ("No incidents")

[data-fx-pager]

Previous/next or page-number navigation

[data-fx-filters]

A filter or sort row above a list

[data-fx-tabs]

A row of view tabs

[data-fx-actions]

An item's row of buttons (subscribe, apply, share)

[data-fx-ad-slot]

Your ad (see below)

Three theme keys reach these parts without any CSS from you:

  • components.radius and components.shadow reach 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.radius reaches every [data-fx-cover] and the picture inside it.

  • components.expandable styles every details[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

status-banner, status-group, status-group-heading, status-row, status-row-name, status-state, status-strip, status-bar, status-day, status-day-panel, status-legend, status-incident, status-maintenance, status-subscribe, status-kpis, status-actions, status-hero, status-title-row, status-tagline, status-nav, status-siblings, status-headline, status-caption, status-active, status-history, status-outages, status-outage, status-metrics, status-chart, status-partners, status-intro, status-back, status-archive, status-archive-month, status-archive-item, status-calendar, status-uptime-month, status-maintenance-list, status-component-header, status-component-latency, status-component-incidents, status-incident-header, status-incident-components, status-incident-update, status-incident-pager, status-related, status-sla

Legal

legal-row, legal-facts, legal-kicker

FAQ

faq-item, faq-question, faq-answer, faq-section, faq-search, faq-category-card

Contact

contact-form, contact-field, contact-facts, contact-topic, contact-success, contact-offices, contact-office, contact-channels, contact-channel, contact-open-now, contact-deflect

Courses

course-hero, course-outline, course-lesson-row, course-sidebar, course-progress, course-end-card, course-certificate

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>] opens theme.<type>.<part> when the type's theme has a group of that name: [data-fx-status-banner] opens theme.status.banner, [data-fx-faq-item] opens theme.faq.item, [data-fx-contact-form] opens theme.contact.form. A blog's hooks open post groups, and a custom space's open docs groups, 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 title typography.levels.indexTitle (and layout.indexHeader), an entry title typography.levels.entryTitle, a topic title typography.levels.topicTitle, a lede typography.levels.lede, a meta row components.meta, a section title typography.levels.h2 and h3, a cover components.image, a collapsible components.expandable, a popover components.popover, and a panel components.radius and components.shadow.

  • An element with data-type-level opens 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

tertiary

The ink of links in an article, when components.link.color is unset (that key, being more specific, still wins)

quaternary

The header's navigation items and the section row under the header; an item's open menu keeps the page's ink

highlight

The background of every highlight the reader draws: search-hit marks, a transcript's find-in-page, marked code lines

love

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.

The Forumulate teamUpdated