Skip to content

Placing islands

Templates are static by design, no scripts, no SDKs, no API calls. Live functionality (taking a donation, buying a ticket, signing in) comes from islands: platform-owned, hydrated components your template places with a single tag.

You cannot re-lay-out an island’s internals, and that’s a design decision, not a missing feature:

  1. Compliance is platform-owned. The donation widget isn’t display, it’s Strong Customer Authentication, the Gift Aid declaration’s legal wording, consent capture. Markup a template could rearrange is markup a template could break in ways that have regulatory consequences.
  2. The upgrade promise depends on it. The platform ships payment, security and accessibility changes underneath published templates with zero author intervention. That’s only possible because transactional internals belong to the platform, your template keeps working precisely because it never reached inside.

The boundary is drawn at transactional vs display. Money movement, identity and consent are islands forever. Display of platform data, what an event or article looks like in a listing, is yours to own through page templates and their documented data context. If you’re fighting an island for layout control, you’re usually on the display side of the line and a page template is the right tool.

<div class="lq-cta-widget">
{% island 'donation_widget' %}
</div>

At render time the platform splits your output at each island marker and mounts the real, interactive component in that position. Payment logic, compliance (Gift Aid, consent), provider SDKs, accessibility, all platform-side, all upgraded platform-side. Your template keeps working when we upgrade the internals; that’s the deal.

The local kit and Studio preview show non-interactive fixture skeletons instead. They expose the same stable styling classes as the live island, so you can assess spacing, type, colour and common content states while the preview remains network dead. They are visual aids, not simulations of a payment, identity or consent flow.

  1. Declare what you place. Every island name used in your renderers must be listed in manifest.supports.islands. The validator enforces placed ⊆ declared ⊆ platform registry.
  2. Unknown names render nothing. An island that isn’t in the registry produces no output, never an error page.
  3. Planned islands are placeable-later. The registry marks each island available or planned. Declaring a planned island is a validator warning today; it starts rendering the day the platform ships it.

Submission islands such as newsletter_signup and form keep abuse checks, privacy acceptance and consent evidence in the platform. search reads public projections only. language_switch and next_prayer own client behaviour that Liquid cannot provide without JavaScript. Templates position these islands and style only their published classes.

Most templates should not place donation_widget in the hero at all. Place this instead:

{% island 'primary_action_widget' %}

It is the widget that leads the site, and the charity decides what that is from their Pages editor: the donation widget when giving leads, the volunteer sign-up when volunteering leads, or nothing when they chose no widget in the hero. site.focus tells your template which one it will be (donate, volunteer or none), and section.action on the home hero is the button that goes with it: always the action whose widget is not on the page, so the leading action never appears twice.

To take part, declare both the island and what your template can lead with:

"islands": ["primary_action_widget"],
"focus": ["donate", "volunteer"]

The validator holds you to it: a template that declares volunteer must place either this island or volunteer_signup.

See both while you work: in p60-template-kit dev the bar at the top has a Leads with switch (giving, volunteering, buttons only), or add ?focus=volunteer or ?focus=none to any preview URL. The hero widget becomes the donation widget, the volunteer sign-up, or nothing, and site.focus, site.actions, the header button and section.action follow, exactly as they do on a live site. Place donation_widget directly only when your template deliberately owns the switch itself by branching on section.primary.kind.

You never restyle island internals, those class names are private and change without notice. Each island publishes a styling API: the stable class names your theme.css may target. The donation widget, for example, exposes .donate-card, .freq-tabs, .amount-grid and friends.

/* Safe: published styling API + platform tokens */
.donate-card {
border-radius: var(--radius);
box-shadow: var(--shadow);
}
/* NOT safe: anything not in the styling API is internal */

Most of an island’s look follows your token overrides automatically, islands are built from the same --primary/--accent/--surface palette as everything else, so a well-themed template usually needs little or no island-specific CSS.

An island renders into the position of its tag, sized by your surrounding markup. Give it a container you control (like the starter’s .lq-cta-widget) and do your layout there, grid placement, max-width, spacing. Treat the island itself as an opaque box.

Three home sections, events, whatsOn and articles, used to be island-only. They now carry data contexts (events, infoEvents, latestArticles, see sections & data), so you choose per section:

  • Render the data yourself, your markup, your voice. A newspaper template might set events as a ruled diary column; a photography-led one as full-bleed cards. Honour derive-or-omit: wrap the section in {% if events.size > 0 %} so an empty list renders nothing (the validator proves both directions).
  • Or place the island, {% island 'events_carousel' %}, {% island 'whats_on_strip' %}, {% island 'latest_articles' %} remain the zero-effort defaults and handle empty states for you.

The payment boundary is absolute either way: these are listing models. Ticket sales, RSVP and enrolment happen on platform-rendered detail views, link with detailHref / href, never rebuild those flows.