<!-- kernel -->
> Creatomate AI reference — topic page "Text elements, sizing & inline styles"; the index of all pages is https://creatomate.com/llms.txt
<!-- /kernel -->

### Text elements

| Property | Default | Type, units, range | Notes |
|---|---|---|---|
| `text` | `""` | string | may contain `{{Placeholder}}` / `{{Placeholder\|default}}` substitutions; `\n` forces a line break |
| `font_family` | `"Aileron"` | string | 1,100+ built-in families — the full list with weights is "Built-in fonts" (https://creatomate.com/llms/built-in-fonts.md) — plus custom fonts. A family that is not built in (Arial and Helvetica are not) renders as Aileron silently |
| `font_weight` | `"400"` | string | a string, e.g. `"700"`; a weight the family lacks falls back within the family |
| `font_style` | `"normal"` | string | `normal` \| `italic` |
| `font_size` | null | number; px, vw, vh, vmin, vmax; ≥ 0.1; nullable | never `%` (e.g. `"7 vmin"`); `null` = auto-fit between the minimum and the maximum |
| `font_size_minimum` | `"1 vmin"` | number; px, vw, vh, vmin, vmax; ≥ 0.1; nullable | auto-fit floor; at it a longer value overflows rather than shrinks |
| `font_size_maximum` | `"100 vmin"` | number; px, vw, vh, vmin, vmax; ≥ 0.1; nullable | auto-fit ceiling |
| `letter_spacing` | `"0%"` | number; %; ≥ -299 | % of the font size ÷ 10: `"1000%"` is one em, so CSS `0.05em` is `"50%"` |
| `line_height` | `"115%"` | number; %; ≥ 0 | % of the font size; a bare number is read as a percentage (`1.55` = 1.55 %, lines overprint), so write `"155%"` |
| `text_direction` | `"ltr"` | `ltr` \| `rtl` |  |
| `text_wrap` | `true` | boolean | false = never wrap |
| `text_clip` | `false` | boolean | true = cut overflow at the box with an "…" ellipsis |
| `text_transform` | `"none"` | `none` \| `capitalize` \| `uppercase` \| `lowercase` | applies to the whole value, units included (`m²` → `M²`), and the transformed string is wider |
| `background_color` | null | string; color; nullable | the text's own background box; it hugs the text and draws nothing for an empty value |
| `background_x_padding` | `"25%"` | number; %; ≥ 0 | % of the font size; the text is inset by the padding inside the element box, so a box must reserve text + 2 × padding |
| `background_y_padding` | `"25%"` | number; %; ≥ 0 | % of the font size |
| `background_border_radius` | `"0%"` | number; %; ≥ 0 | % of the font size |
| `background_align_threshold` | `"20%"` | number; %; ≥ 0 | the background is drawn per line; lines whose widths differ by less than this share of the element width get one box width. `"100%"` merges every line into one block instead of a stepped edge |
| `transcript_source` | null | string or keyframe array; nullable | the `name` of the audio or video element to caption, inline SRT/WebVTT, or your own word timings as a keyframe array — see "Subtitles" (https://creatomate.com/llms/subtitles.md) |
| `transcript_effect` | `"color"` | `color` \| `karaoke` \| `highlight` \| `fade` \| `bounce` \| `slide` \| `enlarge`; no keyframes | how the spoken unit is marked — see "Subtitles" (https://creatomate.com/llms/subtitles.md) |
| `transcript_split` | `"word"` | `none` \| `line` \| `word` \| `letter`; no keyframes | the unit the effect applies to |
| `transcript_placement` | `"static"` | `static` \| `animate` \| `align`; no keyframes |  |
| `transcript_maximum_length` | null | number; ≥ 1; nullable; no keyframes | characters per caption page; `1` shows one word at a time |
| `transcript_color` | `"#e74c3c"` | string; color; nullable | the colour of the spoken unit |
| `fill_color` | `"#000000"` | color, or an array of `{ "offset", "color" }` stops for a gradient | the text colour |
| `fill_mode` | `"solid"` | `solid` \| `linear` \| `radial`; nullable |  |
| `fill_x0` | `"50%"` | number; % |  |
| `fill_y0` | `"0%"` | number; % |  |
| `fill_x1` | `"50%"` | number; % |  |
| `fill_y1` | `"100%"` | number; % |  |
| `fill_radius` | `"50%"` | number; %; ≥ 0 |  |
| `stroke_color` | null | string; color; nullable |  |
| `stroke_width` | `"0.1 vmin"` | number; px, vw, vh, vmin, vmax; ≥ 0 | draws outward from the glyph edge; outline type is a stroke over a transparent fill |
| `stroke_cap` | `"round"` | `butt` \| `square` \| `round` |  |
| `stroke_join` | `"round"` | `miter` \| `bevel` \| `round` |  |
| `stroke_start` | `"0%"` | number; %; 0–100 |  |
| `stroke_end` | `"100%"` | number; %; 0–100 |  |
| `stroke_offset` | `"0%"` | number; % |  |

Text elements default `x_alignment`/`y_alignment` to `0%`/`0%` (top-left within their box) — that is why new text sits left-aligned; every other type defaults to 50%.

**`{{ }}` works only inside a text element's `text` value** and is meant for mixed static + dynamic text ("Hello {{name}}!"); for fully dynamic text, write realistic sample text and mark the element Dynamic instead. The full substitution rules live in "Modifications" (https://creatomate.com/llms/modifications.md).

**Text sizing has no mode property.** The editor's five "Size" modes derive from which of `font_size` / `width` / `height` are null:

| `font_size` | `width` | `height` | Behavior |
|---|---|---|---|
| set | null | null | fixed font; the element hugs the text (labels, buttons) |
| set | set | set | everything fixed; text wraps and can overflow (the classic complaint: automation inserted longer text than the design assumed) |
| null | set | set | font auto-fits the box between min/max — best for dynamic text |
| null | null | set | font fits the fixed height; width grows (matching lower-thirds) |
| null | set | null | columns: with both bounds declared the text takes the maximum and wraps at the width; with the default bounds it can stay at the 1 vmin minimum (see "How the engine lays text out" below) |

Editor gotcha: clicking a size-mode button resets `font_size_minimum`/`maximum` to defaults.

**Text-fit fixes (in order of likelihood):**

- Text overflows its box → `font_size: null` (auto-fit), or enlarge the box.
- Auto-fit text became tiny → long text drove it to the floor; raise `font_size_minimum` (≈ `"4 vmin"`).
- A line runs off the canvas → `text_wrap: true`.
- Per-render size differences → auto-fit working as designed with variable text lengths; not a bug. For a constant size use a fixed `font_size`, or set `font_size_minimum` equal to `font_size_maximum`.
- Words cut off → decide: wrap (`text_wrap: true`) or clip with ellipsis (`text_clip: true`).
- **Keep it on one line:** `text_wrap: false`. For dynamic text add `font_size: null` + a fixed `width` — the font shrinks until the text fits one line. Widening the box is not a fix; the next longer value wraps again. (Short static text can simply drop `width` to null — the element hugs the text and never wraps.)

### Inline text styles

Any text value supports square-bracket spans: `[family Name]…[/family]`, `[weight 700]…[/weight]`, `[style italic]…[/style]`, `[size 150]…[/size]` (% of the element's `font_size`), `[color #ff0000]…[/color]`, `[opacity 50]…[/opacity]`. Spans nest (innermost wins) and work inside modification values too. Unknown tags render as literal text. Custom fonts are referenced by exact family name — see "Custom fonts" (https://creatomate.com/llms/fonts.md).

### How the engine lays text out

- **Wrapping** happens only at spaces (and at CJK break points); the engine never hyphenates, and a hyphen is not a break opportunity. A word wider than its box runs out of it; a no-break space (U+00A0) keeps words together. A character the font lacks draws a box: there is no Latin fallback font, so avoid unusual code points such as the non-breaking hyphen (U+2011) and use a plain hyphen.
- **Auto-fit in a fixed box** (`font_size: null` with `width` and `height` set): the engine wraps the text at `font_size_minimum`, then grows the size in 100 steps toward `font_size_maximum`, re-wrapping at every step, and keeps the largest size whose wrapped block still fits the box height with 10 % headroom (so give a box about 10 % more height than the text needs, or the text lands about 10 % under the intended size). The line count follows the box, not the text: the scan stops at the first size where the wrap no longer fits, so a headline meant to stack comes out as one line in a box too short for two lines and as four lines in a tall square (render-checked); force a layout with `\n` or a fixed `font_size`. Text never goes below the minimum: at the floor a longer value overflows, so size every box for the longest realistic value. Each text fits alone; sizes are never shared between elements.
- **Auto-fit with a hugging height** (`font_size: null`, `width` set, `height: null`): the rule depends on the bounds. With the default bounds the text is wrapped at the minimum and scaled until its widest line fills the width, so a text that already wraps at the minimum stays there, and the default minimum is 1 vmin: unreadable (render-checked). Declare both `font_size_minimum` and `font_size_maximum` and the text takes the maximum and wraps at the width, giving size up only when a single word is wider than the box; with no width either, it is the maximum on one line. Always declare both bounds on a hugging auto-fit text.
- **Vertical metrics** follow the font, not CSS: the first baseline sits at the top of the box plus the font's ascender, and `line_height` only spaces the lines below it. Some faces draw taller lines than others at the same `line_height`. Kerning applies.
- **Stroke and background**: `stroke_width` on text draws outward from the glyph edge (about twice a CSS text-stroke for the same look); outline type is a stroke over a transparent fill. The background box hugs the text on both axes and follows each line: a line's box is the font's ascent to descent plus the padding, so `line_height` changes the space between line boxes rather than their height; the box takes the element's shadow and draws nothing at all for an empty value — a clean empty state. Text has no strikethrough, underline, gradient fill or path: "Out of scope" (https://creatomate.com/llms/out-of-scope.md).

### How it is used

The engine forms behind Creatomate's own templates; values are starting points. Check every move before rendering ("Seeing a render" (https://creatomate.com/llms/seeing-renders.md)).

- **Text that adapts to any value** (a fixed box, the font shrinks to fit): `"font_size": null` with `width` and `height` set, bounded by `font_size_minimum`/`font_size_maximum`. Give the box about 10 % more height than the text needs and size it for the longest realistic value: at the floor a longer value overflows.
- **A label, chip or pill** (the background hugs the text; an empty value draws nothing):
  ```json
  { "type": "text", "name": "Badge", "x": "92%", "y": "8%", "x_anchor": "100%", "y_anchor": "0%",
    "text": "NEW", "font_size": "3.5 vmin", "font_weight": "700", "fill_color": "#ffffff",
    "background_color": "#ff4757", "background_x_padding": "80%", "background_y_padding": "40%", "background_border_radius": "50%" }
  ```
  A wrapped chip becomes a stepped edge; `"background_align_threshold": "100%"` merges the lines into one block.
- **Outline type**: a text stroke over a transparent fill: `"fill_color": "rgba(0,0,0,0)", "stroke_color": "#ffffff", "stroke_width": "0.4 vmin"`. The stroke draws outward, so use about twice a CSS text-stroke width.
- **A struck-through "was" price** (there is no strikethrough): a second text with the same `name`, position and font, transparent glyphs (`"fill_color": "rgba(0,0,0,0)"`) and its own background box in the rule colour, `"background_x_padding": "0%"`, `"background_y_padding": "0%"`, `"y_scale": "8%"`. Give both `"y_anchor": "50%"`: the squash happens toward the anchor, so a top anchor puts the rule above the price. Both hug the value, so the rule always fits it, and one modification by name fills both.
- **A stacked headline that must stay stacked**: `\n` in the text, or a fixed `font_size`. Auto-fit decides the line count at the minimum size and can scale a two-line design into one line.
- **Sizes that survive a format change**: font sizes, paddings and reserved heights in `vmin`, because the short side is the same in 9:16, 1:1 and 16:9.
