slug

Template Expressions

Use {{ expression }} syntax in layer properties (content, src, color, etc.) to make templates dynamic. Expressions can reference brand tokens, variables, and call helper functions.

Fallbacks for optional brand assets

Brand tokens (logos, colors, images) may not always be defined. Use use or isDefined to provide fallbacks so templates work when assets are missing.

use (fallback chain)

Returns the first defined (non-null, non-undefined) value. Use for optional logos, colors, or images. Use function call syntax with parentheses and commas.

{{ use(brand.logos.logo, brand.logos.icon) }}
{{ use(brand.colors.primary, "#333333") }}
{{ use(brand.images.background, "https://picsum.photos/1200/630") }}

If brand.logos.logo is defined, it's used. Otherwise brand.logos.icon. If both are empty, returns "".

isDefined (alias)

Same as use — returns the first defined argument.

{{ isDefined(brand.logos.logo, default.logo) }}

Math functions

FunctionExample
min{{ min(a, b) }}
max{{ max(a, b) }}
abs{{ abs(x) }}
round, ceil, floor{{ round(3.7) }}
clamp{{ clamp(value, 0, 100) }} — clamps value between min and max

Calendar & date helpers

Month arguments are 1–12. Calculations use UTC. You cannot write array literals ([ ]) inside expressions; use the helpers below.

Loop source vs {{ }} in layer fields

Where you use itWhat happens
Loop source (repeater “Source” field)The resolved value must be an array (or object). Put a variable path, a bare helper call (monthDayNumbers(year, month)), or one whole {{ expression }} there. Each item drives one iteration.
{{ … }} in content, src, colors, etc.The result is turned into a string for display. Arrays become comma-separated text (usually not what you want for a full list—use a repeater instead). Numbers and short strings render cleanly.

Returns an array (for Loop source—or nested inside another helper)

Use these as the repeater source, or inside another call (e.g. range(1, daysInMonth(year, month)) uses the scalar daysInMonth inside an array helper).

FunctionReturnsExample Loop sourceNotes
range(count)[0, …, count - 1]range(7)Seven columns (indices 0–6).
range(start, end)Inclusive integersrange(1, 31)Often combined with daysInMonth (below).
monthDayNumbers(year, month)[1, …, lastDay]monthDayNumbers(2026, 5) or monthDayNumbers(year, month)One iteration per day in that month.
yearRange(startYear, endYear)Sorted inclusive yearsyearRange(2020, 2030)Year picker–style lists (length capped).
calendarMonthSlots(year, month, weekStartsOn?)42 entriescalendarMonthSlots(2026, 5, 1)null = empty cell; 1–31 = day of month. weekStartsOn: 0 Sun … 6 Sat; omit or use 1 for week starting Monday.
monthNames()12 long namesmonthNames()Header row for months (iterate item + itemIndex).
monthNamesShort()12 short namesmonthNamesShort()Compact month labels.
weekdayNames()7 long namesweekdayNames()Sun–Sat labels.
weekdayNamesShort()7 short namesweekdayNamesShort()Matches well with range(7) row layout if order aligns with weekStartsOn.

Repeater source examples (bare form; wrapped {{ … }} with the same inner expression is equivalent):

range(7)
monthDayNumbers(2026, 5)
monthDayNumbers(year, month)
calendarMonthSlots(2026, 5, 1)
calendarMonthSlots(year, month, 1)
yearRange(2020, 2030)
weekdayNamesShort()

Inside repeated layers, use your item variable (e.g. day, slot) and dayIndex / slotIndex as in the editor binding reference.

Returns a single value (for {{ }} text/numbers—or inside array helpers)

FunctionReturnsExample in {{ }}
daysInMonth(year, month)Number of days (integer); 0 if invalid{{ daysInMonth(2026, 5) }}31
daysInAMonth(year, month)Same as daysInMonth{{ daysInAMonth(year, month) }}

Scalar inside an array helper (still one expression; no literal [ ]):

range(1, daysInMonth(year, month))

That produces an array from 1 through the last day—useful as a Loop source when monthDayNumbers(year, month) would do the same more directly.

What not to expect from {{ arrayHelper() }}

Expressions like {{ monthDayNumbers(2026, 5) }} or {{ weekdayNamesShort() }} evaluate to arrays but display as a single string (values joined with commas). For UI layouts, point the Loop source at the same helper (or a variable holding the array), not only {{ }} on one text layer.

Layer repeaters still accept variable paths (items, users.list) when they resolve to a non-null value—same resolution order as documented in the editor.

Number parsing (currency / formatted strings)

When prices come in as strings like "$1,234.50", "₦12,500", or "(1,200)" you can convert them to plain numbers with toNumber (alias parseNumber):

FunctionReturnsExample
toNumber(value)Parsed number, or 0 if not parseable{{ toNumber('$1,234.50') }}1234.5
toNumber(value, fallback)Parsed number, or fallback if not parseable{{ toNumber(price, 0) }}
parseNumber(value)Alias for toNumber{{ parseNumber('₦12,500') }}12500

What gets stripped before parsing:

  • Currency symbols: $ € £ ¥ ₦ ₹ ₽ ₩ ¢ ฿ ₪ ₫ ₱ ₴ ﷼ and trailing/leading codes like kr, USD, EUR, GBP, JPY, NGN, INR, RUB, KRW, CNY, CAD, AUD, CHF
  • Thousands commas (en-US convention): "1,234,567.89"1234567.89
  • Spaces, including narrow no-break (\u202F) and thin (\u2009)
  • Trailing percent: "50%"50
  • Accounting parentheses for negatives: "(1,200)"-1200

