Create a subtheme
One command
# from the Drupal root
node web/themes/contrib/frame/bin/frame.mjs new my_theme --name "My Theme"
cd web/themes/custom/my_theme
npm run build
drush theme:install my_theme && drush config:set system.theme default my_theme -y && drush cr| Option | Meaning |
|---|---|
my_theme | machine name: lowercase letters, digits, underscores |
--name | label for the info.yml, default is the machine name in title case |
--path | folder to create the theme in. Default: themes/custom when the base theme is in themes/contrib, otherwise next to the base theme |
It needs only Node, no Drupal bootstrap. The base theme needs npm install once, see Getting started.
What you get:
my_theme/
my_theme.info.yml base theme: frame, libraries, regions
my_theme.libraries.yml global-styling → assets/global.css
package.json three scripts that call the base theme: build, watch, test. No dependencies
scss/global.scss your global styles; reads the design tokens of the info file (frame.space(m), frame.color(dark))
components/hello-react/ working example: yml, twig, index.jsx, HelloReact.jsx, scss, test
config/optional/ block placement: branding, main menu, page title, content, messages...
logo.svg README.md .gitignoreThere is no build configuration in a subtheme: no vite.config.js, no jest.config.cjs, no Babel config, no node_modules. The whole package.json is:
{
"name": "my_theme",
"private": true,
"type": "module",
"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"
}
}The path is the relative path from your theme to the base theme, written by new. Vite, Sass, Jest and their configuration live in the base theme, so updating the base theme updates the build of every subtheme and there is no scaffolding to re-sync. If you move a theme, fix this path.
Need an npm library in a component (a date picker, a table)? npm install <library> in the subtheme: it gets a dependencies entry and its own node_modules, and the library is bundled into the components that import it.
With core's generator
The same template is a core starterkit (frame_starterkit), see the sub-theme documentation:
php web/core/scripts/drupal generate-theme my_theme --starterkit frame_starterkit --path themes/custom --name "My Theme"(On Drupal 11.4+ use web/core/scripts/dr.) The result is the same theme, with generator: frame_starterkit:<version> in the info file. Core cannot compute paths, so the scripts in package.json assume themes/contrib/frame and themes/custom/my_theme. Fix the path there if your layout differs (for example your theme is in themes/my_theme).
What to decide after generating
- Look: write your CSS in
scss/global.scss(built toassets/global.css). The base layout is neutral: header, main with optional sidebar, footer, styled through the.page__*classes and the--page-max-widthand--page-guttervariables. Withbase theme: falsethe markup is core's default templates (no classes on menus and blocks), style by structure or override the templates in your subtheme. - Page layout: to change the HTML, copy
templates/layout/page.html.twigfrom the base theme into the same path in your theme. - Blocks: the generated
config/optional/block.block.my_theme_*.ymlplace the basic blocks when the theme is installed. Edit them, or place blocks in the UI. - Regions: keep the generated list unless you also override
page.html.twig. Regions are not inherited, see Base theme. - Remove the example: delete
components/hello-reactwhen you have your own component.
By hand
The generator is only a copy with renames. The minimum of a subtheme is:
# my_theme.info.yml
name: My Theme
type: theme
base theme: frame
core_version_requirement: ^10.3 || ^11
libraries:
- my_theme/global-styling
regions: # copy from frame.info.yml, regions are not inherited
header: Header
# ...# my_theme.libraries.yml
global-styling:
css:
theme:
assets/global.css: {}This is also what drush generate theme produces when you answer frame for the base theme. For the build and the tests add the three scripts shown above to a package.json (or call the CLI directly: node <base theme>/bin/frame.mjs build in the theme folder). scss/global.scss is built to assets/global.css when it exists, components are discovered from components/.
Existing site, existing theme?
You do not need to rebase your whole theme. A subtheme is just a theme: if your current theme can have frame as base theme, change base theme:. Otherwise keep your theme and add the React runtime to it:
- attach
frame/reactfrom your components'libraryOverrides.dependencies(libraries of any installed theme can be attached from anywhere), - build with the same three
package.jsonscripts as a subtheme.
This was tested with a theme that has base theme: false: the component mounts and executeWhenVisible, apiClient and rawHtml are available. What you give up by not extending the base theme:
- the inherited page template and the
libraries:auto-attach: addframe/global-librariesto yourlibraries:if you want the same libraries on every page, - the CSRF token:
drupalSettings.csrfTokenis set by a hook of the base theme, which only runs whenframeis in the active theme's chain. Without itapiClientsends no token, fine for GET requests, writes will be rejected. Add the hook to your own.themefile:
use Drupal\Core\Access\CsrfRequestHeaderAccessCheck;
use Drupal\Core\Asset\AttachedAssetsInterface;
function my_theme_js_settings_alter(array &$settings, AttachedAssetsInterface $assets) {
$settings['csrfToken'] = \Drupal::service('csrf_token')->get(CsrfRequestHeaderAccessCheck::TOKEN_KEY);
}Deploying
assets/ of the subtheme is git-ignored. In CI run npm ci in the base theme, then npm run build in the subtheme (and npm ci there first if it has dependencies of its own). The base theme's own assets/ come with its release archive.