Skip to content

Provider, components and useIsland

An island is one block of SparkleTree-managed copy: a hero, or a call-to-action. Hero and Cta are thin wrappers over the useIsland hook; when their layout does not suit you, call the hook and write your own.

Wrap the page once with SparkletreeProvider

Section titled “Wrap the page once with SparkletreeProvider”

Everything else must render inside it, or it throws with a message telling you so.

<SparkletreeProvider publishableKey={KEY}>{children}</SparkletreeProvider>
PropWhat it does
publishableKeyst_pk_live_… or st_pk_test_… from the dashboard. Decoded locally to an organization id and API host.
apiBaseAPI host, if you self-host. Wins over the key.
organizationIdYour organization id. Wins over the key.
contextExtra context merged over what the SDK collects. Yours wins on conflict. Must be JSON-serializable.
mintTokenA signed token, or a function returning one, for fragment minting.
storageWhere the visitor’s “standard version” preference is kept. Defaults to window.localStorage; pass your own if a consent manager gates it.
fetchImplReplacement fetch, for tests.

Pass publishableKey, or both apiBase and organizationId. With neither, the provider throws at render. A misconfigured integration is a setup error, not something to silently degrade around.

An inline context={{ … }} object is fine. Identity is compared by content, so re-creating the object each render does not re-request anything.

Time of day, day of week, timezone, language, a coarse mobile/desktop flag, and the origin of the referring page, never the full referrer URL. No permission prompt, no geolocation, no fingerprinting. Each signal is optional: missing ones cost a less well-adapted creative, not a working page.

Override any of them, or drop one entirely:

<SparkletreeProvider publishableKey={KEY} context={{ deviceType: "signage", referrer: undefined }}>

One exception: time of day. The server derives the daypart from the timezone itself, so a timeOfDay override is ignored for island copy — override the timezone if you need a different clock.

Hero renders a greeting, headline and body

Section titled “Hero renders a greeting, headline and body”
<Hero
campaignId="camp_123"
fallback={{
greeting: "Good evening",
headline: "Software that meets the moment",
body: "Adaptive creative for the whole funnel.",
}}
className="my-hero"
/>

Fields you leave out of fallback are not rendered. The component draws the campaign’s background image behind the copy when there is one, and applies the campaign’s colours as CSS custom properties on the section.

<Cta campaignId="camp_123" fallback={{ cta: "Start free" }} href="/signup" />

It renders an <a> when a destination survives vetting: the campaign’s own if it supplies one, otherwise your href. With neither it renders a non-interactive <span> instead. Clicks are reported automatically, and your onClick still runs.

PropWhat it does
campaignIdThe campaign to play.
surfaceIdUse instead of campaignId when a screen or placement decides what plays. Pass one or the other.
fallbackRequired. Your copy: { greeting?, headline?, body?, cta? }.
serverRenderedFinished campaign creative the platform already rendered, not your fallback. Omit it unless that is what you have. Passing true makes smooth inert; false only forces the hold back on.
enabledfalse skips the network entirely and renders your fallback. For Storybook, previews and tests.
sessionIdKeeps copy continuous for a returning visitor on an individual screen. Omit and each page view is independent.
pinDefaults to true: this block shares the page’s campaign version. Set false for a block that is meant to say something different for the same campaign.
smoothDefaults to true: paces every entrance, live typing and composed copy landing on the held canvas alike, so bursts read as writing. Pass false for wire text verbatim. Presentation only.
holdMsHow long the canvas holds waiting for the stream before your fallback paints. While holding, the fallback occupies its space invisibly, so nothing below the island moves when copy lands. Defaults to 2500, and restarts once when the stream proves it is alive. Your fallback is still the floor whatever you set.
className, stylePassed through.
layouts (Hero only)Named render functions the campaign may choose between.

