> ## Documentation Index
> Fetch the complete documentation index at: https://juo.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Blocks

> Self-contained UI components with typed props and slots, described by a type-safe schema.

A **block** is a self-contained UI component. It has **props** to configure it and **slots** to compose it with other blocks. A type-safe **schema** describes both — driving TypeScript inference in the renderer.

What makes a block different from a regular component is that it can consume **services** from its surrounding [contexts](/docs/blocks/concepts/contexts) to read and manipulate live store data (subscriptions, customers, orders) without prop drilling.

## Anatomy of a block

Every block has four parts:

| Part              | Purpose                                                                                                                                                                                                                                                              |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name**          | Unique identifier (e.g. `"Button"`) used to reference the block from other blocks and from the registry.                                                                                                                                                             |
| **Schema**        | JSON Schema that describes the block's `props` and `slots`. Drives type inference for the renderer.                                                                                                                                                                  |
| **Initial value** | Function returning the default `props` / `slots` when the block is first instantiated.                                                                                                                                                                               |
| **Renderer**      | Function that takes a `BlockInstance` and returns an `HTMLElement` to mount. The framework boundary — implementations exist for plain DOM, [Vue](/docs/blocks/frameworks/vue), [React](/docs/blocks/frameworks/react), and [Preact](/docs/blocks/frameworks/preact). |

A block is **defined once** with `defineBlock`, **registered once** with `registerBlock`, and **instantiated many times** through `createBlockInstance`.

```ts theme={null}
import { defineBlock, registerBlock } from "@juo/blocks";
```

<img src="https://mintcdn.com/juo/DmI4hy9S0Wn-6Yem/docs/blocks/images/block-anatomy.svg?fit=max&auto=format&n=DmI4hy9S0Wn-6Yem&q=85&s=9313eed951a5aa7d599a69d1524299ce" alt="A purple juo-block frame named 'SubscriptionCard' containing two green-bordered panels. The props panel has 'shape defined by the schema' as a subtitle at the top, followed by variant, size, and showThumbnail values. The slots panel has 'each slot exposed via juo-extension-root' as a subtitle at the top, followed by header, body, and actions slots. Below, a dashed 'renderer output' frame shows a rendered card with a thumbnail, a bold 'Your plan' heading, two lines of muted text ('Renews monthly · Next: Jul 5, 2026' and '$14.99 / month'), and a 'Manage' button on the right." width="800" height="440" data-path="docs/blocks/images/block-anatomy.svg" />

## A simple block

A button with one prop — a text label. The renderer creates a real DOM element and uses `effect()` to keep it in sync with `block.props`.

```ts theme={null}
import { defineBlock, registerBlock, effect, untracked } from "@juo/blocks";
import type { FromSchema, JSONSchema } from "json-schema-to-ts";

const schema = {
  type: "object",
  properties: {
    props: {
      type: "object",
      properties: { label: { type: "string" } },
      required: ["label"],
      additionalProperties: false,
    },
  },
  required: ["props"],
  additionalProperties: false,
} as const satisfies JSONSchema;

export const ButtonBlock = defineBlock<FromSchema<typeof schema>>("Button", {
  group: "store",
  schema,
  initialValue: () => ({ props: { label: "Click me" } }),
  renderer(block) {
    const el = document.createElement("button");

    effect(() => {
      const { label } = block.props;
      untracked(() => {
        el.textContent = label;
      });
    });

    return el;
  },
});

registerBlock(ButtonBlock);
```

`as const satisfies JSONSchema` plus `FromSchema<typeof schema>` keeps the schema and the renderer's prop types in sync — change the schema, the TS types update.

## A more advanced block

A card block with multiple typed props, nested slots, and a preset shown in the Editor's picker.

```ts theme={null}
import {
  defineBlock,
  registerBlock,
  createBlockInstance,
  effect,
  untracked,
} from "@juo/blocks";
import { html, render } from "lit-html";
import type { FromSchema, JSONSchema } from "json-schema-to-ts";

const schema = {
  type: "object",
  properties: {
    props: {
      type: "object",
      properties: {
        variant: { enum: ["primary", "secondary", "ghost"] },
        size: { enum: ["sm", "md", "lg"] },
        showThumbnail: { type: "boolean" },
      },
      required: ["variant", "size", "showThumbnail"],
      additionalProperties: false,
    },
    slots: {
      type: "object",
      properties: { header: {}, body: {}, actions: {} },
      required: ["header", "body", "actions"],
      additionalProperties: false,
    },
  },
  required: ["props", "slots"],
  additionalProperties: false,
} as const satisfies JSONSchema;

export const SubscriptionCardBlock = defineBlock<FromSchema<typeof schema>>(
  "SubscriptionCard",
  {
    group: "juo",
    schema,
    initialValue: () => ({
      props: { variant: "primary", size: "md", showThumbnail: true },
      slots: {
        header: createBlockInstance("Text", { slots: { default: "Your plan" } }),
        body: [],
        actions: [
          createBlockInstance("Button", { props: { label: "Manage" } }),
        ],
      },
    }),
    presets: {
      compact: {
        label: "Compact",
        description: "Smaller card with no thumbnail",
        group: "Layouts",
        state: { props: { variant: "ghost", size: "sm", showThumbnail: false } },
        context: {},
      },
    },
    renderer(block) {
      const el = document.createElement("div");

      effect(() => {
        const { variant, size, showThumbnail } = block.props;
        untracked(() => {
          render(
            html`
              <div class="card card--${variant} card--${size}">
                ${showThumbnail ? html`<div class="card__thumb"></div>` : null}
                <div class="card__header"></div>
                <div class="card__body"></div>
                <div class="card__actions"></div>
              </div>
            `,
            el,
          );
          // mount slot children into the prepared sockets
          mountSlot(el.querySelector(".card__header"), block.slots?.header);
          mountSlot(el.querySelector(".card__body"), block.slots?.body);
          mountSlot(el.querySelector(".card__actions"), block.slots?.actions);
        });
      });

      return el;
    },
  },
);

registerBlock(SubscriptionCardBlock);
```

Highlights:

* **Typed props.** `variant`, `size`, and `showThumbnail` are fully typed inside the renderer.
* **Slots.** A slot can be a single block, an array of blocks, or a plain string. `createBlockInstance` builds a nested block by name and merges overrides into its default `initialValue`.
* **Presets.** Each preset provides a complete `state` snapshot (props + slot contents) — a named starting point for a block instance.

## Block groups

Every block declares a `group` that identifies its origin:

| Group         | Origin                                        |
| ------------- | --------------------------------------------- |
| **1st party** | Blocks provided by Juo.                       |
| **3rd party** | Blocks provided by partners and integrations. |
| **Store**     | Blocks published by the portal owner.         |

All three groups are first-class — there's no behavioural difference between them.

## Registering blocks

`registerBlock` adds the definition to the registry so it can be instantiated by name. Call it once at the top of the package entry file:

```ts theme={null}
// src/main.ts
import { registerBlock } from "@juo/blocks";
import { ButtonBlock } from "./Button";
import { SubscriptionCardBlock } from "./SubscriptionCard";

export function registerBlocks() {
  registerBlock(ButtonBlock);
  registerBlock(SubscriptionCardBlock);
}
```

After registration, the block becomes available wherever the runtime is loaded.
