PHPackages                             alan-221b/twig-deluxe - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. alan-221b/twig-deluxe

ActiveDrupal-module[Templating &amp; Views](/categories/templating)

alan-221b/twig-deluxe
=====================

Scoped CSS and JS for Drupal Twig templates — Vue/Svelte-style component isolation

1.0.0(4mo ago)029↑1969%GPL-2.0-or-laterPHP

Since Feb 26Pushed 3mo agoCompare

[ Source](https://github.com/Alan-221b/drupal-twig-deluxe)[ Packagist](https://packagist.org/packages/alan-221b/twig-deluxe)[ Docs](https://github.com/Alan-221b/drupal-twig-deluxe)[ RSS](/packages/alan-221b-twig-deluxe/feed)WikiDiscussions main Synced 3mo ago

READMEChangelogDependencies (1)Versions (2)Used By (0)

Twig Deluxe
===========

[](#twig-deluxe)

Scoped CSS and JavaScript for Drupal Twig templates, inspired by Vue and Svelte single-file components.

Overview
--------

[](#overview)

Twig Deluxe brings component-scoped styles and scripts to Drupal theming. Write `` and `` tags inside `{% scoped %}...{% endscoped %}` blocks directly in your `.html.twig` files. At build time, the module extracts these into individual chunk files and automatically scopes CSS rules to the template using `data-twig-scoped` attributes on root HTML elements.

The result: styles defined in a template only affect that template's markup, with no global leakage. JavaScript is wrapped in Drupal behaviors and loaded on demand, only for pages that render the template.

This is a compile-time tool. The `drush twig_deluxe:compile` command processes your templates and writes chunk files to your theme directory. Your theme's asset pipeline (Vite or similar) then picks them up during the normal build.

Requirements
------------

[](#requirements)

- Drupal 10 or 11
- PHP 8.1+
- A theme using a modern bundler (Vite recommended) capable of glob imports and dynamic `import()` for the JS chunk pipeline

Installation
------------

[](#installation)

```
composer require drupal/twig_deluxe
drush en twig_deluxe
```

Usage
-----

[](#usage)

Place a `{% scoped %}` block anywhere in a `.html.twig` file. The block can contain a `` tag, a `` tag, or both.

### CSS only

[](#css-only)

```

  {{ title }}
  {{ content }}

{% scoped %}

    h2 { color: navy; font-size: 1.5rem; }
    .my-component { padding: 2rem; }

{% endscoped %}
```

At compile time, the `h2` and `.my-component` rules are rewritten to include the template's scope hash, and the `` root element receives a `data-twig-scoped="HASH"` attribute in the rendered HTML.

### CSS and JavaScript

[](#css-and-javascript)

```

  {{ items }}

{% scoped %}

    .carousel { position: relative; overflow: hidden; }

    import Splide from '@splidejs/splide';
    document.addEventListener('alpine:init', () => {
      Alpine.data('carousel', () => ({
        init() { new Splide(this.$el).mount(); }
      }));
    });

{% endscoped %}
```

### Twig variables in scoped blocks

[](#twig-variables-in-scoped-blocks)

Twig variables **cannot** be used inside `{% scoped %}` blocks. This is enforced at compile time. The contents of a scoped block are static — they are extracted before Twig renders the template.

If you need to pass dynamic values to your scoped script, use `data-*` attributes on the DOM element:

```

  {{ content }}

{% scoped %}

    Drupal.behaviors.myComponent = {
      attach(context) {
        const el = context.querySelector('.my-component');
        if (!el) return;
        const color = el.dataset.color;
        // use color...
      }
    };

{% endscoped %}
```

Theme Integration
-----------------

[](#theme-integration)

After running `drush twig_deluxe:compile`, the module writes chunk files into your active theme. The theme must be set up to consume them.

The module auto-creates these directories in your theme:

```
{theme}/twig-deluxe/chunks/css/
{theme}/twig-deluxe/chunks/js/

```

### CSS

[](#css)

Create a file at `{theme}/twig-deluxe/generated.css` with the following content:

```
@import-glob "./chunks/css/*.css";
```

Then import it from your theme's main stylesheet:

```
/* main.css */
@import "../twig-deluxe/generated.css";
```

The `@import-glob` syntax requires a PostCSS plugin such as `postcss-import` combined with `postcss-import-ext-glob`, or Vite's glob import support via a plugin.

### JavaScript

[](#javascript)

In your theme's entry point (`main.ts` or `main.js`), add the following to dynamically load JS chunks for any scoped elements present on the page:

```
const chunks = import.meta.glob('../twig-deluxe/chunks/js/**/*.js');

document.querySelectorAll('[data-twig-scoped]').forEach(async (el) => {
  const hashes = el.getAttribute('data-twig-scoped').split(' ');
  for (const hash of hashes) {
    const path = `../twig-deluxe/chunks/js/${hash}.js`;
    if (chunks[path]) await chunks[path]();
  }
});
```

This pattern uses Vite's `import.meta.glob` to statically analyze all chunk files at build time, then loads only the ones needed for the current page at runtime.

Drush Commands
--------------

[](#drush-commands)

### `drush twig_deluxe:compile`

[](#drush-twig_deluxecompile)

Alias: `tdc`

Scans all enabled modules and the active theme for `.html.twig` files, extracts `{% scoped %}` blocks, and writes CSS and JS chunk files to the active theme's `twig-deluxe/chunks/` directory.

```
drush twig_deluxe:compile
```

Run this command before your theme's asset build step in CI/CD pipelines. The typical order is:

1. `drush twig_deluxe:compile`
2. `npm run build` (or equivalent)

Chunk files are named by hash, so unchanged templates produce identical filenames and content. This makes the output safe to commit to version control.

How It Works
------------

[](#how-it-works)

Each template gets a scope hash derived from its file path (MD5, first 8 characters). This hash is stable across runs as long as the file path doesn't change.

At compile time:

1. The `{% scoped %}` block is parsed out of the template source.
2. CSS rules are rewritten by prepending `[data-twig-scoped~="HASH"]` to every selector. For example, `h2 { color: navy; }` becomes `[data-twig-scoped~="a3f9c1b2"] h2 { color: navy; }`.
3. The rewritten CSS is written to `chunks/css/HASH.css`.
4. JavaScript is written as-is to `chunks/js/HASH.js`.
5. The template's root HTML element (the outermost tag) is modified to include `data-twig-scoped="HASH"` as a Twig attribute.

The `root` pseudo-selector maps to the scoped element itself. `root:hover` becomes `[data-twig-scoped~="HASH"]:hover`, targeting the root element directly rather than a descendant.

Template Inheritance
--------------------

[](#template-inheritance)

When a child template extends a parent using `{% extends %}`, the child inherits the parent's scope hash. CSS defined in the parent's `{% scoped %}` block applies correctly to markup rendered by the child, because the `data-twig-scoped` attribute on the root element carries the parent's hash.

If both parent and child define `{% scoped %}` blocks, the root element will carry both hashes as a space-separated list: `data-twig-scoped="PARENT_HASH CHILD_HASH"`. Both stylesheets apply.

Limitations
-----------

[](#limitations)

- **No Twig variables in scoped blocks.** The contents of `{% scoped %}` are extracted statically, before Twig renders the template. Any `{{ variable }}` inside a scoped block will cause a compile error.
- **CSS is parsed by regex.** Complex or malformed CSS may not scope correctly. Standard rule sets work reliably; at-rules like `@keyframes` and `@media` are handled, but deeply nested or non-standard syntax may produce unexpected output.
- **Requires a bundler.** The chunk import pipeline depends on `import.meta.glob` or an equivalent mechanism. Without a bundler that supports this, JS chunks won't load.
- **The `root` selector prefix** maps to the scoped element itself. `root .child` becomes `[data-twig-scoped~="HASH"] .child`. `root:hover` becomes `[data-twig-scoped~="HASH"]:hover`.
- **One `{% scoped %}` block per template.** Multiple scoped blocks in a single file are not supported.

License
-------

[](#license)

GPL-2.0-or-later. See [LICENSE](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).

###  Health Score

34

—

LowBetter than 75% of packages

Maintenance78

Regular maintenance activity

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity34

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

127d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/48279c7cc086f828185fcd5faffb6b50f699fe04d9d284652e489b16203cd913?d=identicon)[Alan221B](/maintainers/Alan221B)

---

Top Contributors

[![Alan-221b](https://avatars.githubusercontent.com/u/185785831?v=4)](https://github.com/Alan-221b "Alan-221b (3 commits)")

---

Tags

twigdrupalcomponentthemingscoped-css

### Embed Badge

![Health badge](/badges/alan-221b-twig-deluxe/health.svg)

```
[![Health](https://phpackages.com/badges/alan-221b-twig-deluxe/health.svg)](https://phpackages.com/packages/alan-221b-twig-deluxe)
```

###  Alternatives

[nystudio107/twig-bundle-installer

Install, update, and manage Twig template bundles via Composer

361.1k1](/packages/nystudio107-twig-bundle-installer)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
