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, templatesWhat the base theme provides
| Provides | Where | Used by subthemes as |
|---|---|---|
| React runtime: React 18 + ReactDOM UMD, loaded once per page | assets/react/*.js, library frame/react | dependencies: [frame/react] in a component |
Helpers: executeWhenVisible, rawHtml, attachBehaviors, ... | src/helpers.jsx → assets/helpers.js | globals, part of frame/react |
| API client with CSRF header | src/apiClient.js → assets/apiClient.js | global apiClient(), library frame/react-api-client |
CSRF token in drupalSettings.csrfToken | frame.theme | automatic, base theme hooks run for subthemes |
| Page layout and region list | templates/layout/page.html.twig, frame.info.yml, scss/base.scss | inherited template, restyle with your CSS |
| Build and test tooling: Vite, Sass, Jest, Babel, Testing Library and their configuration | bin/frame.mjs, vite.base.js, jest.base.cjs, node_modules | npm run build, npm run watch, npm test in a subtheme |
| Subtheme generator | bin/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 automatically | Not 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 chain | block 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:
"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"
}| Command | Does, in the current folder |
|---|---|
build | production build of components/*/index.jsx and scss/global.scss (when it exists) into assets/. build --dev for an unminified build with source maps |
watch | development build, rebuilt on change |
test | Jest 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:
// 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()] };
});// jest.config.cjs
module.exports = require('../../contrib/frame/jest.base.cjs')(__dirname, { testPathIgnorePatterns: ['/node_modules/', '/assets/', '/e2e/'] });createViteConfig options
| Option | Default | Meaning |
|---|---|---|
root | required | the theme directory, pass import.meta.dirname |
mode | production | production 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} |
componentsDir | components | folder scanned for */index.jsx |
outDir | assets | must match the paths in your libraries and libraryOverrides |
runtime | false | base 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.twigto your subtheme (same path) and change it. Or only restyle: the class names are stable. - CSS variables:
--page-max-widthand--page-gutterdrive the layout skeleton, set them in your:root. - Libraries:
libraries-override/libraries-extendin yourinfo.ymlwork 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
.themehooks 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 documentation | In Frame |
|---|---|
| Regions are not inherited: copy them into the subtheme | the 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 config | the 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 IDs | the 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 inherited | the base theme has no theme settings |
| Sub-themes can be nested | a 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:
# 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