Skip to content
HomePagesHomePages template kit

Make a section interactive

Make part of a section interactive with a client-hydrated island.

Make part of a section interactive.

Starting from a section that renders, add a "use client" file next to the section’s contract files: the directive as the file’s first statement, a default-exported function typed against plain props, and useState for the interactive part.

// sections/<name>/ContactReveal.tsx — a new island
"use client";
import type { IslandEditor } from "@homepages/template-kit";
import { type ReactNode, useState } from "react";
export default function ContactReveal({
hiddenLabel,
revealedValue,
}: {
hiddenLabel: string;
revealedValue: string;
}): ReactNode {
const [revealed, setRevealed] = useState(false);
return revealed ? (
<p className="text-base font-medium text-ink">{revealedValue}</p>
) : (
<button
type="button"
className="font-medium text-accent underline"
onClick={() => setRevealed(true)}
>
{hiddenLabel}
</button>
);
}
// Required of every island: whether it hydrates in the editor canvas.
export const editor = { live: true } satisfies IslandEditor;

editor is not optional, and live is all of it. Every "use client" module with a default export states live as a written-out true or falsetrue hydrates it in the editor canvas, where it runs exactly its published behavior; false leaves it as static server-rendered HTML there. There is no default and nothing in between, so an island that says nothing is an undecided island rather than a static one.

A live island can also see what the author selected, through the kit’s useEditor() hook — the other half of the contract, and the way an island opens the row or slide a click landed on. Islands covers both.

Import it into Renderer.tsximport ContactReveal from "./ContactReveal";, a default import because that is how an island exports itself — and render it there like any other component:

Renderer.tsx
<ContactReveal hiddenLabel="Show phone number" revealedValue="(555) 010-2020" />

When the island renders a schema slot’s own content, the slot’s marker has to reach the DOM, and the primitive is the only thing that emits it. A component that owns its own children — a third-party read-more widget, say, imported the same way: import ReadMore from "some-package"; — renders inside the primitive’s children, with the primitive as the marked wrapping element:

<Slot.Text slot={slot.summary} as="div">
<ReadMore text={slots.summary} collapsedChars={140} moreLabel="Read more" />
</Slot.Text>

The island can equally own the marker itself: a binding is plain JSON, so pass slot.summary straight in as a prop and let the island render it through its own Slot.*. It never imports schema or calls bindSlots, so that costs the island bundle nothing.

A standalone island with no bound slot — like ContactReveal above — needs no marker at all; a marker says “this is an editable slot’s content”, not “this is interactive.”

Props crossing into an island must be JSON-serializable — strings, numbers, booleans, and plain arrays/objects only, never a function, a Date, or a class instance; the island owns its own event handlers instead of receiving one. This is the only mechanism for interactivity in a section: a Renderer stays a pure function of its props, and any state, effect, or event handler moves into a "use client" island like the one above.

The island file under Steps, its import in Renderer.tsx, and the one line there that renders it are the whole change.

Verify with the author loop.

  • require-island-editor — the new island has a default export but no literal editor declaration.
  • serializable-island-props — a prop crossing into the island is a function, a Date, or a class instance.
  • island-server-frame — the island declares serverFrame = false but server-renders real markup, or renders nothing without declaring it.
  • no-client-runtime-in-server — the state, effect or event handler stays in Renderer.tsx instead of moving into the island.
  • no-client-directive-in-contract — the "use client" directive lands in Renderer.tsx or a declaration file instead of the island’s own file.
  • missing-slot-marker — the island renders a slot’s content outside its primitive, so the marker never reaches the DOM.
  • Islands — writing one, its props contract, editor options, and the DOM contract a published page hydrates.
  • Server vs client — how a file is scoped server or client, and the three rules an island is exempt from.
  • serializable-island-props — the rule that catches a non-serializable prop before the render ever throws.