JSX Custom Element Types
When a custom element class is available, use the class directly in JSX and declare its public props with the JSX_PROPS symbol.
Using The Class
import type { JSX_PROPS } from '@ecopages/jsx';
type UserCardProps = {
name: string;
isAdmin: boolean;
};
class UserCard extends HTMLElement {
declare readonly [JSX_PROPS]?: UserCardProps;
}
customElements.define('user-card', UserCard);
<UserCard name="Ada" isAdmin={false} />;JSX_PROPS declares unprefixed public props. Required fields remain required. Explicit prop:* bindings use the custom element instance type; attr:* bindings use DOM attribute types.
prop:* accepts any writable, non-method instance property. Readonly fields and getters without a setter are rejected, because assigning them throws or replaces state the element owns. Every public field is writable this way, including Radiant @state fields: decorators do not change a field's type, so JSX cannot tell @prop from @state. Mark internal state protected or private to keep it out of prop:*.
A subclass can widen the props by redeclaring declare readonly [JSX_PROPS]?: WiderProps (no override; declare override does not compile). A class meant to be extended can instead take its public props as a type parameter:
class UserCard<Props extends UserCardProps = UserCardProps> extends RadiantElement {
declare readonly [JSX_PROPS]?: Props;
@prop({ type: String }) name = '';
}
class AdminCard extends UserCard<UserCardProps & { level: string }> {
@prop({ type: String }) level = '';
}Typing By Tag Name
If JSX uses a tag name without importing the class, add that tag to HTMLElementTagNameMap. The class's JSX_PROPS declaration supplies the same public prop contract as the class form.
import type { JSX_PROPS } from '@ecopages/jsx';
type UserCardProps = {
name: string;
isAdmin: boolean;
};
class UserCard extends HTMLElement {
declare readonly [JSX_PROPS]?: UserCardProps;
}
declare global {
interface HTMLElementTagNameMap {
'user-card': UserCard;
}
}
customElements.define('user-card', UserCard);
<user-card name="Ada" isAdmin={false} />;Unprefixed props come from JSX_PROPS; prop:* bindings are typed from the element instance. Existing JsxCustomIntrinsicElements augmentations remain available as a supported compatibility path.
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
<UserGrid id="people" class="panel" items={rows} selection={currentRow} attr:status="ready" prop:api={gridApi} />import type { JSX_PROPS } from '@ecopages/jsx';
type UserGridRow = {
id: string;
};
type UserGridProps = {
items: UserGridRow[];
selection?: UserGridRow;
};
type UserGridApi = {
focusRow(id: string): void;
};
class UserGrid extends HTMLElement {
declare readonly [JSX_PROPS]?: UserGridProps;
api?: UserGridApi;
}
const rows: UserGridRow[] = [{ id: '1' }];
const currentRow = rows[0];
const gridApi: UserGridApi = {
focusRow: (_id) => undefined,
};
customElements.define('user-grid', UserGrid);
<UserGrid 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 fromUserGrid.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.