d11 theme
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
rdrew 6c038f1d45 lots of layout changes + sidbars 1 week ago
css lots of layout changes + sidbars 1 week ago
fonts Scaffold JS behaviors entry point and prune unused .otf font files 2 weeks ago
icons icons a type styles 1 week ago
images/icons first commit 2 weeks ago
js Scaffold JS behaviors entry point and prune unused .otf font files 2 weeks ago
templates icons a type styles 1 week ago
.gitignore lots of layout changes + sidbars 1 week ago
CLAUDE.md Scaffold JS behaviors entry point and prune unused .otf font files 2 weeks ago
README.md lots of layout changes + sidbars 1 week ago
bs.js lots of layout changes + sidbars 1 week ago
druid.icons.yml icons a type styles 1 week ago
druid.info.yml lots of layout changes + sidbars 1 week ago
druid.jpg lots of layout changes + sidbars 1 week ago
druid.libraries.yml icons a type styles 1 week ago
druid.theme lots of layout changes + sidbars 1 week ago
logo.svg lots of layout changes + sidbars 1 week ago
logo.svg.old lots of layout changes + sidbars 1 week ago
package.json lots of layout changes + sidbars 1 week ago
todo.md Establish cascade-layer CSS architecture and self-host theme typography 2 weeks ago
yarn.lock lots of layout changes + sidbars 1 week ago

README.md

Druid Theme for Drupal 11

What this is

druid is a Drupal 11 custom theme generated from core's starterkit_theme (generator: starterkit_theme:11.3.7). It declares stable9 as its base theme in druid.info.yml, so any template not overridden here falls back to stable9's version. The theme lives at web/themes/custom/druid inside a larger Drupal site (sibling theme orion lives next to it; site-level composer.json is at the repo root).

There is no build pipeline, no test suite, no linter, and no preprocessor in this directory. CSS is hand-authored modern CSS (custom properties, cascade layers, nesting, container queries, :where(), color-mix(), etc.) and consumed directly by Drupal. "Building" the theme means clearing Drupal's cache so it picks up .yml and template changes — CSS content edits need no rebuild. See todo.md for the design rationale that informed this stack choice (no Sass, no PostCSS, no Pico).

CSS architecture: cascade layers

This is the central architectural decision and everything below it depends on understanding it.

Layer order, declared once in css/layers.css and authoritative for the whole theme:

@layer reset, tokens, base, layout, components, utilities;

Later layers win against earlier ones regardless of selector specificity. Each layer is filled by exactly one file (except components, which is split per UI element), and each file is registered as its own library so loading is deterministic:

Layer File(s) Library SMACSS cat. Weight
(order decl) css/layers.css druid/layers base -100
reset css/lib/modern-normalize.css druid/reset base -90
(font-face) css/fonts.css druid/fonts base -85
tokens css/tokens.css druid/tokens base -80
base css/base.css druid/base base -70
layout css/layout.css druid/layout layout -60
components css/components/*.css (32 files) druid/components component -10/file
utilities css/utilities.css druid/utilities theme -50

All eight libraries are attached on every page via druid.info.yml libraries: in cascade order. Do not remove or reorder that list — the layer-order declaration in layers.css must parse before any @layer foo { … } block, or the order becomes whatever Drupal happens to load first.

css/fonts.css is the one architecture file that intentionally lives outside any @layer block. @font-face declarations register font resources rather than cascade rules — they don't compete in the cascade, so layer membership is irrelevant. The font files themselves live in fonts/<family>/ (e.g. fonts/adelle/, fonts/open-sans/).

Critical footgun: unlayered CSS beats all layered CSS. Any rule outside a @layer { … } block silently wins against everything inside layers — this is per the CSS spec, not a bug. Every CSS file in this theme is wrapped; any new rule you author must go inside the appropriate @layer NAME { … } block, never alongside it.

!important inverts layer order. An !important declaration in an earlier layer beats !important in a later layer. Avoid !important inside @layer utilities — the layer is already designed to win.

Where to add a new CSS rule

Kind of rule Goes in
A --custom-property definition (color, size, font, radius, etc.) css/tokens.css
A new @font-face / web font css/fonts.css (+ woff2 file in fonts/<family>/)
Bare element selector (body, a, h1, ul, …) css/base.css
Region/grid/container arrangement (.layout-container, page shell) css/layout.css
A discrete UI widget (button, card, menu, form control, Drupal block) css/components/<thing>.css
Single-purpose helper (.text-center, .mt-4, print toggles) css/utilities.css
A browser default override css/lib/modern-normalize.css*

*Only when re-vendoring a new modern-normalize release — see the file header for the workflow.

JavaScript

There's one JS file: js/druid.behaviors.js, registered as the druid/behaviors library with core/drupal + core/once as dependencies, attached on every page. It's currently empty inside its IIFE — a scaffold ready for the first behavior.

All theme JS should use Drupal's behaviors system rather than plain DOMContentLoaded listeners, because Drupal swaps DOM fragments at runtime (AJAX views, form errors, contextual links, modals) and attach() is what re-runs on each injected fragment. Use once() to prevent double-initialization. The file's own header comment is the detailed pattern reference; if you split JS into multiple files, either co-locate them in the behaviors: library entry or register new libraries for conditional loading via libraries-extend.

Tokens / Open Props

css/tokens.css holds CSS custom properties on :root. We do not import the full Open Props library — instead, we cherry-pick individual tokens by copying their declarations into this file. The workflow is documented in the file's header comment; re-read it before adding tokens. The short version: browse https://open-props.style, copy the --prop: value; line, paste into the relevant section, preserve the Open Props name.

Other layout that matters

  • druid.info.yml — declares base theme, always-on libraries (one per cascade layer), and libraries-extend entries that attach extra component CSS only when the corresponding core library is in use (e.g. core/drupal.dialogdruid/dialog). The libraries-extend libraries (dialog, dropbutton, file, item-list, progress, drupal.tablesort, user) are separate from the always-on components library because they target widgets that don't appear on every page.
  • druid.libraries.yml — every CSS library. New component CSS files added to css/components/ need a corresponding entry under the components: library (or their own library if they should be conditional via libraries-extend).
  • druid.themehook_preprocess_HOOK functions. Currently only druid_preprocess_image_widget(), which strips the preview container when the user lacks access.
  • templates/ — Twig overrides organized by Drupal's own taxonomy (block/, content/, content-edit/, dataset/, field/, form/, layout/, misc/, navigation/, user/, views/). Filenames follow Drupal's theme-hook-suggestion convention (field--node--title.html.twig, block--system-menu-block.html.twig, etc.) — renaming them breaks the override.

Working in this theme

  • Cache clear: required after any .yml, .theme, or template change — drush cr from the site root. CSS content edits do not require it; only adding/removing files or library entries does.
  • New component CSS: create the file in css/components/, wrap its rules in @layer components { … }, register it under the components: library in druid.libraries.yml with weight: -10. If the styles should only load when a specific core library is in use, give it its own library and wire it via libraries-extend in druid.info.yml instead.
  • New JS behavior: add it inside js/druid.behaviors.js following the Drupal.behaviors.druidX = { attach(context) { once('druid-x', '.selector', context).forEach(…) } } pattern. Always scope selectors to context, not document. For a separate file, register it in the behaviors: library (same dependencies) or define a new library if it should load conditionally.
  • New Twig override: copy the upstream template from core/themes/stable9/templates/… (or the originating module) into the matching subdirectory here — don't write from scratch; the docblock comments document available variables and are worth keeping.
  • New preprocess hook: follow the existing signature in druid.theme (array &$variables, : void return type, /** Implements hook_X(). */ docblock).
  • Drupal core / contrib CSS is unlayered. That means it wins against any @layer components rule you write — this is the one place the architecture leaks. Options: bump specificity for the override, or wrap the offending core CSS via a future hook_css_alter (not currently implemented). Don't reach for !important reflexively.
  • The repo is generated from a starter kit; upstream Drupal docs at https://www.drupal.org/docs/core-modules-and-themes/core-themes/starterkit-theme are the canonical reference for regeneration and theme-API conventions.