sumItems already applies this normalization internally, so {{ sumItems(details.items, 'price.new') }} works even when price.new is "$1,234.50". Use toNumber explicitly when you need a single value (e.g. multiplying by a tax rate).

{{ toNumber(invoice.subtotal) * 1.1 }}                     // works even if subtotal is "$199.99"
{{ toNumber(invoice.discount, 0) }}                        // safe default when blank
{{ round(toNumber(price.new) - toNumber(price.discount)) }}

Strings using , as the decimal mark (e.g. "1.234,56") are not auto-converted — they're ambiguous against the en-US thousands convention. Normalize on input or replace the comma yourself first.

Array reducers (sums, joins)

For invoice-style layouts and lists where you need a total or a single string built from a repeater's source, use sumItems and joinItems. Both take the same array you would use as a loop.source (so reach for them via the variable path, e.g. details.items).

FunctionReturnsExample
sumItems(items)Total of numeric items{{ sumItems(amounts) }} → sum of a number[]
sumItems(items, path)Total of path read from each item{{ sumItems(details.items, 'price.new') }}
joinItems(items)Items joined with ", "{{ joinItems(tags) }}
joinItems(items, path)path from each item, joined with ", "{{ joinItems(speakers, 'name') }}
joinItems(items, path, sep)Custom separator{{ joinItems(speakers, 'name', ' • ') }}

Aliases sumLoopItems and joinLoopItems exist for parity with the loop.source mental model — they take the exact same arguments.

Rules:

  • Non-array inputs return 0 (sum) or "" (join).
  • null, undefined, and empty-string values are skipped — so a missing field never produces ", , " gaps.
  • sumItems accepts currency-formatted strings out of the box ("$1,234.50", "₦12,500", "(1,200)" for negatives). See Number parsing for the exact rules; truly non-numeric values are skipped.
  • path is a dot path ("price.new", "address.city"); omit it when the array holds primitives directly.

Common patterns:

Total: {{ sumItems(details.items, 'price.new') }}
Net:   {{ sumItems(details.items, 'price.new') - sumItems(details.items, 'discount') }}
Tags:  {{ joinItems(tags) }}
Hosts: {{ joinItems(speakers, 'name', ' • ') }}

You can wrap reducers in other safe functions:

{{ round(sumItems(details.items, 'price.new') * 1.1) }}    // 10% tax, rounded
{{ clamp(sumItems(orders, 'qty'), 0, 999) }}                // sum capped to 3 digits

Color functions (chroma-js)

All color functions accept hex, named colors, or brand token paths like brand.colors.primary.

Light / dark

FunctionDescriptionExample
lightenLighten a color{{ lighten(brand.colors.primary, 1.5) }}
darkenDarken a color{{ darken(brand.colors.primary, 2) }}
saturateIncrease saturation{{ saturate(brand.colors.accent, 2) }}
desaturateDecrease saturation{{ desaturate(brand.colors.primary, 1) }}
getLuminance0 (black) to 1 (white){{ getLuminance(brand.colors.primary) }}
isLightTrue if luminance > 0.5{{ isLight(brand.colors.primary) }}
isDarkTrue if luminance < 0.5{{ isDark(brand.colors.primary) }}

Mixing and blending

FunctionDescriptionExample
mixMix two colors{{ mix(brand.colors.primary, brand.colors.secondary, 0.3) }}
shadeMix with black{{ shade(brand.colors.primary, 0.5) }}
tintMix with white{{ tint(brand.colors.primary, 0.5) }}
blendBlend modes (multiply, darken, lighten, screen, overlay, burn, dodge){{ blend(c1, c2, "multiply") }}
averageAverage of colors{{ average([c1, c2, c3]) }}

Scales and gradients

FunctionDescriptionExample
scaleColorsColor at position (0–1) or array of n colors{{ scaleColors([brand.colors.primary, brand.colors.secondary], 0.5) }}
scaleColorsGet 5 colors from gradient{{ scaleColors(["#ff0000","#0000ff"], 5) }}

Other color helpers

FunctionDescriptionExample
alphaSet opacity (0–1){{ alpha(brand.colors.primary, 0.5) }}
luminanceSet luminance (0–1){{ luminance(brand.colors.primary, 0.5) }}
contrastWCAG contrast ratio (4.5+ for text){{ contrast(brand.colors.primary, brand.colors.secondary) }}
hexGet hex from any color{{ hex(brand.colors.primary) }}
colorCssGet rgb/rgba string{{ colorCss(brand.colors.primary) }}
colorValidCheck if valid color{{ colorValid(brand.colors.primary) }}
temperatureColor from Kelvin (2000=candle, 6500=daylight){{ temperature(3500) }}

Brand token paths

PathUse case
brand.logos.logo, brand.logos.iconImage src for logos
brand.logos.logoDark, brand.logos.logoLightLight/dark variants
brand.colors.primary, brand.colors.secondaryColors
brand.colors.darkTitleText, brand.colors.lightTitleTextText on dark/light backgrounds
brand.typography.heading, brand.typography.bodyFont families
brand.images.background, brand.images.pictureHero/background images
brand.copy.tagline, brand.copy.descriptionText content

Best practices

  1. Always use fallbacks for optional assets{{ use(brand.logos.logo, brand.logos.icon) }} so the template works when a logo isn't set.
  2. Use isLight/isDark for text contrast — Pick light or dark text based on background: {{ isDark(brand.colors.primary) ? "#ffffff" : "#000000" }} (if your expression syntax supports ternaries).
  3. Use contrast() — Ensure text readability: aim for 4.5:1 or higher.
  4. scaleColors for gradients — Generate consistent color ramps from brand colors.