Skip to content

Base theme and subthemes ​

Frame is a base theme. You do not edit it, you create a subtheme that inherits from it and holds your components. That keeps the scaffold updatable: pulling a new version of the base theme gives every subtheme the new runtime, helpers and build config.

themes/
  contrib/frame/        ← base theme (git/composer, never edited)
  custom/my_theme/               ← your subtheme: components, styles, templates

What the base theme provides ​

ProvidesWhereUsed by subthemes as
React runtime: React 18 + ReactDOM UMD, loaded once per pageassets/react/*.js, library frame/reactdependencies: [frame/react] in a component
Helpers: executeWhenVisible, rawHtml, attachBehaviors, ...src/helpers.jsx → assets/helpers.jsglobals, part of frame/react
API client with CSRF headersrc/apiClient.js → assets/apiClient.jsglobal apiClient(), library frame/react-api-client
CSRF token in drupalSettings.csrfTokenframe.themeautomatic, base theme hooks run for subthemes
Page layout and region listtemplates/layout/page.html.twig, frame.info.yml, scss/base.scssinherited template, restyle with your CSS
Build and test tooling: Vite, Sass, Jest, Babel, Testing Library and their configurationbin/frame.mjs, vite.base.js, jest.base.cjs, node_modulesnpm run build, npm run watch, npm test in a subtheme
Subtheme generatorbin/frame.mjs new, template in starterkit/node <base theme>/bin/frame.mjs new my_theme

The base theme itself has base theme: false: it does not depend on stable9 or any other theme, core's default templates and CSS apply. It ships no components, only the runtime. Compiled files are not in git: a release archive contains the built assets/, a git checkout needs npm install && npm run dist once. Node is needed to build your subtheme, never to run the site.

What a subtheme inherits, and what it does not ​

Inherited automaticallyNot inherited, declare it in the subtheme
libraries listed under libraries: in the base info.yml (global-styling, global-libraries)regions: Drupal does not inherit regions: from a base theme. Without them core's default list applies and blocks placed in page_title, breadcrumbs... are disabled
library definitions (frame/react can be used from any subtheme component)the subtheme's own global-styling library and CSS
page.html.twig and all templates of the base chainblock placement (blocks belong to a theme, ship them as config/optional/block.block.*.yml)
.theme hooks (frame_js_settings_alter)the subtheme's own assets (npm run build)
SDC components of the base theme (none shipped)

The generated subtheme declares the current region list in your info.yml (a test keeps it equal to the base theme's). If a new base version adds regions, add them to your subtheme too (the changelog says so).

How the pieces fit at runtime ​

page ─► frame/global-libraries   (base: core/drupal, once, drupalSettings)
    ├─► frame/global-styling     (base: layout skeleton CSS)
    ├─► my_theme/global-styling           (subtheme: your CSS)
    └─► my_theme:my-widget library        (generated by SDC, only when the component renders)
          └─ depends on frame/react   (React UMD + helpers, from the base theme)

The build contract ​

Subtheme build output goes to the subtheme's assets/. The base theme only builds its own runtime. The shared factory guarantees that both follow the same rules (one bundle per components/<name>/index.jsx, React as an external), see Build.

The tooling lives in the base theme and nowhere else. A subtheme has no build configuration and no build dependencies, its package.json is three scripts that call the base theme's command line:

json
"scripts": {
  "build": "node ../../contrib/frame/bin/frame.mjs build",
  "watch": "node ../../contrib/frame/bin/frame.mjs watch",
  "test": "node ../../contrib/frame/bin/frame.mjs test"
}
CommandDoes, in the current folder
buildproduction build of components/*/index.jsx and scss/global.scss (when it exists) into assets/. build --dev for an unminified build with source maps
watchdevelopment build, rebuilt on change
testJest with the shared configuration, extra arguments go to Jest (npm test -- --watch)
new <name>creates a subtheme, see Create a subtheme

Vite, Sass, Jest, the plugins and presets are resolved from the base theme's node_modules, so run npm install in the base theme once. The configuration is read from whatever base theme version is installed: updating the base theme updates the build of every subtheme, nothing is copied into them.

Your own configuration (optional) ​

Most themes never need this. If a theme has a vite.config.js, build and watch use it instead of the shared one, and the same for jest.config.cjs and test. Start from the shared factory:

js
// vite.config.js
import { defineConfig } from 'vite';
import { createViteConfig } from '../../contrib/frame/vite.base.js';

