Theming from content
A theme block lets marketers change your app's accent colours without a release: a seasonal palette for Ramadan, a campaign colour, a different look for one segment. The theme is an ordinary ContentFlow block whose fields hold colour values. The JavaScript SDK has a small helper that turns those values into typed colours with fallbacks; the native SDKs read the same fields with their normal block API. This is a colour reader, not a UI framework: there are no components, no CSS-in-JS and no design tokens.
- Define a block type such as
brand_themewith onetextfield per colour (#brand_primary,#brand_accent), holding values like#5b57e0. - In React and React Native,
useCFTheme('brand_theme', mapping, { fallback })returns typed colours and re-renders when the block changes; in plain JavaScript usethemeFromBlockorgetBlockColor. - The theme helpers exist only in the JavaScript SDK: in Swift, Kotlin and Flutter read the colour fields as text and parse them yourself.
- Always ship a fallback colour per slot: the block may not be delivered yet, or a value may not parse.
How it works
- The theme is a block like any other: it has instances, translations, segment targeting, a schedule and A/B tests (see Blocks: types, instances, collections). The first eligible instance for the device is the theme.
- Colours are values, not a field type. The dashboard has no colour field type: use
textfields. A field declared with any other type is stored and delivered, but the Dynamic Blocks editor shows it as an unsupported field that can't be edited. - The JavaScript helpers parse
#rgb,#rgba,#rrggbb,#rrggbbaa, andrgb()/rgba()in comma or space form, with channels as 0–255 or percentages. Anything else reads as "no colour" and the fallback is used; they never throw. - Reading a theme records no impression: it isn't the same as showing the block's content.
#rrggbb valuesSix-digit hex reads the same on every platform. CSS and the JavaScript helpers read #rrggbbaa with the alpha last, while Android's Color.parseColor reads an 8-digit value as #aarrggbb, alpha first. If marketers can enter alpha, decide on one form and parse it yourself.Define the theme block
Register the block type once from a server or CI job with the write key (never from the app); see Blocks and fields: register one block type. Marketers then edit its instance in Dynamic Blocks and publish it like any other content.
curl -X POST https://api.contentflow.click/sdk/v1/cards/register \
-H "X-CF-Write-Key: $CF_WRITE_KEY" -H 'Content-Type: application/json' \
-d '{
"key": "brand_theme",
"name": "Brand theme",
"screen": "App-wide",
"fields": [
{ "id": "primary", "tag": "#brand_primary", "type": "text", "label": "Primary colour (#rrggbb)" },
{ "id": "accent", "tag": "#brand_accent", "type": "text", "label": "Accent colour (#rrggbb)" }
],
"blank": { "primary": "#5b57e0", "accent": "#f0654a" }
}'
Tags ending in _color or _colour, or starting with #color_, are also picked up by getBlockColors (see API reference). Any tag works with getBlockColor, themeFromBlock and useCFTheme.
Read the theme
import { themeFromBlock } from '@contentflow/sdk/web';
// 1. Map each theme slot to the tag that carries it, with a bundled fallback per slot.
const MAPPING = { primary: '#brand_primary', accent: '#brand_accent' };
const FALLBACK = { primary: '#5b57e0', accent: '#f0654a' };
// 2. Read the brand_theme block (undefined until it is delivered) and resolve the colours.
function applyTheme() {
const theme = themeFromBlock(client.getBlock('brand_theme'), MAPPING, FALLBACK);
const root = document.documentElement.style;
root.setProperty('--brand-primary', theme.colors.primary?.hex ?? FALLBACK.primary);
root.setProperty('--brand-accent', theme.colors.accent?.hex ?? FALLBACK.accent);
}
// 3. Apply it now, and again whenever content changes.
applyTheme();
client.subscribe(applyTheme);import { useCFTheme } from '@contentflow/sdk/react';
function AppShell({ children }: { children: React.ReactNode }) {
// 1-2. Map slots to tags, with a fallback per slot. Re-renders when the block changes (step 3).
const { theme } = useCFTheme(
'brand_theme',
{ primary: '#brand_primary', accent: '#brand_accent' },
{ fallback: { primary: '#5b57e0', accent: '#f0654a' } },
);
// theme.colors.* has a value even before the block arrives, thanks to the fallback.
const style = { '--brand-primary': theme.colors.primary?.hex, '--brand-accent': theme.colors.accent?.hex } as React.CSSProperties;
return <div style={style}>{children}</div>;
}import { View } from 'react-native';
import { useCFTheme } from '@contentflow/sdk/react-native';
function Header() {
// 1-2. Map slots to tags, with a fallback per slot. Re-renders when the block changes (step 3).
const { theme } = useCFTheme('brand_theme', { primary: '#brand_primary' }, {
fallback: { primary: '#5b57e0' },
});
return <View style={{ backgroundColor: theme.colors.primary?.hex ?? '#5b57e0', height: 56 }} />;
}Not available in the Swift SDK: there is no theme helper or colour parser. Read the colour fields as text with the normal block API and parse them yourself:
// 1. A bundled fallback per slot.
let fallbackPrimary = "#5b57e0"
// 2. Read the brand_theme block (nil until delivered) and its colour field as text.
let block = try await client.fetchBlock(key: "brand_theme")
let primaryHex = (try? block?.value(for: FieldKey<String>.text("#brand_primary"))) ?? fallbackPrimary
// 3. Parse it with your own hex helper (SwiftUI and UIKit have no hex initialiser),
// and read again when the block changes (client.blockState(for: "brand_theme")).
let primary = Color(hex: primaryHex) ?? Color(hex: fallbackPrimary)!Not available in the Kotlin SDK: there is no theme helper or colour parser. Read the colour fields as text with the normal block API and parse them yourself:
// 1. A bundled fallback per slot.
const val FALLBACK_PRIMARY = "#5b57e0"
// 2. Read the brand_theme block (null until delivered) and its colour field as text. From a coroutine.
val block = client.fetchBlock("brand_theme")
val primaryHex = block?.value(FieldKeys.text("#brand_primary")) ?: FALLBACK_PRIMARY
// 3. Parse it (Android reads 8 digits as #aarrggbb), and read again when the block
// changes (client.observeBlock("brand_theme")).
val primary = runCatching { android.graphics.Color.parseColor(primaryHex) }
.getOrDefault(android.graphics.Color.parseColor(FALLBACK_PRIMARY))Not available in the Flutter SDK: there is no theme helper or colour parser. Read the colour fields as text with the normal block API and parse them yourself:
final cf = ContentFlow.instance;
// 1. A bundled fallback per slot.
const fallbackPrimary = Color(0xFF5B57E0);
// 2. Read the brand_theme block (null until delivered) and its colour field as text.
final hex = cf.getBlock('brand_theme')?.field<String>('#brand_primary');
// 3. Parse #rrggbb yourself, and read again when the block changes
// (cf.observeBlock('brand_theme', (block) => ...)).
final rgb = (hex != null && hex.length == 7) ? int.tryParse(hex.substring(1), radix: 16) : null;
final primary = rgb != null ? Color(0xFF000000 | rgb) : fallbackPrimary;The JavaScript helpers are exported from @contentflow/sdk/web, /react, /react-native and /advanced; useCFTheme from /react and /react-native. They are not exported from the root @contentflow/sdk entry.
API reference (JavaScript)
| Function | Returns | Notes |
|---|---|---|
parseCFColor(value) | CFColor | undefined | Parses one value. CFColor is { hex, rgba: { r, g, b, a }, raw }; hex is lowercase #rrggbb, or #rrggbbaa when alpha is below 1. Also unwraps { hex } or { value } one level. |
getBlockColor(block, tag) | CFColor | undefined | Reads one field by tag (with or without #), whatever its declared type. |
getBlockColors(block) | Record<tag, CFColor> | Every parseable value whose tag ends in _color or _colour, or starts with #color_. Other tags (such as #brand_primary) aren't found this way: use getBlockColor. |
themeFromBlock(block, mapping, fallback?) | { colors, source } | Each slot in mapping resolves through getBlockColor, else its fallback; a slot with neither is absent. block may be undefined. source holds the block key, instance id and version. |
useCFTheme(blockKey, mapping, { fallback }) | { theme, block, loading, error } | Built on useCFBlock, with no impression. loading is true until a block for the key has been seen, and can't tell "still fetching" from "no live block". error is always undefined. There is no locale option. |
Fallbacks and live updates
- First launch: until the first sync delivers the block, only your fallbacks apply. Paint with them; don't wait for the network.
- Later launches: the cached block paints first (see Blocks: refresh and offline).
- Changes: a published change reaches connected apps within about a second;
useCFThemere-renders, and plain JavaScript re-runs yoursubscribecallback. - Per segment or season: target an instance to a segment, or schedule it (see Blocks: schedules). A hidden instance (
#block_enabledfalse) is skipped bygetBlockin JavaScript, so the fallbacks apply.
When it doesn't work
| What you see | Why, and the fix |
|---|---|
| The app always shows the fallback colours | No live instance of brand_theme reaches this device (not published, targeted to another segment, outside its schedule), or the value doesn't parse (a name such as purple, or a stray character). Check the instance in Dynamic Blocks and use #rrggbb. |
| The colour field can't be edited in the dashboard | It was registered with a type the editor doesn't support (such as color). Register it as text. |
getBlockColors returns an empty object | Your tags don't end in _color or _colour or start with #color_. Read them with getBlockColor or a mapping. |
useCFTheme stays loading | No block for that key has been delivered. The fallbacks are already in theme.colors, so render with them instead of waiting. |
| Android shows a different colour from iOS or web for an 8-digit value | Color.parseColor reads 8 digits as #aarrggbb. Store #rrggbb. |
Next
- Dynamic blocks: publishing, targeting and scheduling the theme instance.
- Blocks and fields: field types: the field types the dashboard edits.