Components

A PascalCase function that returns JSX compiles to a Custom Element. Import it and use it as a tag — no registration step.

JSX
function Badge({ label }) {
  return <span class="badge">{label}</span>;
}

export default function App() {
  return <Badge label="New" />;
}

Badge.jsx compiles to a Custom Element registered under a module-scoped tag such as web-badge-4e54f4d4 (the hash avoids collisions across files). In JSX you always write <Badge> — the compiler addresses Badge.tag for you.

Props

Props are passed as attributes and destructured from the parameter. They are reactive — when a parent passes a new value, the child updates in place.

JSX
function Greeting({ name = "world" }) {
  return <h1>Hello, {name}</h1>;
}
Defaults

Destructuring defaults ({ name = "world" }) work as written.

Context

When many descendants need the same value — theme, locale, auth — pass it through Context instead of drilling props through every layer. A parent wraps the subtree in <ContextProvider>; nested components read it with $context.

Children

Render passed children with props.children:

JSX
function Card({ children }) {
  return <div class="card">{children}</div>;
}

Imperative API

When a parent needs to command a child — open a dialog, focus a field, scroll a row — use $expose to publish methods on the component instance. The parent holds a $ref to the child and calls those methods:

JSX
export default function Modal() {
  let open = $state(false);
  $expose({ show: () => (open = true), hide: () => (open = false) });
  return open ? <dialog open>…</dialog> : null;
}

export default function App() {
  const modal = $ref();
  return (
    <>
      <button onclick={() => modal.show()}>Open</button>
      <Modal ref={modal} />
    </>
  );
}

See the Imperative API guide for modal, focus, list-scroll, and media-control patterns.

Native interop

Because each component is a real HTMLElement, it works in plain HTML once your bundle has loaded and registered it. The registry tag includes a module hash — it is not the bare web-<name> you might guess from the component identifier:

HTML
<!-- After your bundle registers Badge -->
<web-badge-4e54f4d4 label="Beta"></web-badge-4e54f4d4>

Read the exact tag from the exported class (Badge.tag) or from DevTools after a build. For host styling, the compiler also stamps a stable class hook on every instance (web-badge for Badge) so CSS does not need the hash:

CSS
.web-badge {
  display: inline-block;
}
Compose with JSX in app code

Inside OTF Web, always use <Badge … />. Embed the raw custom-element tag only when dropping a pre-built component into a non-OTF page.