Joy DOM

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:

  1. Must contain at least one hyphen (-).
  2. Must be all lowercase.
  3. 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 { Spec } from "@joy-dom/spec";

function ({ , , ,  }: ) {
  return (
    <
      ={}
      ={}
      ={}
      ={() => ..("mailto:hello@example.com")}
    >
      {}
    </>
  );
}

export function ({  }: { : Spec }) {
  return < ={} ={{ "contact-button":  }} />;
}

The component receives:

PropWhat's in it
childrenRendered child nodes and text from the JSON.
nodeThe raw Joy DOM node (type, props, children).
idThe resolved node id (if any).
classNameRenderer-generated classes plus document classes.
styleThe resolved inline style for this platform renderer.

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. This is intentional - silent fallback would hide a misconfiguration that almost always points at a real bug.

Cross-renderer parity

Each renderer registers components in its own idiomatic way (props in React, view builders in SwiftUI, composable functions in Compose). Use the same type name across renderers so the document stays portable.

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

On this page