export default defineConfig(({ mode }) => {
  const config = createViteConfig({ root: import.meta.dirname, mode, entries: { global: './scss/global.scss' } });
  return { ...config, plugins: [...config.plugins, myPlugin()] };
});
js
// jest.config.cjs
module.exports = require('../../contrib/frame/jest.base.cjs')(__dirname, { testPathIgnorePatterns: ['/node_modules/', '/assets/', '/e2e/'] });

createViteConfig options ​

OptionDefaultMeaning
rootrequiredthe theme directory, pass import.meta.dirname
modeproductionproduction minifies, development keeps source maps and uses React dev builds (base theme only)
entries{}extra entries { name: './path' }, e.g. a global SCSS file. Output: assets/<name>.{js,css}
componentsDircomponentsfolder scanned for */index.jsx
outDirassetsmust match the paths in your libraries and libraryOverrides
runtimefalsebase theme only: also builds src/ (helpers, apiClient, common) and copies the React UMD files

Overriding without editing the base theme ​

  • Page layout: copy templates/layout/page.html.twig to your subtheme (same path) and change it. Or only restyle: the class names are stable.
  • CSS variables: --page-max-width and --page-gutter drive the layout skeleton, set them in your :root.
  • Libraries: libraries-override / libraries-extend in your info.yml work as for any base theme, e.g. to replace the React UMD with another build.
  • Regions: your own regions: (keep the base names, the base template prints them).
  • Hooks: the same .theme hooks in your subtheme add to the base ones, they do not replace them.

Following the Drupal sub-theme rules ​

Creating sub-themes lists what a subtheme has to handle itself. How Frame deals with each:

Rule from the Drupal documentationIn Frame
Regions are not inherited: copy them into the subthemethe starterkit's info.yml has the base theme's regions, a test keeps them equal
Block placement is inherited only from the default theme and only if the subtheme ships no block configthe starterkit ships config/optional/block.block.*.yml (branding, main menu, page title...) for the generated theme, edit or replace them
Block templates are named after the block ID, a subtheme's blocks have other IDsthe base theme has no block templates. If you add some in your subtheme, add hook_theme_suggestions_block_alter as described in the doc
Config schema of theme settings is not inheritedthe base theme has no theme settings
Sub-themes can be nesteda subtheme can be the base of another one, it inherits libraries, templates and hooks the same way

Releases and updating the base theme ​

Compiled files are never committed. The workflow .github/workflows/release.yml builds them when a version tag is pushed (git tag v1.0.0 && git push --tags) and attaches frame-<version>.zip and .tar.gz to the GitHub release: the theme, with assets/, without docs and CI files. CI (ci.yml) builds the base theme, the example subtheme, and installs Drupal on SQLite with two subthemes (one from frame new, one from core's generate-theme) to check in a browser that the component mounts and the runtime, CSRF token, page template, regions and blocks are inherited (demo/smoke.mjs), on every push and pull request.

To update:

bash
# release archive: replace the folder (or `composer update` with the pinned version bumped)
# git checkout:
cd themes/contrib/frame && git pull && npm install && npm run dist

# then, per subtheme
cd themes/custom/my_theme && npm run build && drush cr
  • Read CHANGELOG.md: it lists the public API, anything outside it can change in a minor version.
  • Subthemes call the base theme's tooling, so a new base version changes the build of every subtheme at the next npm run build. Nothing in the subtheme has to be updated. The runtime (assets/) updates with the base theme files.
  • If a release changes the React major version, rebuild all subthemes (React is an external, nothing is bundled, but the UMD API must match).
  • If a release adds regions, add them to your subtheme's info.yml.

Example subtheme ​

examples/frame_demo is a complete subtheme that lives inside the base theme repository (Drupal discovers nested themes). It contains the three example components, two views, block placement and styling, and is the subtheme used for the demos. Read it as a reference for how a real subtheme looks:

frame_demo/
  frame_demo.info.yml        base theme: frame, regions, libraries
  frame_demo.libraries.yml   global-styling → assets/global.css
  frame_demo.theme           preprocess for the tooltip
  package.json                        scripts that call the base theme, rsuite and react-tippy as dependencies
  scss/global.scss                    look of the theme
  components/{react-tooltip,node-list,recipe-explorer}/
  templates/views/                    mount the SDCs on the view pages
  config/optional/                    views + block placement