A pattern for building composable web component UIs without tag registry collisions. One shadow DOM boundary per feature, functional components everywhere inside.
Every custom element must be registered in the browser's component registry under a unique tag name. Register the same tag twice and the second attempt throws. In isolation this is rarely an issue — a team owns their tags and there are no conflicts. In a composable UI, it becomes a real problem.
When multiple teams deliver features independently, using different versions of the same design system, they will often depend on the same base components — a button, an input, a card. Each team's bundle tries to register those shared tags. The second feature to load loses. A component that passes all tests in isolation silently breaks in composition.
The collision is version-sensitive: ds-button@2.1 and ds-button@2.3 cannot coexist because the browser sees one tag, not two versions.
Only the top-level feature component registers a tag. Everything below it is a functional component — a plain function that returns DOM — with no tag registration and no shadow DOM of its own.
Three consequences follow directly:
Think of functional components as DOM generators: they receive props, they return nodes, they have no registry footprint.
This pattern applies specifically to Lit and Stencil. Both frameworks build on the Custom Elements API, which is where the registry collision originates.
Stencil has formal support for functional components — they are a first-class feature of the framework, documented and supported by the toolchain.
Lit does not have formal functional components, but the same outcome is achievable: a plain function that takes a LitElement context and returns a TemplateResult composes naturally inside a parent element's render() method. No customElements.define() call, no shadow root, full Lit template syntax.
React, Vue, Svelte, and Solid components do not use the browser registry — their "components" are framework-internal abstractions. The collision problem does not exist for them and this pattern has no equivalent application there.
The Lit and Stencil exporters in the YAML-driven UI platform use Light-Rail by design. A YAML spec defines a feature — one root component with state, elements, and actions. The exporters compile that spec into exactly one registered custom element at the feature boundary, with all internal elements rendered as functional components beneath it.
This is not incidental. The export contract — receive a YAML spec, return a deployable dist — maps cleanly to the pattern: one entry point, no internal tag pollution, styles scoped to the root. Light-Rail is what makes the export output composable by default.
The App Suite (autos, funnel, sudoku, tinker) includes Lit and Stencil implementations that follow the Light-Rail pattern throughout. These implementations predate the YAML platform and established the pattern empirically — the exporters later automated what was being built by hand.
The underlying collision problem may eventually be solved at the platform level. The Scoped Custom Element Registries proposal would allow each shadow root to maintain its own isolated registry, so ds-button@2.1 and ds-button@2.3 could coexist without conflict. A few browsers have experimental implementations behind flags as of 2026, but broad, unflagged support is not yet available.
When scoped registries land across the board, the core motivation for Light-Rail weakens — teams could ship fully encapsulated components without worrying about global tag collisions. The pattern may still have value even then: the single shadow DOM boundary, light DOM internals, and single Tailwind injection point are useful properties in their own right, independent of the registry problem. Whether to keep the pattern or abandon it will be a judgment call based on the specific composition model of a given project.