Summary

The JSX Theme Engine can reach a stable 2.0.0 for Drupal 11.3+ and 12 in five phases over about 9–12 months. Most of that time goes to one dependency: a core change that lets a non-Twig engine render module templates and SDC components.

Where the project stands (September 2026):

  • Dormant since April 2024. 41 commits, a single 1.0.x-dev branch, no tagged release, no security advisory coverage, 47 issues (about 22 still open). The project is marked experimental and minimally maintained. Maintainers are bnjmnm, effulgentsia and hooroomoo, supported by Acquia.
  • Broken on 11.3+. It ships a legacy jsx.engine file, and 11.3 replaced that contract with tagged services. Canvas's sibling engine hit exactly this failure: "Service 'semi_coupled' not found" (canvas#3556101).
  • Its best ideas are already in Canvas. Canvas's semi-coupled theme engine "lifted relevant parts" of JSX (#3446434: Document "Semi-Coupled Theme Engine" and "Redux-integrated field widgets" components). Canvas is now removing that engine in favor of custom HTML tags inside Twig (canvas#3555062), which leaves JSX as the only effort aimed at a full non-Twig engine.

What stable means for this roadmap: a tagged service implementing ThemeEngineInterface; SDC components authored in JSX; server-rendered HTML with client hydration (no blank page before JS runs); full coverage of Drupal core's templates; opt-in to the security advisory policy; and tested support for Drupal 11.3, 11.4, 11.5 and 12.x.

The critical path is #3525011: Allow complete replacement of the default theme engine (twig) in Drupal core, "Allow complete replacement of the default theme engine (twig)". Until it lands, core forces Twig for module templates and SDC. The roadmap treats it as a hard blocker for SDC-to-JSX, drives it from inside core, and builds everything else in parallel.

Review of the current JSX project

The existing design is a sound proof of concept: Drupal emits custom elements with serialized props, and the browser maps them onto JSX components. It is not yet a theme engine in the production sense, because nothing renders without JavaScript.

Review basis: the Drupal.org project page, the 47-issue queue and discussion on merged merge requests. File-level details should be confirmed against the 1.0.x branch before estimates are finalized.

How it works today

