Home / Guides / Theming from content
Product guide

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.

In short
  • Define a block type such as brand_theme with one text field 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 use themeFromBlock or getBlockColor.
  • 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.
Concept

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 text fields. 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, and rgb() / 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.
Use opaque #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.
Setup

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.

In the app

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.

Reference

API reference (JavaScript)

FunctionReturnsNotes
parseCFColor(value)CFColor | undefinedParses 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 | undefinedReads 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.
In the app

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; useCFTheme re-renders, and plain JavaScript re-runs your subscribe callback.
  • Per segment or season: target an instance to a segment, or schedule it (see Blocks: schedules). A hidden instance (#block_enabled false) is skipped by getBlock in JavaScript, so the fallbacks apply.
Troubleshooting

When it doesn't work

What you seeWhy, and the fix
The app always shows the fallback coloursNo 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 dashboardIt was registered with a type the editor doesn't support (such as color). Register it as text.
getBlockColors returns an empty objectYour tags don't end in _color or _colour or start with #color_. Read them with getBlockColor or a mapping.
useCFTheme stays loadingNo 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 valueColor.parseColor reads 8 digits as #aarrggbb. Store #rrggbb.

Next