Light-Rail — Functional Web Components for Composable UI

A pattern for building composable web component UIs without tag registry collisions. One shadow DOM boundary per feature, functional components everywhere inside.

The Problem

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.

The Light-Rail Approach

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:

  1. One registration per feature. Feature-level tag names are unique by nature (teams own their feature names, not their design system internals). The collision surface drops to near zero.
  2. Internal components become light DOM. Without a shadow DOM wrapper, internal markup participates in the feature's single document tree. Styling is simpler; slotting and composition work without workarounds.
  3. One Tailwind injection point. Because there is only one shadow DOM, styles from Tailwind only need to be injected once — at the feature root. Internal components inherit without duplication.

Think of functional components as DOM generators: they receive props, they return nodes, they have no registry footprint.

Framework Scope

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.

Connection to the YAML Platform

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.

Proven in Practice

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.

Future Outlook — Scoped Registries

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.