Skip to content

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.css files 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 the css/ sub-path).
  • Component CSS copy: viteStaticCopy copies dist/css/components/ to components/ so each SDC can reference its own compiled CSS
  • SVG sprite: @pivanov/vite-plugin-svg-sprite generates default-icons.svg from icons/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's vite.config.js defines ONLY the '': 'css/**/*.pcss.css' entry (no component glob, no viteStaticCopy) and its css/ source has no themes/ 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 ships css/themes/dark.pcss.css, so it emits dist/css/app.css plus standalone dist/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:

  1. 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
  1. 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":

  1. Bundle layer (dist/css/app.css) -- Compiled from css/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.
  2. Per-theme override layer (dist/css/themes/<name>.css) -- Compiled from each css/themes/<name>.pcss.css source. Vite's cssGlob (css/**/*.pcss.css) emits one standalone file per override, NOT merged into app.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: false to the libraries-override block. The base theme ships ONLY dist/css/app.css (all themes bundled in; its dist/css/ has no themes/ directory), so there is nothing else to disable. The override layer lives entirely on the sub-theme's own-library side -- registering it in your daisyui block 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 build after generating -- A generated sub-theme ships source .pcss files but no dist/. Without the build step, dist/css/app.css does not exist and the theme has no styles. WHY: The base theme ships a pre-built dist/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.css override but never registered the compiled output -- You create css/themes/brand.pcss.css, run the build (which emits dist/css/themes/brand.css), but forget to add "dist/css/themes/brand.css": { minified: true } to the daisyui library in your *.libraries.yml. Result: the override silently does nothing -- app.css loads fine, so the page looks styled, but the override CSS is never sent to the browser. WHY: Vite emits each css/themes/*.pcss.css as a SEPARATE standalone file (not merged into app.css); only files listed in a loaded library are served. The libraries-override for the base theme does NOT cover this -- the override lives on your own-library side.
  • Not running npm run dev during development -- Changes to .pcss files, 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 -- The dist/ directory is generated output. Edits will be overwritten on next build. WHY: Edit source files in css/ and components/, then rebuild.
  • Not updating @source directives when adding template directories -- Tailwind only scans files matched by @source patterns for class usage. If you add a new directory (e.g., templates/paragraphs/), add a corresponding @source directive in app.pcss.css. WHY: Missing @source entries cause Tailwind to purge classes it thinks are unused.
  • Removing exclude: reset from 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 base styles from being applied.

See Also