A campaign can ask for a named layout; you decide which names you draw. The layout arrives with the stream, so the block starts in the default arrangement and the component swaps when it lands. An undeclared name falls through to the built-in arrangement, so a campaign can never ask for markup you did not write.

<Hero
campaignId="camp_123"
fallback={copy}
layouts={{
split: ({ headline, body, backgroundImage }) => (
<div className="grid grid-cols-2">
<div>{headline}{body}</div>
<img src={backgroundImage ?? ""} alt="" />
</div>
),
}}
/>
import { useIsland, heldReserve, StreamText } from "@sparkletree/react";
function CustomHero() {
const fallback = { headline: "Software that meets the moment" };
const { state, ref } = useIsland({ campaignId: "camp_123", island: "hero", fallback });
// The hold reserves, and useIsland does the reserving: while the canvas
// waits, each blank field arrives already carrying its held copy, and
// StreamText keeps the geometry invisibly. Nothing to wire. The one
// question left to your markup is heldReserve's: a blank field with a
// reserve still occupies its space, so keep its element mounted.
return (
<section ref={ref} data-phase={state.phase}>
<StreamText field={state.fields.headline} as="h1" />
{state.fields.body.text || heldReserve(state.fields.body) ? (
<StreamText field={state.fields.body} as="p" />
) : null}
</section>
);
}

island is "hero" or "cta"; there are no other kinds. Every other option is the same as the props above.

Attach ref to the element that has to be visible. It is how impressions are counted. Leave it off and your campaign renders perfectly while reporting zero traffic. See Impressions and clicks.

state.fields.greeting, .headline, .body and .cta, each carrying:

Field propertyMeaning
textWhat to render right now. Never a placeholder.
source"generated", "cached" or "static" (your own copy).
typingLive text is currently landing.
settledThis text is final for the page view.
modeThe delivery mode the server declared.

state.rewrites counts reader-visible copy changes this page view: 0 until adapted copy lands, 1 after, never 2. The phase values are tabled in When things go wrong. Also on state: phase, theme, layout, backgroundImage, videoUrl, ctaAction, ctaStyle, sequence, contentSource, degraded, variantId, error, rewrites, meta, and held. held is the copy the hold pulled off the canvas per field, whatever was actually painted there, /content copy or your fallback. useIsland attaches it back onto each holding field so a blank field keeps occupying its exact space; heldReserve(field) reads the reservation when your markup needs to know.

An island carries four copy fields today. For copy of your own naming, any keys you like anywhere in your UI, reach for fragments.

<StreamText field={state.fields.headline} as="h1" className="display" />
PropWhat it does
fieldThe field object from state.fields.*. Not field.text.
asElement to render. Defaults to span.
smoothPaces entrances (live typing and composed landings alike). Defaults to true; pass false for wire text verbatim.
className, stylePassed through.

It stamps data-typing, data-settled and data-content-source on the element for styling and auditing, and announces changes politely to screen readers.

CopyCard and ProductCard render copy you already have, from your own CMS, catalogue or API call, and route it through the same provenance logic.

<ProductCard
product={{ id: "sku_1", name: "Trail Runner", price: "$120", imageUrl }}
contentSource="generated" // applies to the DESCRIPTION only
href="/products/trail-runner"
/>

Set contentSource from where the copy actually came from, never a guess; it defaults to static. Product names and prices are always treated as authored facts and never claim adapted provenance.

Style it with class hooks and custom properties

Section titled “Style it with class hooks and custom properties”

No CSS ships with the package. Campaign colours arrive as --st-* custom properties on the rendered element, so your stylesheet can consume them:

.st-hero { background: var(--st-background, #fff); }
.st-cta { background: var(--st-primary); color: var(--st-cta-text); }
.st-mark { color: var(--st-primary); opacity: 0.7; }
.st-stream-text[data-typing="true"]::after { content: "▍"; }

Outside React, themeStyle(state.theme) returns the same properties as a style object and applyTheme(element, state.theme) sets them on a node, returning a function that undoes it.