Skip to content
Flow-UILive
On this page

UI schema atoms: text, color, image, font

Atoms are the reusable JSON shapes behind widgets: TextData, ColorData, ImageData, FontData. Dark variants live on the color atom.

Ayush MishraPublished 6 min read

Atoms are the reusable JSON shapes behind widgets: TextData, ColorData, ImageData, FontData. Dark variants live on the color atom. Without atoms every widget invents titleColor, heading_tint, and labelHex, and theming becomes folklore.

The atoms API is the field list. This article is why you reuse them in custom widgets instead of rolling String everywhere.

The Flow-UI mark: two nodes joined through a filled centre

TextData#

Bare string "Hello" or an object with text, font, color, alignment, max_lines, is_markdown. Alignment is leading, center, trailing. Markdown is inline, not a document renderer. Unlimited lines when max_lines is omitted or 0.

Use FlowText in the view so atoms resolve through the theme. Raw Text(content.title) throws away font and color the backend sent.

TitleUse.swiftswift
var body: some View {
    FlowText(content.headline)
}

Custom widgets should take TextData for anything a designer might restyle later. A raw String is for identifiers, not copy.

swift
let orderNumber: TextData

ColorData#

Hex (#RRGGBB or #AARRGGBB with alpha first), optional dark_hex, optional token, optional alpha. Resolution: token if the theme understands it, else hex plus dark hex. Token wins over hex on purpose so a design system can override a leftover hex in JSON.

Send dark_hex on surfaces. The default theme makes a dynamic color. One payload, two appearances. You do not fetch a second page for dark mode.

FontData#

Size and weight, or a theme token, matching the theming doc. Dynamic Type still depends on how the view applies the font. Atoms describe intent. SwiftUI does the scale when you use the provided text view.

ImageData#

URL (or the documented image fields), plus alt. Accessibility is not optional copy. A decorative image can be marked as such if the atom supports it; otherwise send alt. Native SDUI does not invent alt from the filename.

Images load through host FlowImageLoader. The atom is data. The loader is a seam. Caching policy is yours.

Atoms versus layout versus theme#

Layout (padding, margin, corners) is chrome around a widget. Atoms are paint inside the widget. Theme is how tokens and hex become Color. Mixing a hex into layout.background is correct (that field is ColorData). Mixing a one-off bg string on data that is not ColorData is how dark mode breaks on one widget.

Custom atoms#

You can add product-specific structs (PriceData) in the app. Prefer composing existing atoms (amount: TextData, strikethrough: TextData) until you are sure you need a new shape. Every new atom is a second decoder for backend to get wrong.

ImageData in full#

url is required. Bare string shorthand is a URL. aspect_ratio is width over height and reserves the shape before load. scale_mode is fill (crop) or fit. corner_radius rounds the image itself, separate from widget layout corners. placeholder_color is ColorData. shimmer (JSON) maps to showShimmer. alt is announced by VoiceOver. No alt means decorative: hidden from assistive technology. That is the right default for a backdrop and the wrong default for a product photo. Send alt when the image carries meaning.

Images load through a host image loader. The atom is data. Caching is yours. The package does not fetch the page, and it does not pretend to be your CDN.

ColorData resolution order#

Token first when the theme understands it, then hex with dark_hex, then nothing. Eight-digit hex is #AARRGGBB (alpha first). Bare string shorthand is hex. alpha multiplies after resolution. Send dark_hex on surfaces if you are not on tokens. One payload, two appearances. You do not fetch a night page.

FontData#

size plus weight, or token. Token wins when both are present. Weights in the model comments: regular, medium, semibold, bold, heavy. Unknown weight falls back to regular. Dynamic Type still depends on using FlowText (or applying the resolved font in a way that scales). A raw Text with a fixed size throws the atom away.

TextData details#

alignment: leading, center, trailing, also left and right. max_lines omitted or 0 means unlimited. is_markdown parses inline Markdown, not a document and not HTML. Bare string shorthand is the common authoring path.

Skip atoms in a prototype and you will rewrite the widget. Start with TextData anyway. It accepts a bare string. Markdown on text is not a WebView.

Read atoms and theming. The schema article is the layer above this one.

Why atoms beat per-widget color keys#

Without atoms, title_block grows titleColor, the image text type grows heading_tint, and a host widget grows labelHex. Dark mode becomes a meeting. ColorData puts hex, dark_hex, token, and alpha in one place. ThemeProvider resolves them. The default theme turns a hex pair into a dynamic color. Apps with a design system implement the protocol once. Every custom widget that stored TextData instead of String gets tokens for free.

Accessibility rides on atoms#

ImageData.alt is the VoiceOver string. Missing alt hides the image from assistive technology. Native SDUI does not scrape a filename. TextData scales when you render it through FlowText. Button traits for envelope taps live on WidgetRowView, not on the atom, but the copy the user hears still came from TextData.text.

Install a theme at the root with FlowPageView(store: store).flowTheme(MyTheme()). The theming concept has the protocol and ThemeDefaults.

UI schema atoms are text, color, image, font. Reuse them. Put dark mode on dark_hex or tokens, not on a second payload.

FontData token wins over size and weight when both are sent. ColorData token wins over hex when the theme understands the token. Authors should pick one layer: tokens for a design system, hex plus dark_hex for a team without tokens. Mixing leftover hex with a token you then ignore in a custom theme is how two widgets disagree at night.

Shorthand is for authors, objects are for control#

Let backend authors send "Hello" and "#4F8CFF" and a bare image URL when that is enough. Switch to objects the day you need max_lines, dark_hex, alt, or a font token. Both decode as the same Swift structs. A prototype that starts with objects everywhere is slower to author and no safer. A prototype that starts with String everywhere will be rewritten when dark mode lands.

Icon and button atoms exist too (IconData, ButtonData) and follow the same idea: structured fields, optional actions, colors as ColorData. This article's primary four are the ones every custom widget should reach for first. If you invent PriceData, compose TextData until you are sure the price needs its own decoder.

Theme defaults fill unspecified text color, accent, surface, separator, corner radius, and body font. Tune ThemeDefaults so an unstyled payload still looks like your app. Atoms plus theme is how server driven UI stays native instead of looking like a dump of hex. Send alt on meaningful images. Send dark_hex on surfaces. Reuse TextData for copy. That is the whole habit. Custom widgets that ignore atoms will reinvent dark mode badly. Custom widgets that use them will look like the rest of the page. That is why atoms exist: one shape, many widgets, one theme.

More on the blog.

Read the source

Flow-UI is MIT licensed. The schema, renderer and starter widgets live on GitHub.

GitHub

Get started with Flow-UI

Install the Swift package, register a widget, and render a page from JSON.

Get started