Migration from Traditional Themes
apps/docs/src/content/docs/drupal/migration Click to copy apps/docs/src/content/docs/drupal/migration Migrating from a traditional Drupal theme (Bootstrap, Bartik-based, jQuery-heavy) to HELiX web components does not require a full rewrite. The most effective approach runs HELiX alongside your existing theme, migrates one content pattern at a time, and validates each change before proceeding.
What Changes in the Migration
Section titled “What Changes in the Migration”Traditional Drupal theming:
- Global CSS stylesheets applied via selector specificity
- jQuery for interactive behaviors (accordions, dropdowns, tabs)
- Twig templates producing flat HTML for CSS targeting
Drupal.behaviorswrapping jQuery plugins- SCSS variables for theme customization
HELiX architecture:
- Shadow DOM encapsulation per component
- Native Custom Elements lifecycle (no jQuery dependency)
- Twig templates producing web component tags with slotted content
- CSS custom properties (
--hx-*) for theming Drupal.behaviorsusingonce()for event attachment
Gradual Migration Strategy
Section titled “Gradual Migration Strategy”Migrate in three phases. Each phase can be deployed independently.
Phase 1: Load HELiX alongside the existing theme
Section titled “Phase 1: Load HELiX alongside the existing theme”Add HELiX as a library dependency without removing anything. Both Bootstrap (or your existing CSS framework) and HELiX coexist.
helix-core: version: 1.1.2 css: theme: https://cdn.jsdelivr.net/npm/@helixui/tokens@3.9.0/dist/tokens.css: type: external js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js: type: external preprocess: false attributes: { type: module, crossorigin: anonymous }At this point, no existing HTML is changed. Adding the library causes no visual or functional changes.
Phase 2: Migrate one pattern at a time
Section titled “Phase 2: Migrate one pattern at a time”Pick the simplest, most self-contained pattern first. Cards are a good starting point because they are read-only (no form interactions) and visually isolated.
Before — Bootstrap card:
{# templates/node--article--teaser.html.twig — Bootstrap #}<div class="card"> {% if content.field_image %} <div class="card-img-top">{{ content.field_image }}</div> {% endif %} <div class="card-body"> <h5 class="card-title">{{ node.label }}</h5> <p class="card-text">{{ content.body }}</p> <a href="{{ url }}" class="btn btn-primary">Read More</a> </div></div>After — HELiX:
{# templates/node--article--teaser.html.twig — HELiX #}{{ attach_library('mytheme/helix-card') }}{{ attach_library('mytheme/helix-button') }}
<hx-card variant="default"> {% if content.field_image %} <div slot="image">{{ content.field_image }}</div> {% endif %} <h3 slot="heading">{{ node.label }}</h3> {{ content.body }} <div slot="actions"> <hx-button hx-href="{{ url }}" variant="primary">Read More</hx-button> </div></hx-card>Deploy and verify. No other templates change.
Phase 3: Remove legacy CSS and jQuery dependencies
Section titled “Phase 3: Remove legacy CSS and jQuery dependencies”Once all patterns have been migrated, audit which Bootstrap/jQuery files are still referenced. Remove them from your libraries YAML and theme info file. Test regression.
Coexistence with Existing Themes
Section titled “Coexistence with Existing Themes”Namespace isolation
Section titled “Namespace isolation”HELiX component styles live inside Shadow DOM. Bootstrap’s global CSS selectors cannot reach inside hx-card’s shadow root — there is no conflict between Bootstrap’s .card styles and HELiX’s card component.
The only potential conflict is:
- CSS custom property names — HELiX uses
--hx-prefix to avoid collisions with Bootstrap’s variables (Bootstrap 5 uses--bs-prefix). - JavaScript variable names — the shipped
@helixui/drupal-behaviorsuseshx--prefixedonce()IDs (e.g.hx-dialog,hx-tooltip). Project-local behaviors should pick their own namespace (e.g.mysite:foo) so once-keys never collide with the package’shx-*keys.
Running HELiX on specific page regions only
Section titled “Running HELiX on specific page regions only”During migration, you may want HELiX only in certain page sections:
{# Use HELiX only in the content region, not in header/footer #}{% if page.content %} {{ attach_library('mytheme/helix-card') }} {{ page.content }}{% endif %}Phased template migration
Section titled “Phased template migration”Use Drupal’s template suggestion system to serve both the old and new template simultaneously based on view mode:
node--article--teaser.html.twig ← Bootstrap (existing)node--article--teaser--helix.html.twig ← HELiX (new)Add a template suggestion in preprocess:
function mytheme_preprocess_node(array &$variables): void { if ($variables['view_mode'] === 'teaser') { // Opt specific content types into HELiX on a flag $use_helix = \Drupal::config('mytheme.settings')->get('use_helix_teasers'); if ($use_helix) { $variables['theme_hook_suggestions'][] = 'node__article__teaser__helix'; } }}This allows A/B testing the migration before full cutover.
Replacing Form Elements
Section titled “Replacing Form Elements”Form element migration is higher risk than card migration because it involves server-side validation and form submission. Migrate with a FormElement plugin (see Form Element Plugins) so existing form validation logic continues to work.
Before — standard Drupal textfield
Section titled “Before — standard Drupal textfield”$form['email'] = [ '#type' => 'textfield', '#title' => $this->t('Email Address'), '#required' => TRUE,];After — HELiX text input
Section titled “After — HELiX text input”$form['email'] = [ '#type' => 'helix_text_input', // Custom plugin — same API '#title' => $this->t('Email Address'), '#required' => TRUE, '#attributes' => ['type' => 'email'],];The FormElement plugin maps all standard Form API keys (#title, #required, #default_value, #element_validate) to the HELiX component’s attributes. Your validation handlers, AJAX callbacks, and submit handlers require no changes.
Gradual form migration by form ID
Section titled “Gradual form migration by form ID”Migrate specific forms without touching others:
// Only migrate the contact formfunction my_module_form_contact_message_contact_form_alter( array &$form, FormStateInterface $form_state): void { if (isset($form['field_message'])) { $form['field_message']['widget'][0]['value']['#type'] = 'helix_textarea'; }}Replacing Alerts
Section titled “Replacing Alerts”Before — Bootstrap alert:
<div class="alert alert-danger" role="alert"> {{ message }}</div>After — HELiX alert:
{{ attach_library('mytheme/helix-alert') }}{# hx-alert variants: primary | secondary | success | warning | error | neutral | info. There is no 'danger' variant — destructive messaging uses variant='error'. hx-alert is hidden until `open` is set. #}<hx-alert open variant="error"> {{ message|escape }}</hx-alert>Replacing Accordions and Tabs
Section titled “Replacing Accordions and Tabs”jQuery-dependent accordions require a behavior migration as well as a markup migration.
Before — jQuery accordion:
<div id="accordion-{{ id }}" class="accordion-container"> {% for item in items %} <h3 class="accordion-trigger">{{ item.title }}</h3> <div class="accordion-content">{{ item.body }}</div> {% endfor %}</div>// jQuery behaviorDrupal.behaviors.accordion = { attach(context) { once('jquery-accordion', '.accordion-container', context).forEach((el) => { $(el).accordion({ collapsible: true }); }); },};After — HELiX accordion:
{{ attach_library('mytheme/helix-accordion') }}<hx-accordion> {% for item in items %} {# hx-accordion-item exposes the trigger heading via the `trigger` slot (NOT `heading`); panel content goes in the default slot. `expanded` is the canonical open-state attribute. #} <hx-accordion-item id="accordion-item-{{ loop.index }}"> <span slot="trigger">{{ item.title|escape }}</span> {{ item.body|escape }} </hx-accordion-item> {% endfor %}</hx-accordion>No JavaScript behavior needed — the component handles expand/collapse natively.
CDN-Based Gradual Adoption
Section titled “CDN-Based Gradual Adoption”The CDN approach is the lowest-friction way to introduce HELiX into an existing theme. No npm, no build pipeline.
# Add to an existing theme's libraries YAMLhelix-gradual: version: 1.1.2 css: theme: https://cdn.jsdelivr.net/npm/@helixui/tokens@3.9.0/dist/tokens.css: type: external js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external preprocess: false attributes: { type: module, crossorigin: anonymous }Attach it only on pages you are actively migrating:
{# Only on the page template you are migrating #}{{ attach_library('mytheme/helix-gradual') }}This limits the blast radius — CDN load only on migrated pages, no impact on others.
Migration Risk Register
Section titled “Migration Risk Register”| Migration step | Risk | Mitigation |
|---|---|---|
| Loading HELiX CSS tokens | Medium — --hx-* properties visible globally | Use --hx- prefix — no conflict with --bs- |
| Migrating card templates | Low — no form interaction | Test in staging, compare screenshots |
| Migrating form elements | High — validation and submission | Use FormElement plugin, preserve #type API |
| Removing Bootstrap JS | Medium — existing behaviors may depend on it | Audit all jQuery dependencies before removal |
| Removing Bootstrap CSS | High — global selectors affect many templates | Remove only after all templates migrated |
| Removing jQuery | High — Drupal core still uses jQuery | Do not remove jQuery — only remove your custom jQuery behaviors |
Rollback
Section titled “Rollback”Each template migration is independently revertable. Keep the original template in version control. If a migration causes issues, restore the original template and clear the Drupal theme cache:
drush crBecause HELiX components are additive (new template files, new libraries, new element plugins), rollback never requires destructive changes to existing code.
Related
Section titled “Related”- Form Element Plugins — Wrapping HELiX form components in the Form API
- SDC Architecture — Composition patterns for content types
- Theming — Migrating from SCSS variables to CSS custom properties