Theming
Jant offers three ways to customize the look, from coarse to fine:
- Built-in color themes
- Built-in font themes
- Custom CSS
Start with Settings > Color Theme and Settings > Font Theme. When those aren't enough, write overrides in Settings > Custom CSS.
All built-in themes include both light and dark palettes. Custom CSS layers on top of the selected theme — so the best approach is usually to treat a built-in theme as your starting point rather than rewriting everything from scratch.
Color variables
Color variables come in two layers:
- Core palette (
--primary,--background, etc.) controls the overall feel; changing one affects the whole site. - Site-specific colors (
--site-*) derive from the core palette by default, letting you fine-tune local areas without disturbing the global palette — for example, recoloring links without touching button colors.
Most color variables come in pairs: a background color and its matching foreground (text) color.
Core palette
| Variable | What it controls |
|---|---|
--background |
Page background |
--foreground |
Main text color |
--primary |
Functional brand color (buttons, selected UI) |
--primary-foreground |
Text on primary-colored elements |
--secondary |
Secondary buttons, badges |
--secondary-foreground |
Text on secondary elements |
--muted |
Subtle backgrounds (hover states, badges) |
--muted-foreground |
Secondary text (dates, captions, placeholders) |
--accent |
Hover/focus backgrounds (menus, nav items) |
--accent-foreground |
Text on accent backgrounds |
--card |
Card background |
--card-foreground |
Text on cards |
--popover |
Dropdown / popover background |
--popover-foreground |
Text in popovers |
--destructive |
Danger actions (delete buttons, error states) |
--success |
Success indicators |
--border |
Borders and dividers |
--input |
Input field borders |
--ring |
Focus ring color |
Site-specific colors
These derive from the core palette by default. Built-in themes set --site-accent independently; if you only override the core palette by hand, --site-accent falls back to --primary.
| Variable | Default | What it controls |
|---|---|---|
--site-accent |
var(--primary) |
Editorial accent: quote marks, Thread connector dots, the tint of link cards, hover and focus rings |
--site-page-bg |
var(--background) |
Page background, and the surfaces that sit on it |
--site-subtle-bg |
var(--accent) |
Soft fill for navigation hover, cards in the feed, and code blocks |
--site-text-primary |
var(--foreground) |
Primary text |
--site-text-secondary |
var(--muted-foreground) |
Secondary / caption text |
--site-divider |
var(--border) |
Content dividers |
--site-threadline |
var(--border) |
Thread connection lines |
--site-reading-body |
Derived from --site-text-primary |
Body text on a post's own page. Built-in themes set it, so changing --foreground alone doesn't reach it |
--site-reading-caption |
Derived from the text colors | Captions and footnotes on a post's page |
--site-content-link |
inherit |
Link text in posts |
--site-content-link-hover |
var(--site-text-primary) |
Link text on hover |
--site-content-link-underline |
Derived from --site-text-secondary |
Link underline |
--site-footnote-text |
var(--site-reading-caption) |
Footnote text in the wide-screen rail |
--site-footnote-marker |
Derived from --site-text-secondary |
Footnote numbers in the rail |
--site-search-mark-bg |
Built-in yellow | Search result highlight background |
--site-search-mark-color |
Built-in dark text | Search result highlight text |
Example: custom primary and site accent
:root {
--primary: oklch(0.48 0.08 255);
--primary-foreground: oklch(0.98 0 0);
--site-accent: oklch(0.56 0.06 240);
}
:root[data-theme-mode="dark"] {
--primary: oklch(0.79 0.06 255);
--primary-foreground: oklch(0.19 0.01 255);
--site-accent: oklch(0.74 0.07 240);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme-mode="light"]) {
--primary: oklch(0.79 0.06 255);
--primary-foreground: oklch(0.19 0.01 255);
--site-accent: oklch(0.74 0.07 240);
}
}
The two dark rules are explained under Dark mode.
Example: colored thread connectors
:root {
--site-threadline: oklch(0.8 0.05 250);
}
Typography variables
| Variable | Default | What it controls |
|---|---|---|
--font-body |
System sans-serif | Body text, inputs |
--font-heading |
Editorial serif stack | Post titles and h1–h3 |
--font-site-title |
Editorial serif stack | Site logo (header) |
--font-ui |
System sans-serif | Buttons, navigation, labels, badges (not affected by font themes) |
--font-serif |
System serif + Noto fallback | Serif accent text |
--font-blockquote |
inherit |
Blockquote font family; defaults to the body font |
--font-mono |
System monospace | Code blocks |
--type-body-size |
Content body text size | Post body text; footnote references and code scale from it |
--type-footnote-ref |
75% of content body text |
Inline footnote reference size |
--fw-regular |
400 | Body text |
--fw-medium |
500 | Labels, active nav |
--fw-semibold |
600 | Headings, buttons |
When you pick a font theme in Settings > Font Theme, --font-heading, --font-body, and a few related font weights switch with it. --font-ui is intentionally left out — buttons, navigation, and other interface text always stay system sans-serif for legibility. You can still override any variable in Custom CSS for further tuning.
To use external font sources like Google Fonts, load the font files first via code injection, then point the variables at the new fonts in Custom CSS.
Example: lighter font weights
:root {
--fw-medium: 400;
--fw-semibold: 500;
}
Layout variables
| Variable | Default | What it controls |
|---|---|---|
--layout-body-max-width |
1088px |
Widest the page frame gets: header, content, and footer |
--site-feed-rhythm |
4.4rem |
Space between posts in a list, and above the first one |
--layout-sidenote-width |
50% |
Wide-screen footnote rail width |
--layout-sidenote-gap |
10% |
Gap between prose and footnote rail |
Example: wider content area
On a wide screen the reading column is a fixed share of the frame, so a wider frame gives it more room. Below 1024px the column keeps a 35rem measure whatever the frame.
:root {
--layout-body-max-width: 1280px;
}
Sidenotes and indented blocks
Footnotes use semantic source HTML: superscript noteref links in the body and
native ordered endnotes after the authored content. On detail pages and public
timeline lists, Jant uses each post's actual container—not only the viewport—to
decide whether there is room for a Tufte-style right rail. A small progressive
client enhancer positions the existing endnote list when a container is at
least 56rem wide and the notes fit without excessive overflow. Every timeline
item is measured independently. Narrow or dense containers, print,
no-JavaScript clients, and older browsers keep bottom endnotes.
Because the enhancer reads each noteref's real position instead of anchoring a float to its containing block, padding on blockquotes or other prose blocks no longer needs a compensating anchor offset. Rail numbers share the note text's size and baseline. Customize its proportions and semantic colors through theme tokens:
:root {
--layout-sidenote-width: 42%;
--layout-sidenote-gap: 8%;
--type-footnote-ref: calc(var(--type-body-size) * 0.75);
--site-footnote-text: var(--site-reading-caption);
--site-footnote-marker: var(--site-text-secondary);
}
The rail visually suppresses backlink arrows because each note is already next to its first reference. The backlink remains in the semantic HTML for bottom endnotes and assistive technology, and becomes visible if a keyboard user focuses it.
Theme selectors should prefer .footnote-endnotes, .footnote-list,
.footnote, .footnote-ref, .footnote-backlinks, and .footnote-backref,
the arrow link back to a reference. Fragment IDs are opaque and may change in
any release; never style or parse their compact hash.
Media and avatars
| Variable | Default | What it controls |
|---|---|---|
--media-radius |
0.5rem |
Image / video corner radius |
--avatar-size |
28px |
Header avatar size |
--avatar-radius |
50% |
Avatar corner radius |
Data attributes
You can target specific pages or elements with these data attributes when writing selectors:
| Attribute | Where it appears | Values |
|---|---|---|
data-theme |
<html> |
Active color theme id (tufte, linen, frost, …) |
data-theme-mode |
<html> |
auto, light, dark |
data-page |
<body> |
home, post, search, archive, collection, collections, featured, subscribe |
data-post |
<article> |
Present on every post |
data-format |
<article> |
note, link, quote |
data-post-slug |
<article> |
Post slug (handy for debugging and per-post styling) |
data-post-pinned |
<article> |
Present on pinned posts |
data-post-featured |
<article> |
Present on Featured posts |
data-feed |
Feed container | Wraps the post list |
data-authenticated |
<body> |
Present when logged in |
data-post-end |
After a post's page | Empty slot after the post, where code injection mounts comments |
Inside each post there are four more markers: data-post-body (the body, or a quote's commentary), data-post-quote (a quote's quoted text), data-post-meta (the date and the collections), and data-post-media (the attachments: images, video, audio, documents, and text files). Together they let you style any one section of a post on its own.
Posts in the feed, on a post's page, and in a collection carry every attribute and marker above. A search result carries the <article> attributes but none of the markers inside: it shows a snippet instead of the post. A tile in the archive's grid view carries data-post and data-format only.
Example: dividers only on the home page
[data-page="home"] [data-post] {
border-bottom: 1px solid var(--border);
padding-bottom: 1.5rem;
}
Example: style by format
[data-post-quote] {
font-family: var(--font-serif);
font-style: italic;
}
[data-format="link"] {
border-left: 3px solid var(--site-accent);
padding-left: 1rem;
}
Example: highlight pinned posts
[data-post-pinned] {
background: var(--muted);
border-radius: 8px;
padding: 1rem;
}
Dark mode
A site follows the visitor's system preference (light / dark) unless it is set to always light or always dark; data-theme-mode on <html> says which. A page is dark in two ways, so a dark value takes two rules:
:root {
--site-threadline: oklch(0.8 0.05 250);
}
/* The site is set to dark */
:root[data-theme-mode="dark"] {
--site-threadline: oklch(0.45 0.05 250);
}
/* The system is dark, and the site isn't set to light */
@media (prefers-color-scheme: dark) {
:root:not([data-theme-mode="light"]) {
--site-threadline: oklch(0.45 0.05 250);
}
}
Write the selectors as shown. The built-in theme uses the same ones, and custom CSS comes after it, so yours win. A plain :root inside the media query is less specific and loses to the theme's dark values.
Tips
- Override variables first, write selectors second. Jant keeps the variables, data attributes, and classes on this page stable where it can, and a release that changes one says so in its release notes; see Compatibility. Jant's stylesheets define many more custom properties and class names; those are internal and can change in any release without notice.
- Custom CSS comes after the built-in theme and uses the same selectors, so it overrides every variable the theme sets:
:rootfor light values, the two rules under Dark mode for dark ones. oklch()is convenient for color tuning. A common pattern: keep--primarysaturated and stable for buttons; let--site-accentcarry a softer tone for quote marks and Thread connectors. Links take--site-content-link.- Test in both light and dark. A color set only in
:rootalso applies in dark mode wherever neither the theme nor Jant sets a dark value for it.
What's next
- Code injection — embed third-party scripts, external fonts, analytics, comments
- Export and import — site migration and archiving