Brand and appearance
This page covers a site’s appearance: the name and logo live in hugo.yml,
colours and fonts go through SCSS entry points, and page width and footer shape
are parameters. It assumes the site already builds
(Quick start).
There are four things to change: hugo.yml, the icons under static/,
assets/scss/_variables_project.scss, and
assets/scss/_styles_project.scss. Do not edit files inside the theme
directory: the theme is a Hugo Module, and an upgrade replaces the whole
directory.
Site name
The site name appears in the navbar, the browser title and the footer. A multilingual site writes one per language:
The top-level title is the fallback, and languages.<lang>.title wins.
Logo and wordmark
The theme ships assets/icons/logo.svg and uses it by default. To replace it,
put the icon file in the site’s assets/ or static/ and point the
configuration at it.
params.logois the square mark, shared by the navbar, the sidebar and the footer. Underassets/it goes through Hugo’s resource pipeline (and can be fingerprinted); understatic/it is published as is. Either way the path is relative to theassets/orstatic/root.params.wordmarkis the horizontal wordmark. Once set, the navbar uses it instead of “icon + site name”, falling back toparams.logowhen the screen is too narrow. Left unset, “icon + site name” stays.
Crop the source SVG tight to the artwork, or the sizes will not line up. An SVG
needs a viewBox, and its colours should inherit currentColor or hold enough
contrast in both light and dark.
This site leaves both unset: the navbar pairs the theme’s own
assets/icons/logo.svg with the site title, drawn in the display font.
favicon
The favicon has no parameter. The theme scans the site’s static/ directory for
conventional filenames and emits the matching <link> on every page for
whichever it finds:
| File | Link generated |
|---|---|
static/favicon.ico |
rel="icon" |
static/favicon.svg |
rel="icon" type="image/svg+xml" |
static/favicon-32x32.png |
rel="icon" with sizes, emitted in ascending size order |
static/apple-touch-icon.png |
rel="apple-touch-icon" |
static/apple-touch-icon-180x180.png |
rel="apple-touch-icon" with sizes |
A sufficient minimum is favicon.ico plus favicon.svg plus
apple-touch-icon.png. A file with a size suffix has to be square (NxN) or it
is not recognized.
Generate these with any graphics tool. The theme needs no Node.js, and Hugo
simply publishes what is already in static/.
Extra head metadata such as a Web App Manifest is outside the scan; emit it
yourself through the layouts/_partials/hooks/head-end.html hook. To change the
discovery rules themselves (a different directory, more filenames), override
layouts/_partials/favicons.html in the site’s layouts/.
Accent colour and palette
Colour comes in two layers: Bootstrap’s semantic colours (Sass variables, at compile time) and OINK’s brand layer (CSS custom properties, at run time).
Change the semantic colours first; they decide the tone of buttons, links and callouts:
This file is loaded before Bootstrap and the OINK defaults, which is where
Sass variables are overridden. To reference a variable or map Bootstrap has
already defined, use _variables_project_after_bs.scss instead.
The brand layer is a set of CSS custom properties, and light and dark must be overridden in pairs or one mode leaks the original colour:
The brand properties available are --td-brand-elev (overlay ground),
--td-brand-silk (secondary text), --td-brand-copper and
--td-brand-copper-dim (the accent and its muted form),
--td-brand-line-strong (rules), --td-brand-header-bg (navbar background),
--td-brand-shadow-sm / --td-brand-shadow-md (shadows), and
--td-brand-mark-from / --td-brand-mark-to / --td-brand-mark-gradient (the
brand gradient).
Light and dark mode
The theme does not show a light/dark control by default. To enable it:
A theme control then appears in the navbar: clicking it toggles light and dark,
and hovering or focusing it expands “follow system / light / dark”. The reader’s
choice is stored locally in the browser, and with no choice it follows
prefers-color-scheme. The switching script sets data-bs-theme before the
first paint, so there is no theme flash.
For the dark palette without the control, write
dark_mode: { show_menu: false, enable: true }; dark_mode: false (the
default) enables neither.
Custom components need readable hover, focus, disabled and selected states in both modes, with at least 4.5:1 contrast for body text and 3:1 for large text.
Fonts
There are two font presets, decided at build time with no JavaScript involved:
technical(the default): interface and body text use the bundled Inter (variable weight, with Latin / Cyrillic / Greek / Vietnamese subsets, while Chinese and emoji fall through to platform fonts), display headings use Chakra Petch, and code uses IBM Plex Mono. All font files are local, and Google Fonts is never requested.system: the interface, display, metadata, print and monospace roles all fall back to the platform stack, and the browser requests no brand font. The font files still ship with the theme; they are simply not referenced.
An invalid value fails the build (invalid params.ui.typography). The chosen
value is written to <html data-td-typography="…"> and can be confirmed in the
browser.
Custom fonts
The font roles are seven CSS custom properties. Override them rather than hunting for component selectors:
| Property | Where it is used |
|---|---|
--td-ui-font-family |
Navigation, controls and interface text |
--td-body-font-family |
Body text and blog posts |
--td-heading-font-family |
Headings in the body |
--td-code-font-family |
Code and terminals |
--td-display-font-family |
Wordmark and display headings |
--td-meta-font-family |
Technical labels and metadata |
--td-print-font-family |
Print body text |
Put the .woff2 in the site’s static/webfonts/, declare the face in the
project stylesheet, then rewrite the roles:
Roles inherit by ordinary CSS rules, so changing the font for one kind of content needs no component selectors either:
A monospace stack needs a CJK fallback, or mixed code blocks fail to align:
A site migrating from Docsy need not change how it writes this. The old Sass
variables still feed the corresponding roles, still work from
_variables_project.scss, and take precedence over the preset defaults:
| Legacy Sass variable | Font role it feeds | Note |
|---|---|---|
$td-fonts-serif |
--td-ui-font-family / --td-body-font-family |
Docsy’s interface stack, assigned to $font-family-sans-serif |
$font-family-sans-serif |
--td-ui-font-family / --td-body-font-family |
Once a project supplies its own stack, the technical preset stops putting Inter in front of it |
$font-family-base |
--td-ui-font-family / --td-body-font-family |
Bootstrap’s body variable, reaching the role through --bs-body-font-family |
$headings-font-family |
--td-heading-font-family |
Unset, headings inherit the body role |
$font-family-code |
--td-code-font-family |
Code, terminals and pre / code / kbd |
$td-font-family-monospace |
--bs-font-monospace |
Assigned to $font-family-monospace |
$font-family-monospace |
--bs-font-monospace |
Under the system preset, an explicit project value beats the platform monospace stack |
Docsy’s three Google Fonts variables — $td-enable-google-fonts,
$td-google-font-name and $td-web-font-path — are no longer read by the
theme. Leaving them in _variables_project.scss breaks nothing and does
nothing: what ships with the theme is Inter, Chakra Petch and IBM Plex Mono, and
neither preset requests anything from Google Fonts. The print role
--td-print-font-family follows the body role, and the theme ships no separate
font for paper.
YAML accepts neither a remote font URL nor arbitrary CSS: font files and styles must both be auditable local inputs.
Page width
page_width controls the overall shell width and can be overridden per page or
per section by cascade. Book pages additionally have reading_width (slim /
normal / wide), which changes the reading measure of the body rather than
the shell. An invalid value in either key fails the build.
Footer
fat(the default): a multi-column link grid plus the copyright line;slim: the copyright line only;none: no footer at all.
Page front matter (including a section cascade) can override it; this site’s
documentation section uses footer_style: slim. An unrecognized value fails the
build.
The grid’s data lives in data/footer/<language>.yaml — see
Navigation and menus. With fat
configured but no data, it degrades to slim automatically, so it can be
enabled before the content exists.
params.copyright accepts a Markdown string, or a map of authors /
from_year / to_year (present means this year). footer_center_info is
inline Markdown in the centre of the footer, and setting it explicitly to an
empty string hides that region.
SCSS entry points, and what not to do
A site’s SCSS overrides join the theme’s single style bundle, and a production
build still emits one fingerprinted stylesheet with an integrity attribute.
Three entry files go under the site’s assets/scss/:
| File | When to use it |
|---|---|
_variables_project.scss |
Sass variables set before Bootstrap and the OINK defaults ($primary, the font variables) |
_variables_project_after_bs.scss |
Variables or maps that depend on Bootstrap’s own definitions |
_styles_project.scss |
Selectors and CSS custom properties written after the theme’s component styles |
The compilation order is: Bootstrap functions → project variables → OINK defaults and Bootstrap → post-Bootstrap project variables → OINK components and the brand layer → project styles.
The CSS interface has a defined boundary. The seven font roles in
Fonts and the --td-brand-* properties are public, and the theme
keeps their names and meanings across minor versions. Component aliases such as
--td-asciinema-font-family promise only to work within that component, and
undocumented variables such as the --td-shell-* family are implementation
detail that may be renamed or removed at any time.
What not to do:
- Do not edit any file inside the theme directory (
hugo modoverwrites it); - Do not
@importthe theme’s internal partials individually — they are not a public Sass interface and their import order may change; - Do not override
baseof.htmlto change one colour. Use a design variable where one exists, and otherwise write the narrowest selector that works; - Do not reference a remote stylesheet or a font CDN.
For additional third-party CSS, publish a local resource through the
layouts/_partials/hooks/head-end.html hook rather than writing a <link> in
Markdown.
Verify
- The build prints
Total in …with no ERROR and no WARN; - The page source has
data-td-typography="technical"(or your chosen preset) on<html>; - In the browser the navbar shows your logo and site name, and the tab shows your favicon;
- Switch to dark mode and look again at body text, tables, callouts, code blocks and focus rings. A colour change is easy to verify in only one mode;
- Switch language and confirm the site name changes with it.
To check whether the font really was replaced, inspect any paragraph’s
font-family in the browser’s developer tools: it should be the face you
declared rather than Inter.
Related
- Configuration — types and defaults of the brand parameters
- Navigation and menus — navbar menu, page actions and footer data
- Layouts and page types — shell, sidebar and table of contents
- Images — images in the body, light/dark pairs and captions
- Home and landing pages — hero, sections and landing data