Make a section interactive
Make part of a section interactive with a client-hydrated island.
Make part of a section interactive.
Prerequisites
Section titled “Prerequisites”- A section to add the island to —
Add a section to a template
adds one, with
Renderer.tsxalongsidesection.tsandfixtures.ts. - A slot for the island to render, when it owns one —
AI writes slot content adds a
summary.
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 false — true 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.tsx — import ContactReveal from "./ContactReveal";, a
default import because that is how an island exports itself — and render it there like
any other component:
<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.
Complete diff
Section titled “Complete diff”The island file under Steps, its import in Renderer.tsx, and the one
line there that renders it are the whole change.
Verify
Section titled “Verify”Verify with the author loop.
Rules that can fire
Section titled “Rules that can fire”require-island-editor— the new island has a default export but no literaleditordeclaration.serializable-island-props— a prop crossing into the island is a function, aDate, or a class instance.island-server-frame— the island declaresserverFrame = falsebut server-renders real markup, or renders nothing without declaring it.no-client-runtime-in-server— the state, effect or event handler stays inRenderer.tsxinstead of moving into the island.no-client-directive-in-contract— the"use client"directive lands inRenderer.tsxor 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.
See also
Section titled “See also”- 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.