Skip to content
HomePagesHomePages template kit
Menu

Make part of a section interactive

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

Make part of a section interactive.

Starting from the starter’s hero section, add a "use client" file next to the four contract files, matching the shape of the starter’s own island, ExpandableText.tsx: the directive as the file’s first statement, a default-exported function typed against plain props, and useState for the interactive part.

// sections/hero/ContactReveal.tsx — a new island, same shape as ExpandableText
"use client";
import { type ReactNode, useState } from "react";
import type { IslandEditorOptions } from "@homepages/template-kit";
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>
);
}
// Optional, as on ExpandableText: hydrate live in the editor canvas too.
export const editor: IslandEditorOptions = {
live: true,
};

Render it from Renderer.tsx like any other component:

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

If the island renders a schema slot’s own content — the way the starter’s blurb slot renders through ExpandableText — wrap it in <Slot id="…"> with the matching schema id, exactly as Renderer.tsx already does:

<Slot id="blurb">
<ExpandableText text={slots.blurb} collapsedChars={140} moreLabel="Read more" />
</Slot>

A standalone island with no bound slot — like ContactReveal above — needs no <Slot> wrapper; <Slot> marks an editable slot’s DOM for the editor, 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.

None — this is a pure content edit. No CLI command beyond the author loop below.

Verify with the author loop.

  • 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.