solidjs/solid
solid-element
Active contributors: Ryan Carniato
solid-element wraps a Solid component as a standards-based Custom Element. It uses component-register for the registration mechanics and Solid's reactive primitives for the inside of the element.
Purpose
Custom Elements give you a globally-named, framework-agnostic component that any HTML page or any other UI framework can mount. solid-element lets you ship Solid-built components in that shape: declare default props, write the body as a Solid render function, and customElement("my-tag", ...) registers it.
The package is small (under 100 lines of source) and intentionally so — component-register does the registration heavy lifting.
Directory layout
packages/solid-element/
├── package.json # version, deps, peerDep on solid-js
├── README.md # full usage docs
├── CHANGELOG.md
├── sample.jsx # working example component
├── tsconfig.json
└── src/
└── index.ts # customElement, withSolid, hot, getCurrentElement, noShadowDOMKey abstractions
| Symbol | Source | Description |
|---|---|---|
customElement(tag, ComponentType) |
packages/solid-element/src/index.ts |
Two-arg overload: register without default props. |
customElement(tag, propsDefinition, ComponentType) |
packages/solid-element/src/index.ts |
Three-arg overload: register with explicit default-prop values. |
withSolid(ComponentType) |
packages/solid-element/src/index.ts |
Mixin that you can compose(register(tag), withSolid)(ComponentType) instead of using customElement directly. |
hot, getCurrentElement, noShadowDOM |
re-exports from component-register |
Hot module reload helper, current-element accessor (for use inside the component body), and shadow-DOM opt-out. |
ComponentType<T> (alias) |
packages/solid-element/src/index.ts |
Re-export of component-register's component type. |
How it works
graph TD
Register["register(tag, defaults)<br>(component-register)"]
WithSolid["withSolid(Component)<br>(packages/solid-element)"]
Element["Class extends HTMLElement"]
Connect["connectedCallback()"]
Root["createRoot(dispose => ...)"]
Props["createProps(rawProps)<br>signal-backed proxy"]
Insert["insert(renderRoot, comp)"]
Disconnect["addReleaseCallback(...)"]
Register -->|wraps| WithSolid
WithSolid --> Element
Element --> Connect
Connect --> Root
Root --> Props
Root --> Insert
Connect --> DisconnectThe flow inside withSolid (packages/solid-element/src/index.ts):
- Per-prop signal allocation.
createProps(raw)walks the keys of the default-prop bag and allocates acreateSignalfor each. The returned object has aget/setpair that wraps the signal accessor and setter, soprops.somePropis a tracked read. - Owner allocation.
createRoot(dispose => ...)allocates a fresh reactive owner.lookupContext(element)walks the DOM up to find an_$owner(the markersolid-js/webadds to host elements that own a Solid tree) and threads it as the parent owner so context propagates across element boundaries. - Property-changed bridge.
element.addPropertyChangedCallback((key, val) => props[key] = val)is what makes the element's reactive props update when an outer page setsel.someProp = .... - Release callback.
element.addReleaseCallback(() => { renderRoot.textContent = ""; dispose(); })clears the element's render root and disposes the Solid owner when the element disconnects. - Mount.
insert(element.renderRoot, comp)(fromsolid-js/web) renders the component's output into the element. The render root is either the element's shadow root (default) or its light children ifnoShadowDOM()was called.
customElement is a thin wrapper that picks between two overloads (with or without explicit prop defaults) and chains register(tag, props)(withSolid(ComponentType)).
Sample component
packages/solid-element/sample.jsx shows the canonical usage:
import { customElement } from 'solid-element';
import { createSignal } from 'solid-js';
customElement('my-counter', () => {
const [count, setCount] = createSignal(0);
return (
<div>
<button onClick={() => setCount(count() - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(count() + 1)}>+</button>
</div>
);
});After this script runs, <my-counter></my-counter> works in any HTML document.
Key source files
| File | Purpose |
|---|---|
packages/solid-element/src/index.ts |
The whole implementation: createProps, lookupContext, withSolid, customElement. |
packages/solid-element/sample.jsx |
A runnable example in JSX form. |
packages/solid-element/README.md |
Full usage docs, including the compose(register, withSolid) form for adding extra mixins. |
Integration points
- Depends on
component-register(declared inpackages/solid-element/package.json).component-registerhandles the actualcustomElements.define, the V1 standards, the property-changed callback infrastructure, and the optional ShadyCSS Polymer polyfill. - Depends on
solid-jsandsolid-js/web(peerDep). UsescreateRoot,createSignalfromsolid-jsandinsertfromsolid-js/web. - Built with
tscdirectly (packages/solid-element/package.jsonbuildscript), not Rollup. The output goes todist/. - Only
pnpm run buildis wired up — there are no in-package tests beyond whatpackages/test-integration/test-imports.mjscovers indirectly.
Entry points for modification
- Adding a new shadow-mode option: the shadow vs light-DOM decision happens inside
component-register(addReleaseCallback,renderRoot). New modes would land there first. - Hooking a new lifecycle:
withSolidalready wires connect (createRoot) and disconnect (addReleaseCallback). To hook adoption or attribute changes, add a Custom Element callback to theregister(tag, props)chain viacompose(register, ...mixins, withSolid). - Improving context propagation:
lookupContext(element)is the algorithm that walks parents to find an_$owner. Edge cases around assigned slots, slot retargeting, and shadow boundaries are intentionally tricky — review changes againstpackages/solid/web/test/(which exercises the host marker behaviour from thesolid-js/webside).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.