Drupal SDC + UIkit 3: Setting Up Your First Theme

Type
Knowledge

The combination of Single Directory Components and UIkit 3 provides a solid foundation for component-driven Drupal theming. But how do you actually set up such a theme? This article walks through the practical steps: from theme skeleton to your first working component.

Theme skeleton: clear directory structure

Start with a minimal theme structure that leaves room for growth:

mytheme/
├── mytheme.info.yml
├── mytheme.libraries.yml
├── mytheme.theme
├── components/
│   └── alert/
│       ├── alert.component.yml
│       ├── alert.twig
│       └── alert.css
├── templates/
└── assets/
    ├── uikit/
    ├── css/
    └── js/

The components/ directory is the heart of your SDC setup. Drupal automatically scans it and makes components available via Twig ({% include 'mytheme:alert' %}). Classic template overrides remain in templates/, allowing both approaches to coexist.

Integrating UIkit as foundation library

Load UIkit as a global library in mytheme.libraries.yml:

global-styling:
  css:
    theme:
      assets/uikit/css/uikit.min.css: {}
      assets/css/custom.css: {}
  js:
    assets/uikit/js/uikit.min.js: {}
    assets/uikit/js/uikit-icons.min.js: {}

Activate the library in mytheme.info.yml:

name: My Theme
type: theme
base theme: false
core_version_requirement: ^10 || ^11
libraries:
  - mytheme/global-styling

Download UIkit directly from getyikit.com and place the files in assets/uikit/. This keeps dependencies local and avoids external CDN dependencies—important for projects with strict CSP policies or compliance requirements.

Your first component: a Card

A card component illustrates the collaboration between SDC and UIkit. Start with components/card/card.component.yml:

'$schema': https://git.drupalcode.org/project/drupal/-/raw/11.x/core/modules/sdc/src/metadata.schema.json
name: Card
status: stable
props:
  type: object
  properties:
    title:
      type: string
      title: Card title
    image_url:
      type: string
      title: Image URL
    variant:
      type: string
      title: Card variant
      enum:
        - default
        - primary
        - secondary
      default: default
slots:
  content:
    title: Card content

The component schema explicitly defines what a card can and should do. This prevents ad-hoc modifications and makes the API transparent to other developers.

Markup in card.twig:

{% set variant_class = variant != 'default' ? 'uk-card-' ~ variant : '' %}

<div class="uk-card uk-card-body {{ variant_class }}">
  {% if image_url %}
    <div class="uk-card-media-top">
      <img src="{{ image_url }}" alt="{{ title }}">
    </div>
  {% endif %}
  
  {% if title %}
    <h3 class="uk-card-title">{{ title }}</h3>
  {% endif %}
  
  <div class="uk-card-content">
    {% block content %}{% endblock %}
  </div>
</div>

UIkit styling works immediately—no additional CSS needed. Place component-specific overrides in card.css, only when necessary.

Using components in Drupal

SDC components support two syntax styles depending on your Drupal version.

Modern syntax (Drupal 10.3+, recommended)

The component tag syntax is cleaner and reads like HTML:

<twig:mytheme:card title="Project title" variant="primary">
  <twig:block name="content">
    <p>Project description goes here.</p>
  </twig:block>
</twig:mytheme:card>

For node templates (node--article.html.twig):

<twig:mytheme:card 
  title="{{ node.title.value }}" 
  variant="secondary"
  image_url="{{ file_url(node.field_image.entity.uri.value) }}">
  <twig:block name="content">
    {{ content.body }}
  </twig:block>
</twig:mytheme:card>

Classic syntax (all versions)

For components without slots, use {% include %}:

{% include 'mytheme:button' with {
  label: 'Read more',
  url: node.url
} only %}

For components with slots or blocks, use {% embed %}:

{% embed 'mytheme:card' with {
  title: 'Project title',
  variant: 'primary'
} only %}
  {% block content %}
    <p>Project description goes here.</p>
  {% endblock %}
{% endembed %}

Important: {% include %} is self-closing and cannot contain blocks. Use {% embed %} when you need to fill slots with dynamic content.

The only keyword isolates scope and prevents variables from the parent template leaking into the component. This keeps components predictable.

Component library dependency management

When a component needs specific UIkit modules (such as lightbox or slideshow), define a library in mytheme.libraries.yml:

component.gallery:
  css:
    component:
      components/gallery/gallery.css: {}
  js:
    assets/uikit/js/components/lightbox.min.js: {}
  dependencies:
    - mytheme/global-styling

Download additional UIkit components from getyikit.com and place them in your assets/uikit/js/components/ directory alongside the core files. This maintains consistency with your self-hosted approach.

Link the library to your component in gallery.component.yml:

libraryOverrides:
  dependencies:
    - mytheme/component.gallery

Drupal automatically loads the library when the component is used. This prevents unnecessary asset loading on pages without galleries.

Naming and organization

Maintain consistent naming conventions:

  • Component name: lowercase, descriptive (card, hero-banner, author-bio)
  • Props: camelCase (imageUrl, showMeta)
  • Variants: lowercase strings (primary, secondary, large)

Group related components in subfolders as your theming grows:

components/
├── content/
│   ├── card/
│   ├── teaser/
│   └── author-bio/
├── navigation/
│   ├── main-menu/
│   └── breadcrumb/
└── layout/
    ├── hero/
    └── section/

This keeps your component library organized and makes code reviews more efficient.

Development workflow: cache clearing

SDC discovery cache often needs clearing during development. Three options:

  • Drush: drush cr
  • Admin UI: Configuration > Development > Clear all caches
  • Null coalescing in settings.local.php for automatic discovery

For active development:

# settings.local.php
$settings['cache']['bins']['discovery'] = 'cache.backend.null';

This ensures component changes are immediately visible without cache clearing.

Security and Content Security Policy

Self-hosted UIkit assets significantly simplify CSP configuration. Your CSP header can remain restrictive:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';

The unsafe-inline for styles is needed for UIkit's dynamic positioning, but scripts remain limited to your own domain. This is a major advantage over setups with external CDN dependencies.

Conclusion and next steps

You now have a working SDC + UIkit setup with clear separation between foundation and project layer. This foundation provides room to grow: add new components, extend existing ones, or gradually migrate legacy templates.

In a follow-up article, we'll dive into more complex patterns: form integration, Views rendering, Layout Builder support, and component composition.

For a production setup: thoroughly test your components in different contexts, document your component API for team members, and set up monitoring to optimize asset loading.