Add new html tags to the remote elements (#19723)

- Add 72 missing HTML and SVG elements to the remote-dom component
registry (48 HTML + 24 SVG), bringing the total from 47 to 119 supported
elements
- HTML additions include semantic inline text (b, i, u, s, mark, sub,
sup, kbd, etc.), description lists, ruby annotations, structural
elements (figure, details, dialog), and form utilities (fieldset,
progress, meter, optgroup)
- SVG additions include containers (svg, g, defs), shapes (path, circle,
rect, line, polygon), text (text, tspan), gradients (linearGradient,
radialGradient, stop), and utilities (clipPath, mask, foreignObject,
marker)
- Add htmlTag override to support SVG elements with camelCase names
(e.g. clipPath, foreignObject) while keeping custom element tags
lowercase per the Web Components spec
This commit is contained in:
Raphaël Bosi
2026-04-15 16:53:23 +02:00
committed by GitHub
parent 7601dbc218
commit 7ba5fe32f8
9 changed files with 4166 additions and 14 deletions
@@ -45,7 +45,7 @@ const getHtmlElementSchemas = (): ComponentSchema[] => {
events: element.events
? [...COMMON_HTML_EVENTS, ...element.events]
: COMMON_HTML_EVENTS,
htmlTag: extractHtmlTag(element.tag),
htmlTag: element.htmlTag ?? extractHtmlTag(element.tag),
}));
};
@@ -12,6 +12,7 @@ export const HtmlElementConfigZ = z.object({
.regex(/^Html[A-Z]/, 'Name must be PascalCase starting with Html'),
properties: z.record(z.string(), PropertySchemaZ),
events: z.array(z.string()).optional(),
htmlTag: z.string().optional(),
});
export const HtmlElementConfigArrayZ = z.array(HtmlElementConfigZ);
@@ -1,13 +1,12 @@
type PropertySchema = {
type: 'string' | 'number' | 'boolean';
optional: boolean;
};
import { type PropertySchema } from './PropertySchema';
import { SVG_PRESENTATION_PROPERTIES } from './SvgPresentationProperties';
export type AllowedHtmlElement = {
tag: string;
name: string;
properties: Record<string, PropertySchema>;
events?: string[];
htmlTag?: string;
};
export const ALLOWED_HTML_ELEMENTS: AllowedHtmlElement[] = [
@@ -249,4 +248,421 @@ export const ALLOWED_HTML_ELEMENTS: AllowedHtmlElement[] = [
height: { type: 'number', optional: true },
},
},
// Semantic inline text
{ tag: 'html-b', name: 'HtmlB', properties: {} },
{ tag: 'html-i', name: 'HtmlI', properties: {} },
{ tag: 'html-u', name: 'HtmlU', properties: {} },
{ tag: 'html-s', name: 'HtmlS', properties: {} },
{ tag: 'html-mark', name: 'HtmlMark', properties: {} },
{ tag: 'html-sub', name: 'HtmlSub', properties: {} },
{ tag: 'html-sup', name: 'HtmlSup', properties: {} },
{ tag: 'html-abbr', name: 'HtmlAbbr', properties: {} },
{ tag: 'html-cite', name: 'HtmlCite', properties: {} },
{ tag: 'html-kbd', name: 'HtmlKbd', properties: {} },
{ tag: 'html-samp', name: 'HtmlSamp', properties: {} },
{ tag: 'html-var', name: 'HtmlVar', properties: {} },
{ tag: 'html-dfn', name: 'HtmlDfn', properties: {} },
{ tag: 'html-bdi', name: 'HtmlBdi', properties: {} },
{
tag: 'html-bdo',
name: 'HtmlBdo',
properties: {
dir: { type: 'string', optional: true },
},
},
{
tag: 'html-data',
name: 'HtmlData',
properties: {
value: { type: 'string', optional: true },
},
},
// Edited/annotated text
{
tag: 'html-del',
name: 'HtmlDel',
properties: {
cite: { type: 'string', optional: true },
dateTime: { type: 'string', optional: true },
},
},
{
tag: 'html-ins',
name: 'HtmlIns',
properties: {
cite: { type: 'string', optional: true },
dateTime: { type: 'string', optional: true },
},
},
{
tag: 'html-q',
name: 'HtmlQ',
properties: {
cite: { type: 'string', optional: true },
},
},
{
tag: 'html-time',
name: 'HtmlTime',
properties: {
dateTime: { type: 'string', optional: true },
},
},
// Ruby annotations
{ tag: 'html-ruby', name: 'HtmlRuby', properties: {} },
{ tag: 'html-rt', name: 'HtmlRt', properties: {} },
{ tag: 'html-rp', name: 'HtmlRp', properties: {} },
// Description lists
{ tag: 'html-dl', name: 'HtmlDl', properties: {} },
{ tag: 'html-dt', name: 'HtmlDt', properties: {} },
{ tag: 'html-dd', name: 'HtmlDd', properties: {} },
// Structural/semantic
{ tag: 'html-figure', name: 'HtmlFigure', properties: {} },
{ tag: 'html-figcaption', name: 'HtmlFigcaption', properties: {} },
{
tag: 'html-details',
name: 'HtmlDetails',
properties: {
open: { type: 'boolean', optional: true },
},
},
{ tag: 'html-summary', name: 'HtmlSummary', properties: {} },
{ tag: 'html-address', name: 'HtmlAddress', properties: {} },
{
tag: 'html-dialog',
name: 'HtmlDialog',
properties: {
open: { type: 'boolean', optional: true },
},
},
{ tag: 'html-hgroup', name: 'HtmlHgroup', properties: {} },
{ tag: 'html-search', name: 'HtmlSearch', properties: {} },
// Table additions
{ tag: 'html-caption', name: 'HtmlCaption', properties: {} },
{
tag: 'html-colgroup',
name: 'HtmlColgroup',
properties: {
span: { type: 'number', optional: true },
},
},
{
tag: 'html-col',
name: 'HtmlCol',
properties: {
span: { type: 'number', optional: true },
},
},
// Form additions
{
tag: 'html-fieldset',
name: 'HtmlFieldset',
properties: {
disabled: { type: 'boolean', optional: true },
name: { type: 'string', optional: true },
},
},
{ tag: 'html-legend', name: 'HtmlLegend', properties: {} },
{
tag: 'html-output',
name: 'HtmlOutput',
properties: {
name: { type: 'string', optional: true },
htmlFor: { type: 'string', optional: true },
},
},
{
tag: 'html-progress',
name: 'HtmlProgress',
properties: {
value: { type: 'number', optional: true },
max: { type: 'number', optional: true },
},
},
{
tag: 'html-meter',
name: 'HtmlMeter',
properties: {
value: { type: 'number', optional: true },
min: { type: 'number', optional: true },
max: { type: 'number', optional: true },
low: { type: 'number', optional: true },
high: { type: 'number', optional: true },
optimum: { type: 'number', optional: true },
},
},
{
tag: 'html-optgroup',
name: 'HtmlOptgroup',
properties: {
label: { type: 'string', optional: true },
disabled: { type: 'boolean', optional: true },
},
},
{ tag: 'html-datalist', name: 'HtmlDatalist', properties: {} },
// Media additions
{ tag: 'html-picture', name: 'HtmlPicture', properties: {} },
{
tag: 'html-track',
name: 'HtmlTrack',
properties: {
src: { type: 'string', optional: true },
kind: { type: 'string', optional: true },
srclang: { type: 'string', optional: true },
label: { type: 'string', optional: true },
default: { type: 'boolean', optional: true },
},
},
// Miscellaneous
{ tag: 'html-wbr', name: 'HtmlWbr', properties: {} },
{ tag: 'html-menu', name: 'HtmlMenu', properties: {} },
// SVG container/structural
{
tag: 'html-svg',
name: 'HtmlSvg',
properties: {
...SVG_PRESENTATION_PROPERTIES,
viewBox: { type: 'string', optional: true },
xmlns: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
preserveAspectRatio: { type: 'string', optional: true },
},
},
{
tag: 'html-g',
name: 'HtmlG',
properties: {
...SVG_PRESENTATION_PROPERTIES,
},
},
{ tag: 'html-defs', name: 'HtmlDefs', properties: {} },
{
tag: 'html-symbol',
name: 'HtmlSymbol',
properties: {
viewBox: { type: 'string', optional: true },
},
},
{
tag: 'html-use',
name: 'HtmlUse',
properties: {
href: { type: 'string', optional: true },
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
},
},
{
tag: 'html-clippath',
name: 'HtmlClipPath',
htmlTag: 'clipPath',
properties: {
clipPathUnits: { type: 'string', optional: true },
},
},
{
tag: 'html-mask',
name: 'HtmlMask',
properties: {
maskUnits: { type: 'string', optional: true },
},
},
// SVG shapes
{
tag: 'html-circle',
name: 'HtmlCircle',
properties: {
...SVG_PRESENTATION_PROPERTIES,
cx: { type: 'string', optional: true },
cy: { type: 'string', optional: true },
r: { type: 'string', optional: true },
},
},
{
tag: 'html-ellipse',
name: 'HtmlEllipse',
properties: {
...SVG_PRESENTATION_PROPERTIES,
cx: { type: 'string', optional: true },
cy: { type: 'string', optional: true },
rx: { type: 'string', optional: true },
ry: { type: 'string', optional: true },
},
},
{
tag: 'html-rect',
name: 'HtmlRect',
properties: {
...SVG_PRESENTATION_PROPERTIES,
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
rx: { type: 'string', optional: true },
ry: { type: 'string', optional: true },
},
},
{
tag: 'html-line',
name: 'HtmlLine',
properties: {
...SVG_PRESENTATION_PROPERTIES,
x1: { type: 'string', optional: true },
y1: { type: 'string', optional: true },
x2: { type: 'string', optional: true },
y2: { type: 'string', optional: true },
},
},
{
tag: 'html-path',
name: 'HtmlPath',
properties: {
...SVG_PRESENTATION_PROPERTIES,
d: { type: 'string', optional: true },
},
},
{
tag: 'html-polygon',
name: 'HtmlPolygon',
properties: {
...SVG_PRESENTATION_PROPERTIES,
points: { type: 'string', optional: true },
},
},
{
tag: 'html-polyline',
name: 'HtmlPolyline',
properties: {
...SVG_PRESENTATION_PROPERTIES,
points: { type: 'string', optional: true },
},
},
// SVG text
{
tag: 'html-text',
name: 'HtmlText',
properties: {
...SVG_PRESENTATION_PROPERTIES,
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
dx: { type: 'string', optional: true },
dy: { type: 'string', optional: true },
textAnchor: { type: 'string', optional: true },
dominantBaseline: { type: 'string', optional: true },
},
},
{
tag: 'html-tspan',
name: 'HtmlTspan',
properties: {
...SVG_PRESENTATION_PROPERTIES,
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
dx: { type: 'string', optional: true },
dy: { type: 'string', optional: true },
},
},
// SVG gradients/patterns
{
tag: 'html-lineargradient',
name: 'HtmlLinearGradient',
htmlTag: 'linearGradient',
properties: {
x1: { type: 'string', optional: true },
y1: { type: 'string', optional: true },
x2: { type: 'string', optional: true },
y2: { type: 'string', optional: true },
gradientUnits: { type: 'string', optional: true },
gradientTransform: { type: 'string', optional: true },
},
},
{
tag: 'html-radialgradient',
name: 'HtmlRadialGradient',
htmlTag: 'radialGradient',
properties: {
cx: { type: 'string', optional: true },
cy: { type: 'string', optional: true },
r: { type: 'string', optional: true },
fx: { type: 'string', optional: true },
fy: { type: 'string', optional: true },
gradientUnits: { type: 'string', optional: true },
gradientTransform: { type: 'string', optional: true },
},
},
{
tag: 'html-stop',
name: 'HtmlStop',
properties: {
offset: { type: 'string', optional: true },
stopColor: { type: 'string', optional: true },
stopOpacity: { type: 'string', optional: true },
},
},
{
tag: 'html-pattern',
name: 'HtmlPattern',
properties: {
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
patternUnits: { type: 'string', optional: true },
patternTransform: { type: 'string', optional: true },
},
},
// SVG other
{
tag: 'html-image',
name: 'HtmlImage',
properties: {
href: { type: 'string', optional: true },
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
preserveAspectRatio: { type: 'string', optional: true },
},
},
{
tag: 'html-foreignobject',
name: 'HtmlForeignObject',
htmlTag: 'foreignObject',
properties: {
x: { type: 'string', optional: true },
y: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
},
},
{
tag: 'html-marker',
name: 'HtmlMarker',
properties: {
markerWidth: { type: 'string', optional: true },
markerHeight: { type: 'string', optional: true },
refX: { type: 'string', optional: true },
refY: { type: 'string', optional: true },
orient: { type: 'string', optional: true },
markerUnits: { type: 'string', optional: true },
},
},
{ tag: 'html-title', name: 'HtmlTitle', properties: {} },
];
@@ -4,24 +4,29 @@ const UTILITY_TAG_MAPPINGS: Record<string, string> = {
'remote-style': 'RemoteStyle',
};
const getHostTagName = (element: (typeof ALLOWED_HTML_ELEMENTS)[number]) =>
element.htmlTag ??
(element.tag.startsWith('html-') ? element.tag.slice(5) : element.tag);
export const HTML_TAG_TO_REMOTE_COMPONENT: Record<string, string> = {
...Object.fromEntries(
ALLOWED_HTML_ELEMENTS.map((element) => [
element.tag.startsWith('html-') ? element.tag.slice(5) : element.tag,
getHostTagName(element),
element.name,
]),
),
...UTILITY_TAG_MAPPINGS,
};
// Maps standard HTML tag names to their custom element equivalents
// used by the remote DOM polyfill (e.g. "div" → "html-div").
// Maps standard HTML/SVG tag names to their custom element equivalents
// used by the remote DOM polyfill (e.g. "div" → "html-div",
// "clipPath" → "html-clippath").
// Consumed by the jsx-runtime wrapper so React creates the correct
// custom elements instead of standard HTML tags.
// custom elements instead of standard HTML/SVG tags.
export const HTML_TAG_TO_CUSTOM_ELEMENT_TAG: Record<string, string> = {
...Object.fromEntries(
ALLOWED_HTML_ELEMENTS.map((element) => [
element.tag.startsWith('html-') ? element.tag.slice(5) : element.tag,
getHostTagName(element),
element.tag,
]),
),
@@ -0,0 +1,22 @@
import { type PropertySchema } from './PropertySchema';
export const SVG_PRESENTATION_PROPERTIES: Record<string, PropertySchema> = {
fill: { type: 'string', optional: true },
fillOpacity: { type: 'string', optional: true },
fillRule: { type: 'string', optional: true },
stroke: { type: 'string', optional: true },
strokeWidth: { type: 'string', optional: true },
strokeOpacity: { type: 'string', optional: true },
strokeLinecap: { type: 'string', optional: true },
strokeLinejoin: { type: 'string', optional: true },
strokeDasharray: { type: 'string', optional: true },
strokeDashoffset: { type: 'string', optional: true },
strokeMiterlimit: { type: 'string', optional: true },
opacity: { type: 'string', optional: true },
transform: { type: 'string', optional: true },
clipPath: { type: 'string', optional: true },
clipRule: { type: 'string', optional: true },
mask: { type: 'string', optional: true },
filter: { type: 'string', optional: true },
pointerEvents: { type: 'string', optional: true },
};
@@ -92,6 +92,162 @@ export const componentRegistry: Map<string, ComponentRegistryValue> = new Map([
'html-source',
createRemoteComponentRenderer(createHtmlHostWrapper('source')),
],
['html-b', createRemoteComponentRenderer(createHtmlHostWrapper('b'))],
['html-i', createRemoteComponentRenderer(createHtmlHostWrapper('i'))],
['html-u', createRemoteComponentRenderer(createHtmlHostWrapper('u'))],
['html-s', createRemoteComponentRenderer(createHtmlHostWrapper('s'))],
['html-mark', createRemoteComponentRenderer(createHtmlHostWrapper('mark'))],
['html-sub', createRemoteComponentRenderer(createHtmlHostWrapper('sub'))],
['html-sup', createRemoteComponentRenderer(createHtmlHostWrapper('sup'))],
['html-abbr', createRemoteComponentRenderer(createHtmlHostWrapper('abbr'))],
['html-cite', createRemoteComponentRenderer(createHtmlHostWrapper('cite'))],
['html-kbd', createRemoteComponentRenderer(createHtmlHostWrapper('kbd'))],
['html-samp', createRemoteComponentRenderer(createHtmlHostWrapper('samp'))],
['html-var', createRemoteComponentRenderer(createHtmlHostWrapper('var'))],
['html-dfn', createRemoteComponentRenderer(createHtmlHostWrapper('dfn'))],
['html-bdi', createRemoteComponentRenderer(createHtmlHostWrapper('bdi'))],
['html-bdo', createRemoteComponentRenderer(createHtmlHostWrapper('bdo'))],
['html-data', createRemoteComponentRenderer(createHtmlHostWrapper('data'))],
['html-del', createRemoteComponentRenderer(createHtmlHostWrapper('del'))],
['html-ins', createRemoteComponentRenderer(createHtmlHostWrapper('ins'))],
['html-q', createRemoteComponentRenderer(createHtmlHostWrapper('q'))],
['html-time', createRemoteComponentRenderer(createHtmlHostWrapper('time'))],
['html-ruby', createRemoteComponentRenderer(createHtmlHostWrapper('ruby'))],
['html-rt', createRemoteComponentRenderer(createHtmlHostWrapper('rt'))],
['html-rp', createRemoteComponentRenderer(createHtmlHostWrapper('rp'))],
['html-dl', createRemoteComponentRenderer(createHtmlHostWrapper('dl'))],
['html-dt', createRemoteComponentRenderer(createHtmlHostWrapper('dt'))],
['html-dd', createRemoteComponentRenderer(createHtmlHostWrapper('dd'))],
[
'html-figure',
createRemoteComponentRenderer(createHtmlHostWrapper('figure')),
],
[
'html-figcaption',
createRemoteComponentRenderer(createHtmlHostWrapper('figcaption')),
],
[
'html-details',
createRemoteComponentRenderer(createHtmlHostWrapper('details')),
],
[
'html-summary',
createRemoteComponentRenderer(createHtmlHostWrapper('summary')),
],
[
'html-address',
createRemoteComponentRenderer(createHtmlHostWrapper('address')),
],
[
'html-dialog',
createRemoteComponentRenderer(createHtmlHostWrapper('dialog')),
],
[
'html-hgroup',
createRemoteComponentRenderer(createHtmlHostWrapper('hgroup')),
],
[
'html-search',
createRemoteComponentRenderer(createHtmlHostWrapper('search')),
],
[
'html-caption',
createRemoteComponentRenderer(createHtmlHostWrapper('caption')),
],
[
'html-colgroup',
createRemoteComponentRenderer(createHtmlHostWrapper('colgroup')),
],
['html-col', createRemoteComponentRenderer(createHtmlHostWrapper('col'))],
[
'html-fieldset',
createRemoteComponentRenderer(createHtmlHostWrapper('fieldset')),
],
[
'html-legend',
createRemoteComponentRenderer(createHtmlHostWrapper('legend')),
],
[
'html-output',
createRemoteComponentRenderer(createHtmlHostWrapper('output')),
],
[
'html-progress',
createRemoteComponentRenderer(createHtmlHostWrapper('progress')),
],
['html-meter', createRemoteComponentRenderer(createHtmlHostWrapper('meter'))],
[
'html-optgroup',
createRemoteComponentRenderer(createHtmlHostWrapper('optgroup')),
],
[
'html-datalist',
createRemoteComponentRenderer(createHtmlHostWrapper('datalist')),
],
[
'html-picture',
createRemoteComponentRenderer(createHtmlHostWrapper('picture')),
],
['html-track', createRemoteComponentRenderer(createHtmlHostWrapper('track'))],
['html-wbr', createRemoteComponentRenderer(createHtmlHostWrapper('wbr'))],
['html-menu', createRemoteComponentRenderer(createHtmlHostWrapper('menu'))],
['html-svg', createRemoteComponentRenderer(createHtmlHostWrapper('svg'))],
['html-g', createRemoteComponentRenderer(createHtmlHostWrapper('g'))],
['html-defs', createRemoteComponentRenderer(createHtmlHostWrapper('defs'))],
[
'html-symbol',
createRemoteComponentRenderer(createHtmlHostWrapper('symbol')),
],
['html-use', createRemoteComponentRenderer(createHtmlHostWrapper('use'))],
[
'html-clippath',
createRemoteComponentRenderer(createHtmlHostWrapper('clipPath')),
],
['html-mask', createRemoteComponentRenderer(createHtmlHostWrapper('mask'))],
[
'html-circle',
createRemoteComponentRenderer(createHtmlHostWrapper('circle')),
],
[
'html-ellipse',
createRemoteComponentRenderer(createHtmlHostWrapper('ellipse')),
],
['html-rect', createRemoteComponentRenderer(createHtmlHostWrapper('rect'))],
['html-line', createRemoteComponentRenderer(createHtmlHostWrapper('line'))],
['html-path', createRemoteComponentRenderer(createHtmlHostWrapper('path'))],
[
'html-polygon',
createRemoteComponentRenderer(createHtmlHostWrapper('polygon')),
],
[
'html-polyline',
createRemoteComponentRenderer(createHtmlHostWrapper('polyline')),
],
['html-text', createRemoteComponentRenderer(createHtmlHostWrapper('text'))],
['html-tspan', createRemoteComponentRenderer(createHtmlHostWrapper('tspan'))],
[
'html-lineargradient',
createRemoteComponentRenderer(createHtmlHostWrapper('linearGradient')),
],
[
'html-radialgradient',
createRemoteComponentRenderer(createHtmlHostWrapper('radialGradient')),
],
['html-stop', createRemoteComponentRenderer(createHtmlHostWrapper('stop'))],
[
'html-pattern',
createRemoteComponentRenderer(createHtmlHostWrapper('pattern')),
],
['html-image', createRemoteComponentRenderer(createHtmlHostWrapper('image'))],
[
'html-foreignobject',
createRemoteComponentRenderer(createHtmlHostWrapper('foreignObject')),
],
[
'html-marker',
createRemoteComponentRenderer(createHtmlHostWrapper('marker')),
],
['html-title', createRemoteComponentRenderer(createHtmlHostWrapper('title'))],
['remote-style', createRemoteComponentRenderer(RemoteStyleRenderer)],
['remote-fragment', RemoteFragmentRenderer],
]);
@@ -13,14 +13,14 @@ const EVENT_NAME_MAP: Record<string, string> = Object.fromEntries(
);
const VOID_ELEMENTS = new Set([
'input',
'br',
'hr',
'img',
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'source',
File diff suppressed because it is too large Load Diff