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 {  } from "@joy-dom/core";

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

export function ({  }: { :  }) {
  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).
classNameThe document's class list for the node, joined.
styleBuilt-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 components map of React components.
  • Swift — a ComponentRegistry of view builders.
  • Kotlin — a JoyDomComponents map 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

On this page