Sub-theming (Starterkit)
When to Use
Create a sub-theme when you need to customize the base theme's components, add new components, override UI Styles, or set up a proper Tailwind/DaisyUI build pipeline. Alpha6 ships a full starterkit with Vite, Tailwind CSS 4, DaisyUI 5, and example component overrides.
Steps
Generate a sub-theme using Drupal's starterkit command:
# From the Drupal root:
php core/scripts/drupal generate-theme my_theme \
--starterkit ui_suite_daisyui_starterkit \
--path themes/custom
# Install npm dependencies:
cd themes/custom/my_theme
npm install
# Build assets:
npm run build
# Or watch for changes during development:
npm run dev
Then enable the theme:
drush theme:enable my_theme
drush config:set system.theme default my_theme
Starterkit Structure
my_theme/
my_theme.info.yml # Base theme: ui_suite_daisyui, library overrides
my_theme.libraries.yml # daisyui library: dist/css/app.css + one dist/css/themes/<name>.css per theme override
my_theme.theme # Preprocess hooks
my_theme.ui_styles.yml # Custom UI Styles (replaces some base theme styles)
my_theme.ui_skins.themes.yml # Theme switching config
my_theme.icons.yml # Icon pack config
package.json # npm deps: Tailwind 4, DaisyUI 5, Vite 6
vite.config.js # Rollup entry gen + viteStaticCopy + SVG sprite
postcss.config.js # PostCSS: stylelint + postcss-import + @tailwindcss/postcss
stylelint.config.js # Stylelint config for CSS linting
css/
app.pcss.css # Main entry: imports config, plugins, base, utilities
tailwind.config.pcss # @theme: fonts + custom grid column tokens
safelist.txt # Safelist of DaisyUI classes for the build
plugins/
daisyui.config.pcss # @plugin "daisyui": theme list, exclude: reset
tailwindcss.typography.config.pcss # @plugin "@tailwindcss/typography"
base/
base.pcss # @layer base: heading sizes (h1-h6)
themes/
dark.pcss.css # Theme override example (dark theme primary color)
utilities/
containers.pcss # container-xs through container-full
gap.pcss # Responsive gap-xs through gap-2xl
grid.pcss # grid-1 through grid-4, ratio variants
margin.pcss # Semantic mb-/mt- (xs through 2xl + auto)
padding.pcss # Semantic pb-/pt- (xs through 2xl + auto)
typography.pcss # title-2xl through title-2xs, copy-2xl through copy-xs
utilities.pcss # Custom utility overrides (empty by default)
components/
card/ # Overrides ui_suite_daisyui:card (replaces key)
cta/ # New component (CTA section)
section/ # New component (flexible layout section)
config/
optional/ # 10 block placement configs
install/ # Theme settings
schema/ # Config schema
templates/
system/ # System template overrides (page, block, etc.)
ui_patterns_library/ # UI Patterns library page templates
ui_styles_library/ # UI Styles overview template
ui_examples/ # UI Examples overview template
icons/
default/ # SVG icons (close, play) for sprite generation
dist/ # Build output (generated by Vite)
Vite Build Pipeline
The vite.config.js generates Rollup entry points from glob patterns:
// Entry point generation (starterkit vite.config.js):
const cssGlob = {
'': 'css/**/*.pcss.css', // Global CSS -> dist/css/ (flat, per source file)
'css': 'components/**/*.pcss.css', // Component CSS -> dist/css/components/
};
Key behaviors:
- Entry generation: Globs all
.pcss.cssfiles and creates one Rollup entry per file. Each source file becomes a separate output -- the build does NOT merge them.css/app.pcss.css->dist/css/app.css;css/themes/dark.pcss.css->dist/css/themes/dark.css(one standalone file per theme override, preserving thecss/sub-path). - Component CSS copy:
viteStaticCopycopiesdist/css/components/tocomponents/so each SDC can reference its own compiled CSS - SVG sprite:
@pivanov/vite-plugin-svg-spritegeneratesdefault-icons.svgfromicons/default/ - PostCSS pipeline:
postcss-import+@tailwindcss/postcss(Tailwind 4) +stylelint - Minification: Only in production (
NODE_ENV=production)
Base theme vs starterkit
cssGlob: the base theme'svite.config.jsdefines ONLY the'': 'css/**/*.pcss.css'entry (no component glob, noviteStaticCopy) and itscss/source has nothemes/overrides -- so the base theme emits exactly one file,dist/css/app.css, with all 35 DaisyUI themes bundled in. The starterkit adds the component glob AND shipscss/themes/dark.pcss.css, so it emitsdist/css/app.cssplus standalonedist/css/themes/dark.css. This is the root of the two-layer CSS model documented below.
npm scripts:
| Script | Command | Purpose |
|---|---|---|
npm run build |
NODE_ENV=production vite build |
Production build (minified) |
npm run dev |
NODE_ENV=development vite build --watch |
Development watch mode |
npm run lint:css |
stylelint '**/*.(pcss.css\|pcss)' |
Lint CSS files |
npm run lint:css:fix |
stylelint ... --fix |
Auto-fix lint issues |
Tailwind & DaisyUI Config
The starterkit uses Tailwind CSS 4's CSS-first configuration (no tailwind.config.js file). All config lives in .pcss files:
css/app.pcss.css -- Main entry:
@import "./tailwind.config.pcss";
@import "tailwindcss" source(none);
/* Plugins */
@import "./plugins/daisyui.config.pcss";
@import "./plugins/tailwindcss.typography.config.pcss";
/* Base styles and utilities */
@import "./base/base.pcss";
@import "./utilities/containers.pcss";
/* ... other utility imports ... */
/* Source directives: tell Tailwind where to scan for class usage */
@source "../templates/**/*.{twig,js}";
@source "../components/**/*.{twig,js,yml}";
@source "../*.ui_styles.yml";
@source "../*.icons.yml";
@source "./safelist.txt";
/* Also scan the BASE theme so its component/template classes survive tree-shaking: */
@source "../../../contrib/ui_suite_daisyui/templates/**/*.{twig,js}";
@source "../../../contrib/ui_suite_daisyui/components/**/*.{twig,js,yml}";
@source "../../../contrib/ui_suite_daisyui/*.icons.yml";
@source "../../../contrib/ui_suite_daisyui/*.ui_styles.yml";
css/plugins/daisyui.config.pcss -- DaisyUI plugin:
@plugin "daisyui" {
exclude: reset; /* Prevent DaisyUI reset from conflicting with Tailwind preflight */
themes: light --default, dark, cupcake, aqua, /* ... all 35 themes ... */;
logs: false;
}
css/tailwind.config.pcss -- Theme tokens:
@theme {
--font-sans: ui-sans-serif, system-ui, sans-serif, ...;
--font-serif: ui-serif, Georgia, ...;
--font-mono: ui-monospace, SFMono-Regular, ...;
/* Custom grid column ratios */
--grid-cols--66x33: minmax(0, 2fr) minmax(0, 1fr);
--grid-cols--33x66: minmax(0, 1fr) minmax(0, 2fr);
--grid-cols--50x25x25: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
--grid-cols--25x50x25: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
--grid-cols--25x25x50: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2fr);
}
css/themes/dark.pcss.css -- Theme override example:
@plugin "daisyui/theme" {
name: "dark";
--color-primary: oklch(55% 0.3 240);
}
Custom Utilities
The css/utilities/ directory defines semantic utility classes using Tailwind's @utility directive with responsive breakpoints. These replace raw Tailwind spacing/sizing values with a semantic token system:
| File | Utilities | Pattern |
|---|---|---|
containers.pcss |
container-xs through container-full |
Max-width + responsive padding |
gap.pcss |
gap-xs through gap-2xl |
Responsive gap (e.g., gap-sm = gap-2 md:gap-4) |
grid.pcss |
grid-1 through grid-4, ratio variants |
Responsive column layouts (e.g., grid-2-66x33) |
margin.pcss |
mb-xs/mt-xs through mb-2xl/mt-2xl + auto |
Responsive vertical margin |
padding.pcss |
pb-xs/pt-xs through pb-2xl/pt-2xl + auto |
Responsive vertical padding |
typography.pcss |
title-2xl through title-2xs, copy-2xl through copy-xs |
Custom font-size tokens via @theme variables |
Example -- gap-md resolves to gap-4 md:gap-6 (16px on mobile, 24px on md+).
Component Overrides
The starterkit includes three example components showing both override and creation patterns:
Card override (components/card/card.component.yml):
name: Card
replaces: 'ui_suite_daisyui:card'
# Full schema defined -- adds url prop and actions_position prop
# Twig template customizes card layout (link wrapper, action positioning)
The replaces key tells Drupal: "When anything references ui_suite_daisyui:card, use this sub-theme's card instead." The component defines its own full schema and Twig template. The card also includes card.pcss.css for custom styles (e.g., .card-title { @apply title-md; }).
CTA section (components/cta/) -- New component (no replaces):
name: CTA
group: CTA Section
variants: default, primary, secondary
slots: title, text, buttons
props: heading_level (1-6), centered (boolean)
Uses custom CSS classes (.cta, .cta-primary, .cta-secondary) defined in cta.pcss.css with Tailwind @apply directives. Demonstrates how to create entirely new components in the sub-theme.
Section layout (components/section/) -- New flexible layout component (experimental):
name: Section
group: Layout system
status: experimental
props: layout_attributes, grid_attributes, title_attributes, content_attributes,
footer_attributes (all $ref: ui-patterns://attributes), heading_level
slots: title, content, items (grid), footer
Designed to work with UI Styles -- each *_attributes prop accepts CSS classes from the admin UI. The Twig template applies sensible defaults (e.g., container-lg, gap-md, pt-auto/pb-auto) when no styles are set.
UI Styles Customization
The starterkit's .ui_styles.yml does two things:
- Adds custom style options using semantic tokens from the utility classes:
typography_font_size_starterkit:
category: "Typography"
label: "Font size"
options:
title-2xl: Title 2xl (42px)
title-xl: Title xl (36px)
# ... through title-2xs and copy-2xl through copy-xs
layout_grid_starterkit:
category: "Layout"
label: "Grid"
options:
grid-1: 1 column
grid-2: 2 columns 50x50
grid-2-33x66: 2 columns 33x66
grid-2-66x33: 2 columns 66x33
# ... through grid-4
layout_container_starterkit:
category: "Layout"
label: "Container"
options:
container-xs: Container xs (672px)
container-sm: Container sm (896px)
# ... through container-full
- Disables base theme styles that the starterkit replaces:
# Disable base theme styles replaced by starterkit versions
daisyui_glass:
enabled: false
typography_font_size:
enabled: false
spacing_padding_top:
enabled: false
spacing_padding_bottom:
enabled: false
spacing_margin_bottom:
enabled: false
effects_box_shadow:
enabled: false
sizing_width:
enabled: false
daisyui_mask:
enabled: false
Two-Layer CSS Model & Library Registration
A generated sub-theme uses a two-layer compiled-CSS model. Getting both layers registered is the single most common cause of "my theme override does nothing":
- Bundle layer (
dist/css/app.css) -- Compiled fromcss/app.pcss.css. Contains Tailwind, the DaisyUI plugin, and ALL 35 DaisyUI themes bundled together. This is the same shape as the base theme's single library. - Per-theme override layer (
dist/css/themes/<name>.css) -- Compiled from eachcss/themes/<name>.pcss.csssource. Vite'scssGlob(css/**/*.pcss.css) emits one standalone file per override, NOT merged intoapp.css. The starterkit ships exactly one such override:css/themes/dark.pcss.css->dist/css/themes/dark.css.
Because the override is a separate output file, the sub-theme's OWN *.libraries.yml must register BOTH layers explicitly. The real starterkit ui_suite_daisyui_starterkit.libraries.yml:
# my_theme.libraries.yml
daisyui:
css:
theme:
"dist/css/app.css": { minified: true } # bundle (all themes)
"dist/css/themes/dark.css": { minified: true } # standalone per-theme override
Add one dist/css/themes/<name>.css line here for every css/themes/<name>.pcss.css you create. Then disable the base theme's bundle in .info.yml so the two app.css files do not stack:
# my_theme.info.yml
libraries:
- my_theme/daisyui # Sub-theme's own library (the daisyui block above)
libraries-override:
ui_suite_daisyui/daisyui: # Disable ONLY the base theme's app.css
css:
theme:
"dist/css/app.css": false
Do NOT add
themes/*.css: falseto thelibraries-overrideblock. The base theme ships ONLYdist/css/app.css(all themes bundled in; itsdist/css/has nothemes/directory), so there is nothing else to disable. The override layer lives entirely on the sub-theme's own-library side -- registering it in yourdaisyuiblock is what makes it load.
This replaces (does not stack on) the base theme's pre-built dist/css/app.css with the sub-theme's Vite-built CSS.
Common Mistakes
- Forgetting
npm install && npm run buildafter generating -- A generated sub-theme ships source.pcssfiles but nodist/. Without the build step,dist/css/app.cssdoes not exist and the theme has no styles. WHY: The base theme ships a pre-builtdist/css/app.css, but the starterkit ships only source -- you must run its local Vite/PostCSS build to produce the compiled CSS. - Added a
css/themes/<name>.pcss.cssoverride but never registered the compiled output -- You createcss/themes/brand.pcss.css, run the build (which emitsdist/css/themes/brand.css), but forget to add"dist/css/themes/brand.css": { minified: true }to thedaisyuilibrary in your*.libraries.yml. Result: the override silently does nothing --app.cssloads fine, so the page looks styled, but the override CSS is never sent to the browser. WHY: Vite emits eachcss/themes/*.pcss.cssas a SEPARATE standalone file (not merged intoapp.css); only files listed in a loaded library are served. Thelibraries-overridefor the base theme does NOT cover this -- the override lives on your own-library side. - Not running
npm run devduring development -- Changes to.pcssfiles, Twig templates, or utility classes are not reflected until rebuilt. WHY: The build step compiles PostCSS/Tailwind and copies component CSS. - Editing
dist/files directly -- Thedist/directory is generated output. Edits will be overwritten on next build. WHY: Edit source files incss/andcomponents/, then rebuild. - Not updating
@sourcedirectives when adding template directories -- Tailwind only scans files matched by@sourcepatterns for class usage. If you add a new directory (e.g.,templates/paragraphs/), add a corresponding@sourcedirective inapp.pcss.css. WHY: Missing@sourceentries cause Tailwind to purge classes it thinks are unused. - Removing
exclude: resetfrom DaisyUI plugin config -- The starterkit excludes DaisyUI's reset because Tailwind 4 provides its own (Preflight). Including both causes style conflicts. WHY: DaisyUI's reset prevents@layer basestyles from being applied.
See Also
drupal-twig-theming.md-- Sub-theme creation patterns- Starterkit issue #3508143 -- Original starterkit feature request