Render array -> ThemeManager -> jsx.engine (legacy .engine file) -> <drupal-*> custom elements + serialized props -> Browser: hyperscriptify() -> React / Preact / Solid components mount

  1. jsx.engine swaps each template for a custom element named after the hook, e.g. form-element becomes <drupal-form-element>, carrying the template variables as props (#3397049: Integrate with AJAX).
  2. Each component's prop types are declared with PropTypes. *.template-info.json files generated from them tell the engine which props a component expects (#3396749: Automate the generation of *.template-info.json files from the components' PropTypes declarations, #3396706: Add a way to discover possible props).
  3. In the browser, index-*.js entry points call hyperscriptify(), which walks the DOM and builds a component tree for the chosen framework.
  4. Preprocess logic moved from PHP into JS (#3392970: Move preprocess functions to JS code), and CSS moved from attach_library() to CSS-in-JS (#3393003: Move CSS attachment from {{ attach_library() }} to a CSS-in-JS solution).
  5. The Umami JSX demo theme ships React, Preact and Solid builds, plus a React debug tool showing template suggestions.

Strengths to keep

  • Custom-element boundary. It lets JSX and Twig output coexist on one page, so the engine can be adopted one template at a time.
  • Framework-agnostic mapping. Hyperscriptify targets React, Preact and Solid alike.
  • Real Drupal UIs exercised. Layout Builder, AJAX, BigPipe and language switchers were all tested against it, and AJAX responses are re-hyperscriptified (#3397049: Integrate with AJAX).
  • Props metadata anticipates SDC's *.component.yml schemas.
  • Lineage. Its approach was proven again in Canvas's semi-coupled engine.

Findings

# Finding Severity Evidence
1 Legacy jsx.engine contract; fails on 11.3+ exactly as Canvas's engine did Blocker CR 3547356, canvas#3556101
2 Client-only rendering: no HTML before JS runs, which hurts SEO, accessibility, no-JS users and Core Web Vitals Blocker for stable SSR only via an external Node app; #3393032: Add a server-side rendered (SSR or RSC) build for Umami JSX closed as outdated
3 Variables serialized to the client, which may expose data Twig would never print High (security) RSC was proposed partly to keep variables off the client
4 Prop type coercion unfinished; themes need Drupal internals knowledge High #3398446: Add type coercion logic to jsx.engine so that themes can declare prop types based on their needs, not on knowledge of Drupal internals Active
5 SDC integration is an initial implementation; SDC's single-directory rule conflicts with the app/ directory layout High #3396976: Integrate with SDC (single directory components)
6 Partial AJAX: replaceWith and off-canvas submit buttons incomplete; nested React roots per AJAX response Medium #3397049: Integrate with AJAX, #3432283: Layout Builder "Add Section" does not have submit button
7 Layout Builder gaps: Remove Section, breadcrumbs, contextual links Medium #3432278: Layout Builder "Remove Section" does not work in Umami JSX, #3432903: In Layout Builder the breadcrumbs are not showing up., #3432277: Get contextual links working in Layout Builder + Umami JSX
8 Template coverage limited to Umami; full-page conversion of 22 templates unfinished Medium #3396754: Convert all Twig templates to JSX components for a single URL
9 Preprocess and form_alter must be reimplemented in JS; no PHP escape hatch Medium #3392970: Move preprocess functions to JS code, #3432503: Implement form_alter-esque functionality
10 No automated test suite; helper libraries not published to npm Medium #3432275: [META] Tests, #3393037: Add tests for Umami JSX, #3427734: Publish helper libraries to npm or jsr
11 Build tooling mid-migration to Vite; TypeScript types missing Low #3439774: Refactor jsx_umami to use Vite , #3396747: Add TypeScript declarations to the JSX component props
12 CSS-in-JS bypasses Drupal libraries, aggregation and libraries-override Low–medium #3393003: Move CSS attachment from {{ attach_library() }} to a CSS-in-JS solution

The existing npm toolkit (drupal-jsx org)

The rendering approach above is not just a design on paper: it already exists as eight small, single-purpose packages under the drupal-jsx GitHub organization, all type: module, MIT-licensed, last touched December 2023 — the same dormancy window as the main project. Phase 1 should revive and publish these rather than rebuild them from scratch.

Package Role Depends on
hyperscriptify Foundation. Converts a DOM tree into a hyperscript/JSX element tree, so a server-rendered <drupal-*> custom element becomes a real component. Also exports propsifyFactory() for attribute-to-prop conversion. none
propsify Turns HTML attributes into component props: kebab-case to camelCase, JSON-parses stringified values, maps slots into nested prop paths. A strict build for React, a looser petite build for Preact/Solid. hyperscriptify, just-* helper libs
serialize-prop-types Runs a component through a Web Worker to extract and serialize its PropTypes — the automation behind the *.template-info.json files (#3396749: Automate the generation of *.template-info.json files from the components' PropTypes declarations). prop-types (peer)
drupal-utils Framework-agnostic main(): finds hyperscriptify-tagged elements on the page, converts attributes to props, mounts components. hyperscriptify
drupal-utils-react React-specific wrapper: calls drupal-utils' main() with createElement/createRoot, and patches Drupal.attachBehaviors to run after React's render tick so Drupal's own JS doesn't race React. drupal-utils, propsify, React 18 (peer)
drupal-utils-dev Dev-time helpers built on serialize-prop-types. drupal-utils, serialize-prop-types
vite-plugin-drupal Vite plugin (Bun-powered): watches .jsx files, regenerates .template-info.json on change, invalidates Drupal's theme registry cache over a WebSocket — the live-reload story for theming with JSX. drupal-utils, drupal-utils-dev, Vite 5 (peer)
starter-react The project scaffold: a starter Drupal theme wired to the engine, React and Vite, with a "JSX Devel" module for inspecting available props. README states it is experimental and not for production. the packages above

What this changes in the plan: @drupal-jsx/runtime (target architecture, below) is a revival of hyperscriptify + drupal-utils + drupal-utils-react, generalized to Preact as the default per the framework-scope decision, and @drupal-jsx/vite is a revival of vite-plugin-drupal + drupal-utils-dev. serialize-prop-types maps directly onto @drupal-jsx/types's job, but should be re-pointed at SDC's *.component.yml schemas instead of hand-authored PropTypes, per the SDC bridge in Phase 2. All of it is currently pinned to React 18 and Vite 5 peer dependencies, which need bumping as part of the 11.3+/12 compatibility pass.

Gap analysis: Drupal 11.3+ and Drupal 12

The project was written against Drupal 10's theme layer, and nearly every integration point it touches has moved since. Target core_version_requirement: ^11.3 || ^12 and drop Drupal 10, which reaches end of life on December 9, 2026.

Release timeline that sets the support matrix

Release Date Implication for JSX
Drupal 11.3.0 December 2025 Engines become tagged services; .engine deprecated
Drupal 11.4.0 Week of June 29, 2026 Current 11.x minor; primary development target now
Drupal 11.5.0 / 12.0 beta 1 Week of September 14, 2026 12.0 APIs are frozen enough to test against
Drupal 12.0.0 / 11.5.0 Week of December 7, 2026 11.3 security support ends; first stable pair to certify
Drupal 10 EOL December 9, 2026 No reason to support it

Source: core release schedule.

What must change

Area JSX today Required on 11.3+ / 12
Engine registration jsx.engine file with jsx_*() functions Service JsxThemeEngine implements ThemeEngineInterface, tagged { name: theme_engine, engine_name: jsx }, provided by the jsx module
Template discovery jsx_theme() + template-info JSON theme() returning drupal_find_theme_templates($existing, '.jsx', $path) or a custom scan that pairs .jsx files with metadata
Rendering jsx_render_template() emitting custom elements renderTemplate() returning MarkupInterface; see architecture
Engine preprocess Allowed pre-11.3 Removed in 11.3; move to #[Hook('preprocess_HOOK')] classes in the module or theme
Hooks Procedural .module/.theme functions OOP #[Hook] attribute classes, which themes support too
Theme config engine: jsx in the theme Unchanged, but sub-themes inherit the root base theme's engine, so ship a jsx_base base theme
Theme debug Custom React debug tool Also emit THEME HOOK / suggestion comments server-side like TwigThemeEngine
Module templates Rendered by Twig Still forced to Twig: ThemeManager::render() sets $theme_engine = 'twig' for type: module
SDC Initial bridge from 2023 ComponentElement compiles to a Twig {% embed %} via inline_template, so SDC is Twig-only by construction
Canvas Not considered Canvas code components already use Preact and JSX; JSX engine components should be usable as Canvas components

Carryover from the 2023 design that no longer fits

  • Semi-coupled mapping JSON. Canvas is removing the same pattern because "few understand" it (canvas#3555062). Replace hand-maintained template-info JSON with SDC's *.component.yml prop schemas plus generated TypeScript types.
  • CSS-in-JS. Return to Drupal libraries (or SDC's automatic per-component CSS/JS) so aggregation, libraries-override and asset bubbling work.
  • Framework trio. Supporting React, Preact and Solid triples the test matrix. Standardize on Preact with preact/compat to align with Canvas, and treat React and Solid as community adapters.

The core blocker: Twig as default infrastructure

#3525011: Allow complete replacement of the default theme engine (twig) in Drupal core "Allow complete replacement of the default theme engine (twig) in Drupal core" is the gate for SDC-to-JSX, and it needs a broader scope and a reroll before it can land. It was opened May 16, 2025 by d34dman, is still Active, and its summary predates 11.3: it cites twig_render_template, which no longer exists.

Three couplings, three different fixes

Reading core main (12.0-dev, September 2026) shows that "Twig as default" is really three separate couplings. Only one of them is solvable in contrib today.

Coupling Core code Can contrib work around it? Needed in core
1. Module templates fall back to Twig ThemeManager::render(): if ($info['type'] != 'module') {...} else { $theme_engine = 'twig'; } Yes, partly. A template the JSX engine registers through theme() is stamped type: theme_engine, so it renders with JSX. Replicating every core template therefore works today; Twig only catches templates JSX has not ported. An engine-owned fallback policy, e.g. exclusive: true, so missing templates fail loudly instead of silently rendering Twig
2. SDC templates must be .twig ComponentPluginManager::alterDefinition() calls findAsset($dir, $machine_name, 'twig') No. A card.jsx next to card.component.yml is never discovered Engine-aware template discovery; the definition records which engine owns the component
3. SDC rendering is Twig by construction ComponentElement builds {% embed 'provider:id' %} and renders it as #type => 'inline_template'; ComponentLoader is a Twig loader Only by replacing the element class via hook_element_info_alter(), which is fragile and breaks Twig-to-component include() Delegate component rendering to the owning engine through an optional engine interface

inline_template itself, and Views rewrites, are Twig-language strings by definition. They should stay Twig and are not blockers.

Proposed core change set

File these as child issues of #3525011: Allow complete replacement of the default theme engine (twig) in Drupal core so each can land in its own minor release:

  1. Reroll #3525011: Allow complete replacement of the default theme engine (twig) in Drupal core on the 11.3 service architecture. Replace the hard-coded 'twig' with an engine-declared fallback. Add an optional exclusive flag that makes the active engine own module templates, throwing when a template is missing.
  2. New issue: engine-agnostic SDC template discovery. ComponentPluginManager asks the tagged engines which extensions they own (.twig, .jsx, .tsx) and stores engine on the component definition.
  3. New issue: ComponentRenderingEngineInterface. An optional interface, e.g. renderComponent(ComponentPlugin $component, array $props, array $slots): array|MarkupInterface. ComponentElement delegates to the owning engine; Twig keeps today's inline_template path as its implementation.
  4. New issue: cross-engine composition. Twig's include('provider:card') must render a JSX-owned component (via a render array placeholder), and JSX must be able to render Twig-owned components as slots. This keeps mixed sites working during migration.
  5. Test engine upgrade. Extend core's nyan_cat test engine to own an SDC component and run in exclusive mode, so the new APIs stay covered in core CI.

Timing

New APIs land only in minor releases. 12.0 is in beta with release candidate 1 due the week of November 9, 2026, so the earliest realistic core target is a Drupal 12.x minor in 2027, with a backport to 11.x if the change is API-additive. The roadmap therefore ships a contrib-side SDC bridge first (Phase 2) and retires it once core lands (Phase 4).

Target architecture

Stable JSX renders HTML on the server and hydrates in the browser only where a component is interactive. SDC is the unit of authoring, and Drupal's render pipeline (caching, attachments, BigPipe) stays in charge. ThemeManager -> JsxThemeEngine::renderTemplate() -> PropNormalizer (Attribute, Markup, Url) -> Slots: nested render arrays via Renderer -> SSR renderer (Node sidecar or client-only) -> HTML + island markers -> Render cache / BigPipe -> Browser: hydrate interactive islands only

Components of the system

Package Type Responsibility
jsx Drupal module JsxThemeEngine service; prop normalizer; SSR client; SDC bridge; server-side theme debug comments; Drush commands
jsx_base Base theme (engine: jsx) JSX ports of all core templates, so sub-themes inherit a complete, Twig-free engine
jsx_umami Demo theme Rebuilt Umami JSX as the reference site and test fixture
@drupal-jsx/runtime npm Island hydration, Drupal.behaviors bridge, AJAX command handling (one root per island, not per AJAX response). Revives hyperscriptify + drupal-utils + drupal-utils-react
@drupal-jsx/vite npm Vite plugin: builds SSR and client bundles, emits Drupal library definitions. Revives vite-plugin-drupal + drupal-utils-dev
@drupal-jsx/types npm Generates TypeScript prop types from *.component.yml and theme-hook variables. Revives serialize-prop-types, re-pointed at SDC schemas instead of hand-authored PropTypes

Rendering pipeline decisions

  1. Props are normalized in PHP, not JS. Attribute becomes a plain object, MarkupInterface a pre-escaped HTML string, TranslatableMarkup a string, Url an href. This resolves #3398446: Add type coercion logic to jsx.engine so that themes can declare prop types based on their needs, not on knowledge of Drupal internals and replaces JS-side preprocess.
  2. Render arrays become slots. Nested render arrays render through RendererInterface first, so #cache and #attached bubble exactly as with Twig. They arrive in JSX as a <Slot html={...} />.
  3. SSR renderer is pluggable.
    • Node sidecar (default for production): a long-lived Node/Bun process on a local socket renders the component; it is called only on render-cache misses.
    • Client-only (development fallback): today's custom-element behavior, for hosts without a sidecar.
    • Compile-to-PHP (research track): compile a static JSX subset to PHP at build time, removing the runtime dependency entirely.
  4. Islands, not whole-page hydration. Only components that export hydrate = true (or use a 'use client' directive) ship JS and serialized props. Server-only components never send variables to the browser, which closes the data-exposure finding.
  5. Escaping. JSX autoescapes text; the only raw-HTML path is <Slot>, which accepts MarkupInterface output only.
  6. Assets through Drupal libraries. The Vite plugin writes per-component libraries, so aggregation, libraries-override and SDC's automatic CSS/JS keep working.

SDC in JSX

themes/custom/mytheme/components/card/ card.component.yml # props + slots schema (unchanged SDC format) card.tsx # default export: the component card.css # auto-attached, as with Twig SDC card.stories.tsx # optional import type { CardProps } from './card.types'; // generated from card.component.yml export default function Card({ heading, image, children }: CardProps) { return ( <article className="card"> {image} <h3>{heading}</h3> {children /* SDC slot */} </article> ); }

With core's engine-aware discovery (blocker items 2–3), card.tsx is discovered like card.twig. Placing it with #type => 'component', Layout Builder or Canvas then works unchanged. Until then, the jsx module's bridge registers these components under its own element type.

Roadmap

The roadmap has five phases that end in a 2.0.0 stable release, targeted for mid-2027. The core track (Phase C) runs in parallel from day one because it gates the stable release.

Phase Window Release Exit criteria
0. Collaboration and triage Oct 2026 none Roadmap agreed with the current maintainers; META published; queue triaged together
1. Compatibility Oct – Dec 2026 2.0.0-alpha1 (week of Dec 7, with Drupal 12.0) Engine runs as a tagged service on 11.3–12.0 with CI green
2. SSR, islands and SDC bridge Jan – Mar 2027 2.0.0-beta1 Server-rendered HTML; SDC components in .tsx via the contrib bridge
3. Full template coverage Mar – Jun 2027 2.0.0-rc1 jsx_base renders every front-end core template in exclusive mode
C. Core track (#3525011: Allow complete replacement of the default theme engine (twig) in Drupal core + children) Oct 2026 – mid 2027 Drupal 12.x minor Engine-agnostic SDC discovery and rendering committed to core
4. Stable After C lands 2.0.0 Bridge swapped for core API; security advisory coverage opted in

The new work goes on a fresh 2.0.x branch. 1.0.x never had a release, so users see a clean line between the 2023 experiment and the supported engine.

Phase 0 — Collaboration and triage (October 2026)

Phase 1 — Compatibility (October – December 2026)

  • Replace jsx.engine with JsxThemeEngine implements ThemeEngineInterface, tagged engine_name: jsx.
  • Move engine-level preprocess into #[Hook] classes; convert .module/.theme procedural hooks to OOP.
  • Set core_version_requirement: ^11.3 || ^12 and drop Drupal 10.
  • Adopt the Drupal Association GitLab CI template; test on 11.3, 11.4, 11.5 and 12.0 (beta, then RC, then release), with next major runs.
  • Finish the Vite migration (#3439774: Refactor jsx_umami to use Vite ) by reviving vite-plugin-drupal; standardize on Preact with preact/compat.
  • Audit the eight dormant drupal-jsx npm packages (hyperscriptify, propsify, serialize-prop-types, drupal-utils, drupal-utils-react, drupal-utils-dev, vite-plugin-drupal, starter-react): bump their React 18 / Vite 5 peer dependencies, decide on a monorepo, and publish the revived @drupal-jsx/runtime and @drupal-jsx/vite to npm (#3427734: Publish helper libraries to npm or jsr).
  • Rebuild jsx_umami on the new engine as the smoke test.

Phase 2 — SSR, islands and the SDC bridge (January – March 2027)

  • Build the PHP prop normalizer and slot rendering through RendererInterface.
  • Build the Node sidecar SSR renderer and a DDEV add-on, plus a client-only fallback mode.
  • Add island hydration with opt-in hydrate, and stop serializing props for server-only components.
  • Rework AJAX to one root per island; fix replaceWith and off-canvas submit buttons.
  • Ship the contrib SDC bridge: discover *.tsx beside *.component.yml, validate props against the schema, render via the engine.
  • Build @drupal-jsx/types to generate TypeScript prop types from component.yml.
  • Emit server-side theme debug comments matching Twig's format.

Phase 3 — Full template coverage (March – June 2027)

Core main contains 263 non-test *.html.twig templates across its modules; system alone has 72. Admin-only templates (Views UI, Field UI, Help, Navigation) can stay on the admin theme's Twig, which leaves roughly half for jsx_base.

  • Generate a coverage report: every theme hook versus the templates jsx_base provides.
  • Port front-end templates in priority order: page/region/block, node/field/entity, forms and form elements, Views output, media/image/responsive image, menus, comments, user.
  • Add a Drush generator, drush jsx:generate-theme, that clones jsx_base the way starterkit_theme does for Twig.
  • Port the relevant core theme tests plus Layout Builder, AJAX and BigPipe tests (#3435995: Port Layout Builder tests to umami JSX , #3432275: [META] Tests).
  • Run accessibility (axe) and performance budgets (TTFB and hydration cost) against Olivero-on-Twig as the baseline.

Phase C — Core track (parallel, October 2026 – mid 2027)

  • Reroll #3525011: Allow complete replacement of the default theme engine (twig) in Drupal core against the 11.3 service architecture, adding an engine-declared fallback and an exclusive mode.
  • File and drive the child issues: engine-agnostic SDC discovery; ComponentRenderingEngineInterface; cross-engine include(); an SDC-capable nyan_cat test engine.
  • Present the plan at a DrupalCon or camp session and at a core committer office hour to get framework-manager sign-off early.
  • Coordinate with Canvas maintainers so JSX-owned SDC components appear in Canvas's component library.

Phase 4 — Stable

2.0.0 ships when these gates are all met:

  • The core SDC APIs from Phase C are in a released Drupal minor, and the contrib bridge becomes a shim for older 11.x versions.
  • No open critical or major bugs; test coverage includes every ported template.
  • Security advisory coverage opted in, which requires a stable release and a vetted maintainer.
  • Documentation covers the theming guide, SDC-in-JSX guide, migrating a Twig theme, and SSR hosting.

Risks and open questions

The largest risk is that the core track moves slower than the contrib work. The mitigation is a contrib bridge that is good enough to ship a beta without core changes.

Risk Likelihood Impact Mitigation
#3525011: Allow complete replacement of the default theme engine (twig) in Drupal core and its SDC children miss the 2027 minors Medium Stable slips Contrib SDC bridge in Phase 2; get framework-manager buy-in in Phase C before writing patches
Core prefers Canvas's direction (custom tags inside Twig) over non-Twig engines Medium Core APIs rejected Frame the APIs as engine-neutral, proven by nyan_cat, and useful to any engine rather than JSX-specific
SSR sidecar is a hosting burden (Pantheon, Acquia, shared hosts) High Adoption Client-only fallback; research compile-to-PHP; document supported hosts
Porting ~130 front-end templates is slow Medium RC slips Coverage report; generator scaffolding; recruit front-end contributors per template group
Contrib modules ship Twig templates and Twig extensions High Mixed-engine sites Non-exclusive mode stays the default; per-module JSX template packs are community work
Maintainer time is limited across the team Low–medium Slower reviews Spread review and release duties across co-maintainers so no one person is a bottleneck
Security coverage review Low Delays stable Start the security opt-in process during the RC

Open questions:

  • Framework scope: Preact-only for stable, with React and Solid as community adapters, or keep all three first-class?
  • Branch: start 2.0.x, or continue 1.0.x because it was never released?
  • SSR default: is a Node sidecar acceptable as the production default, or must client-only be the default?
  • Canvas: should JSX SDC components be registered as Canvas "code components", or only as SDC components that Canvas already understands?

Sources

AI disclosure

The author used Claude Code throughout authoring this plan, with human review and decisions made independently, per Drupal.org's AI contribution policy.

CommentFileSizeAuthor
JSX-Theme-Engine.png41.63 KBmatthand

Comments

matthand created an issue. See original summary.

matthand’s picture

Issue summary: View changes
matthand’s picture