Custom components
Extend Joy DOM with renderer-specific UI.
Joy DOM renders a focused subset of HTML. When you need something outside that subset - a chart, a payment button, a platform-native control - describe it in JSON as a custom node and let the renderer fill it in.
This chapter adds a contact-button to the business card.
Anatomy
A custom node looks just like a built-in node, except the type is a kebab-case custom name:
{
"type": "contact-button",
"props": {
"id": "primary"
},
"children": ["Get in touch"]
}Joy DOM enforces three rules on the name, drawn from the HTML custom element spec:
- Must contain at least one hyphen (
-). - Must be all lowercase.
- Must not collide with a reserved name (e.g.
font-face,annotation-xml). See §3.6.
Register a React component
The React renderer takes a components map keyed by custom name. Each value is a React component that receives the resolved props:
import { , type } from "@joy-dom/react";
import type { } from "@joy-dom/core";
function ({ , , }: ) {
return (
<
={}
={}
={() => ..("mailto:hello@example.com")}
>
{}
</>
);
}
export function ({ }: { : }) {
return < ={} ={{ "contact-button": }} />;
}The component receives:
| Prop | What's in it |
|---|---|
children | Rendered child nodes and text from the JSON. |
node | The raw Joy DOM node (type, props, children). |
id | The resolved node id (if any). |
className | The document's class list for the node, joined. |
style | Built-in nodes only; undefined for custom types. |
Style it like a built-in
Custom components participate in the full style cascade. Style them with a class selector in the document:
{
"style": {
".cta": {
"display": "flex",
"padding": {
"value": 12,
"unit": "px"
},
"backgroundColor": "#0f172a",
"color": "#ffffff",
"borderRadius": {
"value": 8,
"unit": "px"
},
"borderWidth": {
"value": 0,
"unit": "px"
}
}
}
}Then reference it from the node:
{
"type": "contact-button",
"props": {
"id": "primary",
"className": ["cta"]
},
"children": ["Get in touch"]
}Missing registrations throw
If a document references a custom node type without a registered component, the React renderer throws at render time. The fail-fast is deliberate: a silent fallback would hide a misconfiguration that points at a real bug.
To handle it, register a component for every custom type your documents use. The renderer matches by exact name, so a missing or misspelled entry surfaces the moment you render rather than as a blank box in production.
Cross-renderer parity
Each renderer registers components in its own way. Use the same type name across all three so
the document stays portable.
Each renderer's page covers its own registration:
- React — a
componentsmap of React components. - Swift — a
ComponentRegistryof view builders. - Kotlin — a
JoyDomComponentsmap of composables.
Add it to the card
The full business card with the button:
{
"version": 1,
"style": {
".card": {
"display": "flex",
"flexDirection": "column",
"gap": {
"value": 8,
"unit": "px"
},
"padding": {
"value": 24,
"unit": "px"
}
},
"h1": {
"display": "flex",
"fontSize": {
"value": 28,
"unit": "px"
},
"fontWeight": "bold"
},
"p": {
"display": "flex",
"color": "#475569"
},
".cta": {
"display": "flex",
"padding": {
"value": 12,
"unit": "px"
},
"backgroundColor": "#0f172a",
"color": "#ffffff",
"borderRadius": {
"value": 8,
"unit": "px"
},
"borderWidth": {
"value": 0,
"unit": "px"
}
}
},
"breakpoints": [],
"layout": {
"type": "div",
"props": {
"className": ["card"]
},
"children": [
{
"type": "h1",
"children": ["Avery Chen"]
},
{
"type": "p",
"children": ["Product designer · Tokyo"]
},
{
"type": "contact-button",
"props": {
"id": "primary",
"className": ["cta"]
},
"children": ["Get in touch"]
}
]
}
}Wrap-up
You now have a complete, responsive, extensible Joy DOM document. The last chapter ships it: where to put the JSON, how to load it at runtime, and what print-mode looks like. See chapter 5.
Spec references: §3.6 Custom nodes · §7 Custom components