JSX Custom Element Types
When jsxImportSource points at @ecopages/jsx, custom elements should augment the runtime module instead of the global JSX namespace.
Augmenting The Runtime
import type { JsxCustomElementAttributes } from '@ecopages/jsx';
type UserCardProps = {
name: string;
isAdmin: boolean;
};
declare module '@ecopages/jsx/jsx-runtime' {
interface JsxCustomIntrinsicElements {
'user-card': JsxCustomElementAttributes<HTMLElement, UserCardProps>;
}
}JsxCustomElementAttributes<Host, Props> types the element in two layers:
Propsholds the public, unprefixed JSX props. It keeps its own required and optional fields, so required props stay required.- the host type backs explicit
prop:*bindings, which are typed from the element class properties.
Binding Defaults For Custom Elements
Unprefixed names on custom elements default to property bindings. A fixed set of names keeps attribute semantics because they describe obvious HTML markup:
class, dir, hidden, id, lang, part, role, slot, style, tabindex, title — plus every data-* and aria-* name.
This split is deliberate: markup names serialize so SSR output stays meaningful, while everything else passes real values as properties. Use attr:* to force serialization of any other name, and prop:* to override the attribute defaults explicitly.
Worked Example
<user-grid id="people" class="panel" items={rows} selection={currentRow} attr:status="ready" prop:api={gridApi} />import type { JsxCustomElementAttributes } from '@ecopages/jsx';
type UserGridRow = {
id: string;
};
type UserGridProps = {
items: UserGridRow[];
selection?: UserGridRow;
};
class UserGridElement extends HTMLElement {
api?: UserGridApi;
}
type UserGridApi = {
focusRow(id: string): void;
};
declare module '@ecopages/jsx/jsx-runtime' {
interface JsxCustomIntrinsicElements {
'user-grid': JsxCustomElementAttributes<UserGridElement, UserGridProps>;
}
}
const rows: UserGridRow[] = [{ id: '1' }];
const currentRow = rows[0];
const gridApi: UserGridApi = {
focusRow: (_id) => undefined,
};
<user-grid items={rows} selection={currentRow} prop:api={gridApi} />;In that example:
itemsandselectionare typed fromUserGridProps(defaults to property binding on the instance)idandclassare typed and serialized as attributesattr:statusserializes to markup even thoughstatusis not an attribute defaultprop:apiis typed fromUserGridElement.apiand never serializes to HTML
Design Note: Custom Elements Versus Radiant Hosts
JSX itself has two escape seams for custom elements, not Radiant-specific branches:
- Generic SSR contract. Any registered tag containing
-whose element implementsrenderHostToString(options?)can be serialized directly by@ecopages/jsx/server. This contract is for third-party custom elements that opt into JSX SSR directly. - Framework render hook.
RadiantElementhosts do not expose a durable instance method namedrenderHostToString(). Radiant installs a server custom-element render hook and serializes hosts through its own pipeline.
See JSX SSR for the serialization rules and scope helpers, and Component SSR for the Radiant adapter path.