API FLEETDocs

Portal Customization & Theming

How to change how a portal looks — from config only. Three layers, in order of preference:

  1. Theme tokens — colors, fonts, shape, layout. Covered in PORTAL_JSON_COMPONENTS.md §2. Prefer these; they adapt light/dark automatically and never break.
  2. custom_css — a free-form CSS escape hatch in the theme. Powerful, but global and CSS-only (see limits below).
  3. New block types — anything the two above can't express needs a renderer change (code), not config.

This page documents layers 2 and 3: what custom_css can target, the stable CSS hooks, and the hard limitations.


Backgrounds & art direction

For rich hero/page backgrounds — gradients, mesh blobs, grids, grain, images, pinned decorations, a readability scrim, and a slow CSS drift — use the declarative theme.background spec rather than custom_css. It is a layer stack (theme layer 1, config only), reskins with the theme via token refs, and renders identically in edit, preview, and publish. Build it from the composer's Theme → Background panel or author the JSON directly.

Schema + examples: PORTAL_JSON_COMPONENTS.md §2.7. Design rationale (and the coding-agent authoring guide): ART_DIRECTION_SPEC.md.

Reach for custom_css (below) only for looks the background spec can't express.


custom_css

Set theme.custom_css to a string of CSS. It is injected into every rendered portal page (builder preview, published build, custom domain).

{
  "apifleet_portal": 1,
  "name": "Acme",
  "theme": {
    "mode": "dark",
    "custom_css": ".card { border-radius: 12px; } .card-grid { gap: 14px; }"
  },
  "pages": [ ... ]
}

What it can do: restyle any rendered element via the class hooks below, override theme CSS variables, add gradients/shadows/spacing, tweak hover states.

Safety: the renderer neutralizes a </style> breakout and does not accept custom JavaScript. CSS can still make network requests through url(...), hide or reposition interface elements, and affect the whole portal. Treat custom_css as trusted-editor content and use only trusted asset URLs.

Theme CSS variables you can override

custom_css can reassign any token the renderer reads (full list in the components doc). Common ones:

:root {
  --brand: #2b5cff;          /* primary */
  --surface: #15181c;        /* cards / panels */
  --text: #e8ecf1;
  --radius: 14px;            /* corner radius */
  --card-icon-img: ...;      /* etc. */
}

Prefer setting these via theme tokens (primary, surface, radius, …) rather than custom_css — tokens are validated and theme-aware.


CSS hook reference

Stable class names the renderer emits, by block. Target these in custom_css. (Source of truth: app/render.py + app/static/portal.css.)

Page chrome

Hook Element
.prose wrapper around rendered Markdown (h1–h6, p, ul, table, blockquote…)
.doc-topbar, .doc-brand, .doc-cta header bar, logo, CTA button
.doc-tabs, .doc-tab secondary nav tab strip
.doc-sidebar, .doc-nav, .nav-link, .nav-icon left nav + per-item icon
.doc-content main content column
.doc-footer, .doc-page-meta footer, "was this helpful" row

card_grid

Hook Element
.card-grid the grid container (--cols custom prop = column count)
.card one card
.card-icon emoji / text icon span
.card-icon.material-symbols-outlined Material-font icon span
.card-icon-img icon_url image (24×24)
.card-media image hero wrapper
.card-title, .card-link card heading + stretched link
.card-desc card description (rendered Markdown)

callout

Hook Element
.callout container
.callout-info / .callout-warning / .callout-success variant modifier
.callout-icon, .callout-body icon, body

code

Hook Element
.doc-code-block container
.cb-bar, .cb-title, .cb-lang, .cb-copy, .cb-panel title bar, language tag, copy button, code panel

tabs

Hook Element
.content-tabs, .ct-bar, .ct-panels tab set, tab bar, panels

Spec blocks (endpoint, schema, endpoint_list)

Hook Element
.endpoint, .ep-head, .ep-title, .ep-path, .ep-main, .ep-aside endpoint block parts
.ep-list, .ep-item, .ep-item-sum endpoint_list rows
.schema-table, .prop, .prop-name, .ptype, .pdesc schema table
.code-sample, .cs-bar per-language request samples

divider

.block-divider.

Icons

.material-symbols-outlined — Material Symbols font span. Content is the icon name (e.g. rocket_launch). ~4,300 names in app/static/material-symbols.json.


Hard limitations (read before designing)

These are the boundaries a config author will hit. None are bugs — they're the shape of the system today.

  1. custom_css is global. It applies to the whole portal. There is no per-block class or id, so you cannot style one card_grid differently from another, or one callout differently from the next. Everything of a type shares the look. (Workaround: style by position with :nth-of-type, which is brittle.)

  2. Blocks are siblings, not nestable. Each block renders as its own top-level element in .doc-content. You cannot wrap several blocks in one container — e.g. put a heading inside the same panel as a card_grid. A "step number + cards in one window" layout places the heading above the panel, not within it.

  3. No custom JavaScript. Published portals load only the fixed portal.js (nav, tabs, copy buttons, search overlay, try-it). custom_css is CSS-only. So bespoke interactions, canvas effects, or third-party widgets are not possible from config. (Rich backgrounds — gradients, mesh, grain, images, decorations, and a slow CSS drift via theme.background "animated": true — are the supported way to get motion without JS.)

  4. One theme per portal. mode (light/dark/system) and all tokens are portal-wide. You can't set a different theme per page.

  5. Published builds are immutable. A theme/CSS change takes effect on the next publish; already-published builds keep their snapshot until republished.

When a design needs something on this list, it's a new block type (renderer change), not config.


Recipe: stepped cards (like a "01 · Build" section)

Achievable config-only — structure + static panel, no animation:

{ "type": "markdown", "config": { "source": "### 01 · Build" } },
{ "type": "card_grid", "config": { "columns": 1, "cards": [
  { "title": "Payment products", "description": "Compare your options.", "href": "/overview", "icon": "credit_card" },
  { "title": "Webhooks", "description": "React to every event.", "href": "/webhooks", "icon": "bolt" }
] } }
/* theme.custom_css */
.card-grid { gap: 14px; padding: 22px; border-radius: 16px;
  background: linear-gradient(135deg, #2a3350, #1b2740 55%, #223a4d); }
.card { background: #15181c; border: 1px solid #23262b; border-radius: 12px; }
.card-icon.material-symbols-outlined { width: 44px; height: 44px;
  border-radius: 999px; background: #1e2330; color: #8fb0ff;
  display: inline-flex; align-items: center; justify-content: center; }

Caveat (limitation #1): this styles every card grid in the portal. For a per-section treatment or an animated background, a dedicated steps block is the right path.

Start typing to search every guide.