|
|
1 week ago | |
|---|---|---|
| css | 1 week ago | |
| fonts | 2 weeks ago | |
| icons | 1 week ago | |
| images/icons | 2 weeks ago | |
| js | 2 weeks ago | |
| templates | 1 week ago | |
| .gitignore | 1 week ago | |
| CLAUDE.md | 2 weeks ago | |
| README.md | 1 week ago | |
| bs.js | 1 week ago | |
| druid.icons.yml | 1 week ago | |
| druid.info.yml | 1 week ago | |
| druid.jpg | 1 week ago | |
| druid.libraries.yml | 1 week ago | |
| druid.theme | 1 week ago | |
| logo.svg | 1 week ago | |
| logo.svg.old | 1 week ago | |
| package.json | 1 week ago | |
| todo.md | 2 weeks ago | |
| yarn.lock | 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), andlibraries-extendentries that attach extra component CSS only when the corresponding core library is in use (e.g.core/drupal.dialog→druid/dialog). Thelibraries-extendlibraries (dialog,dropbutton,file,item-list,progress,drupal.tablesort,user) are separate from the always-oncomponentslibrary because they target widgets that don't appear on every page.druid.libraries.yml— every CSS library. New component CSS files added tocss/components/need a corresponding entry under thecomponents:library (or their own library if they should be conditional vialibraries-extend).druid.theme—hook_preprocess_HOOKfunctions. Currently onlydruid_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 crfrom 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 thecomponents:library indruid.libraries.ymlwithweight: -10. If the styles should only load when a specific core library is in use, give it its own library and wire it vialibraries-extendindruid.info.ymlinstead. - New JS behavior: add it inside
js/druid.behaviors.jsfollowing theDrupal.behaviors.druidX = { attach(context) { once('druid-x', '.selector', context).forEach(…) } }pattern. Always scope selectors tocontext, notdocument. For a separate file, register it in thebehaviors: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,: voidreturn type,/** Implements hook_X(). */docblock). - Drupal core / contrib CSS is unlayered. That means it wins against any
@layer componentsrule you write — this is the one place the architecture leaks. Options: bump specificity for the override, or wrap the offending core CSS via a futurehook_css_alter(not currently implemented). Don't reach for!importantreflexively. - 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.