← Back to Blog
Development by Featured

A Practical Guide to Fonts in Odoo

A Practical Guide to Fonts in Odoo

Odoo ships three separate font pipelines: one for the backend, one for PDF reports, one for the website. This guide gives you the mental map and one working code path per layer.

You changed the SCSS variable, restarted Odoo, and the invoice PDF still prints in the old font. That is not a bug. The backend and the report do not share a font pipeline, and neither of them shares one with the website.

Font advice for Odoo lives in theme docs, QWeb tutorials, and forum threads, and it is rarely labelled by layer. This article consolidates it. After reading, you will know which file to touch for which surface, with one working code path per layer and a short list of pre-built modules to skip coding when possible.

Code examples target Odoo 17. The report pipeline still runs on wkhtmltopdf.

Font formats and families in 60 seconds

If you already know font formats, skip to the next section.

File formats:

  • WOFF2 — the default choice for the web and for Odoo assets. Best compression, supported by all modern browsers.
  • WOFF — older compressed format, kept as a fallback for very old browsers.
  • TTF / OTF — uncompressed formats. Still useful because some PDF renderers (wkhtmltopdf in particular) accept them more reliably than WOFF2.

In Odoo you will usually declare all three inside a single @font-face and let the browser or renderer pick.

Families: pick by function, not aesthetic. Odoo already exposes one SCSS variable per role so you rarely need to invent a new one.

  • Sans-serif — backend UI and body text. Examples: Roboto, Open Sans, Inter, Segoe UI.
  • Serif — long-form print reports where readers scan across many lines. Examples: Merriweather, Source Serif, Noto Serif.
  • Monospace — code blocks and technical data. Examples: Fira Code, JetBrains Mono, Source Code Pro.

Where fonts live in Odoo

The mental map, one bullet per layer.

Backend web client

  • SCSS variables in web/static/src/scss/primary_variables.scss
  • Asset bundle: web.assets_backend
  • Override by redefining the variable in your module’s SCSS

PDF reports

  • @font-face declarations in web/static/fonts/fonts.scss
  • Asset bundle: web.report_assets_common
  • Override by adding your own @font-face in a module and pushing it into the report bundle

Website

  • SCSS map $o-theme-font-configs in website/static/src/scss/secondary_variables.scss
  • Asset bundle: web.assets_frontend (starting from Odoo 15.0+)
  • Override by extending the map, usually inside a theme’s primary_variables.scss

Pick your layer first, then the code writes itself.

Fonts in the backend

Three SCSS variables control the backend web client:

  • $o-system-fonts: the base cascade (Apple system, Segoe UI, Roboto, Helvetica Neue, etc.)
  • $o-font-family-sans-serif: body text
  • $o-headings-font-family: headings, defaults to SF Pro Display

One nuance before you touch these. Odoo wraps every backend font family in o-add-unicode-support-font(), which layers Noto glyphs underneath so Vietnamese, Cyrillic, and other non-Latin scripts still render when the primary font lacks them. Redefine the variable, do not remove the wrapper.

A minimal custom-font module:

your_module/
├── __manifest__.py
└── static/
    ├── fonts/
    │   ├── CorporateSans.woff2
    │   ├── CorporateSans.woff
    │   └── CorporateSans.ttf
    └── src/scss/
        ├── fonts.scss
        └── variables.scss

fonts.scss:

@font-face {
  font-family: 'Corporate Sans';
  src: url('/your_module/static/fonts/CorporateSans.woff2') format('woff2'),
       url('/your_module/static/fonts/CorporateSans.woff')  format('woff'),
       url('/your_module/static/fonts/CorporateSans.ttf')   format('truetype');
  font-weight: 400;
  font-style: normal;
}

variables.scss:

$o-font-family-sans-serif: o-add-unicode-support-font(('Corporate Sans', sans-serif));

__manifest__.py:

'assets': {
    'web.assets_backend': [
        'your_module/static/src/scss/fonts.scss',
        'your_module/static/src/scss/variables.scss',
    ],
},

Order matters: the variable override must load after Odoo’s core primary_variables so the redefinition wins.

Fonts in PDF reports

The report bundle is separate because wkhtmltopdf renders in its own asset context. web.assets_backend is not loaded there. Instead, every QWeb report calls <t t-call-assets="web.report_assets_common"/>, and that bundle is what you extend.

The same pattern as the backend, redirected at the report bundle. Ship your @font-face in a module SCSS file, load it into web.report_assets_common via the manifest, then reference the family in your report CSS or QWeb template.

'assets': {
    'web.report_assets_common': [
        'your_module/static/src/scss/report_fonts.scss',
    ],
},
@font-face {
  font-family: 'Corporate Sans';
  src: url('/your_module/static/fonts/CorporateSans.ttf') format('truetype');
  font-weight: 400;
}

.o_report_layout, .header, .footer {
  font-family: 'Corporate Sans', sans-serif;
}

Prefer TTF for the report bundle. Some wkhtmltopdf builds handle WOFF2 inconsistently, and there is no benefit to compression in a server-side render.

Vietnamese and CJK glyphs: wkhtmltopdf substitutes silently when a glyph is missing, so a font that looks correct in the browser can drop diacritics in the PDF. Two ways out: ship a font whose glyph coverage matches your locales, or lean on Odoo’s built-in Odoo Unicode Support Noto family (already loaded by web/static/fonts/fonts.scss) as an explicit fallback in your CSS.

Fonts on the website

The website pipeline is the most interesting of the three, because Odoo does more work on your behalf. When a site editor picks a Google Font in the theme customizer, Odoo downloads the font server-side, stores the WOFF2 files as public ir.attachment records, and rewrites the SCSS imports to point at the local URLs. The result is a GDPR-friendly locally hosted font without any manual download step. This behaviour was introduced in commit b06ce21.

The map at the centre of it all is $o-theme-font-configs. Each entry looks like:

$o-theme-font-configs: (
    'Corporate Sans': (
        'family': ('Corporate Sans', sans-serif),
        'url':    'Corporate+Sans:300,400,700',
        'name':   'Corporate Sans',
    ),
    ...
);

The url value points at a Google Fonts family string. If the site editor saves with this font selected, Odoo swaps url for attachment and stores the font locally. To extend the picker with a new font, ship a theme (or extend an existing one) and merge new entries into the map inside primary_variables.scss. The pattern in theme_avantgarde is the reference implementation.

Unicode: as with the backend, Odoo automatically applies o-add-unicode-support-font() to every entry in the map. You do not need to add it yourself, and you should not strip it, or Vietnamese-language sites will start showing boxes.

If you need a full custom-font-upload UI for non-developers rather than a Google Fonts picker, the WYSIWYG builder exposes some of this, but the flow lives in the JavaScript layer and is outside this article’s scope. A follow-up post will cover the community modules that fill this gap.

The takeaway

  • Backend: redefine an SCSS variable, load into web.assets_backend.
  • Report: add @font-face, load into web.report_assets_common, reference in report CSS.
  • Website: extend $o-theme-font-configs in a theme, let Odoo do the download.

Pick the layer that is blocking you today, copy the snippet, ship it. The mental model sticks the first time you use it in anger. Fonts in Odoo are not hard, they are three problems dressed as one.

Ready to get the most out of Odoo?

Whether you are starting a new implementation, upgrading from an older version, or optimizing your current setup — our Odoo-first team is here to help.