docs
TYPO3 v14
Extending  /  Theming: tokens, SCSS, your own theme

15 — Theming: tokens, SCSS, your own theme

For developers. How the styling is organised, and how to build a theme on top of yavi_core.


No build step

SCSS is compiled server-side by scssphp, which comes with Bootstrap Package. There is no npm, no webpack, no dist/. Edit the SCSS, flush the cache, reload:

bash
ddev exec vendor/bin/typo3 cache:flush

The compiled file lands in typo3temp/assets/bootstrappackage/css/theme-<hash>.css. The hash is derived from the source, so a stale file is never served — but only after the cache has been flushed.

Structure and Skin

Core's SCSS is split along one line:

FolderContainsA theme…
Scss/Structure/Geometry, layout, behaviour — what makes a card a cardrarely touches it
Scss/Skin/Colour, shadow, border, decoration — what makes it look like this themereplaces it per component family

A theme replaces a whole skin file for a component family instead of overriding individual declarations. There are no cascade layers; the theme's file simply comes later.

text
packages/yavi-lucerne/Resources/Public/Scss/
├── theme.scss          entry point
├── _variables.scss     the theme's own tokens
├── _structure.scss     generated: the list of core structure partials
├── _skin.scss          generated: the list of skin partials, core's or the theme's
├── _custom.scss        free space for this theme
└── Skin/               the skin files this theme owns
    ├── audioplayer.scss
    ├── buttons.scss
    ├── cards.scss
    ├── carousel.scss
    ├── cta.scss
    └── menu.scss

theme.scss is short on purpose:

scss
@import "EXT:yavi_core/Resources/Public/Scss/structure";
@import "variables";
@import "structure";
@import "skin";
@import "custom";

Keeping the lists in sync

_structure.scss and _skin.scss are generated. When core gains a partial, regenerate every theme's lists:

bash
ddev exec php packages/yavi-core/Build/sync-theme-scss.php
ddev exec php packages/yavi-core/Build/sync-theme-scss.php --check   # writes nothing, fails on drift

The --check variant runs in the test suite, so a forgotten partial fails the build instead of silently disappearing from one theme.

The token system

Every configurable value is a CSS custom property with exactly one owner.

scss
// 1. yavi_core/_variables-defaults.scss — the theme-independent default
--card-radius: calc(12px * var(--theme-radius-scale, 1));

// 2. the theme's _variables.scss — this theme's value
--card-radius: 25px;

// 3. site settings — emitted into the page head by ThemeCssVariablesRenderer
--card-radius: calc(18px * var(--theme-radius-scale, 1));

Rules that follow from this:

Fallback chains

A token that may be overridden from several layers reads them in order of authority:

scss
--megamenu-link-border-radius:
  var(--megamenu-link-radius,            /* inline override — absolute px */
  var(--nav-menu-radius,                 /* backend field */
  calc(8px * var(--theme-radius-scale-mega, var(--theme-radius-scale, 1)))));  /* theme */

Read it as: whoever is more specific wins, and an unset token falls through.

Literal, then derived

Brand tokens are declared twice:

scss
--nav-link-active-bg: #{derive($secondary, 1.0674, 0.7915, 6.13)};
--nav-link-active-bg: oklch(from var(--theme-secondary) calc(l * 1.0674) …);

The first line is a static SCSS-computed colour for browsers without relative colour syntax; the second overwrites it where oklch(from …) is supported. Both lines are needed — the first is not dead code.

Building a new theme

  1. Copy the skeleton. Take yavi_lucerne as the template: composer.json, ext_emconf.php, Configuration/Sets/Full/, Resources/Public/Scss/.
  2. Set the Composer name and extension key, e.g. webagentur-yahya/yavi-zurich / yavi_zurich.
  3. Declare the Set:

``yaml # Configuration/Sets/Full/config.yaml name: yavi-zurich/full label: 'Yavi Zurich' dependencies: - yavi-core/full ``

  1. Give it its own product code in Configuration/Sets/Full/settings.yaml:

``yaml licence: product: yavi-zurich ``

Miss this and the licence check fails exactly like a wrong key.

  1. Declare the design in settings.yaml — colours, fonts, heights, radii. Completely, not as a deviation from Lucerne.
  2. Own the skins you need in Scss/Skin/, then run sync-theme-scss.php.
  3. Shape the navigation through the nav tokens plus Skin/navigation.scss. The eight variants themselves stay in core: they are imported through an interpolated path and cannot be replaced per theme.

Overriding templates

Template, partial and layout root paths are configured per Set. A theme adds its own paths at a higher index; TYPO3 falls back to core's file when the theme does not provide one. Copy only the file you actually change — every copied template is one you have to maintain when core's changes.

Things worth knowing before you debug for an hour