US
EN

Schema Controls Reference

Every schema control available in Fluid themes.

Category

Patterns

How controls combine for common section shapes. Copy the full recipe, then tune — these are the defaults every section should start from.

Section Shell + Container

17 settings · Container (9) + Section Shell (6) + two composition groups

The baseline every section ships with. Container (9 settings) is a nested shell with its own max-width, padding, radius, background color/image, overlay, and border. Section Shell (6 settings) wraps it with outer padding, radius, background, and border. Two nested shells give editors full control — outer can be full-bleed with a colored bg, inner can be a bounded card with its own styling.

Fluid builder renders
Container
Max Width
Default (1280px)
Container Padding
0
64
0
64
Container Border Radius
0
0
0
0
Container Background Color
Transparent
Container Background Image
📁
Select or Upload image
Container Overlay Color
Transparent
Container Overlay Opacity
0
Container Border Width
0
Container Border Color
Primary
Section Shell
Section Padding
80
0
80
0
Section Border Radius
0
0
0
0
Background Color
Primary
Background Image
📁
Select or Upload image
Section Border Width
0
Section Border Color
Primary
Schema json
{
  "settings": [
    { "type": "header", "content": "Container" },
    { "type": "select", "id": "container_max_width", "label": "Max Width", "default": "1280px",
      "options": [
        { "value": "960px",  "label": "Narrow (960px)" },
        { "value": "1280px", "label": "Default (1280px)" },
        { "value": "1440px", "label": "Wide (1440px)" },
        { "value": "100%",   "label": "Full (100%)" }
      ] },
    { "type": "padding", "id": "container_padding", "label": "Container Padding" },
    { "type": "corner_radius", "id": "container_border_radius", "label": "Container Border Radius" },
    { "type": "select", "id": "container_background_color", "label": "Container Background Color",
      "options": "background_colors", "default": "transparent" },
    { "type": "image_picker", "id": "container_background_image", "label": "Container Background Image" },
    { "type": "select", "id": "container_overlay_color", "label": "Container Overlay Color",
      "options": "background_colors", "default": "transparent" },
    { "type": "range", "id": "container_overlay_opacity", "label": "Container Overlay Opacity",
      "min": 0, "max": 100, "step": 5, "default": 0, "unit": "%" },
    { "type": "range", "id": "container_border_width", "label": "Container Border Width",
      "min": 0, "max": 10, "step": 1, "default": 0, "unit": "px" },
    { "type": "select", "id": "container_border_color", "label": "Container Border Color",
      "options": "background_colors", "default": "var(--clr-primary)" },

    { "type": "header", "content": "Section Shell" },
    { "type": "padding", "id": "section_padding", "label": "Section Padding" },
    { "type": "corner_radius", "id": "section_border_radius", "label": "Section Border Radius" },
    { "type": "select", "id": "background_color", "label": "Background Color",
      "options": "background_colors", "default": "transparent" },
    { "type": "image_picker", "id": "background_image", "label": "Background Image" },
    { "type": "range", "id": "section_border_width", "label": "Section Border Width",
      "min": 0, "max": 10, "step": 1, "default": 0, "unit": "px" },
    { "type": "select", "id": "section_border_color", "label": "Section Border Color",
      "options": "background_colors", "default": "var(--clr-primary)" }
  ]
}
Liquid wire-up liquid
{%- style -%}
  .my-section.section-{{ section.id }} {
    {%- assign p = section.settings.section_padding -%}
    {%- if p -%}padding: {{ p.top }}px {{ p.right }}px {{ p.bottom }}px {{ p.left }}px;{%- endif -%}
    {%- assign r = section.settings.section_border_radius -%}
    {%- if r -%}border-radius: {{ r.tl }}px {{ r.tr }}px {{ r.br }}px {{ r.bl }}px;{%- endif -%}
    {% if section.settings.section_border_width > 0 %}
      border: {{ section.settings.section_border_width }}px solid {{ section.settings.section_border_color }};
    {% endif %}
    background-color: {{ section.settings.background_color | default: 'transparent' }};
    {%- if section.settings.background_image != blank -%}
      background-image: url({{ section.settings.background_image | img_url: '2400x' }});
      background-size: cover; background-position: center; background-repeat: no-repeat;
    {%- endif -%}
  }
  .my-section.section-{{ section.id }} .my-section__container {
    max-width: {{ section.settings.container_max_width | default: '1280px' }};
    margin: 0 auto;
    {%- assign cp = section.settings.container_padding -%}
    {%- if cp -%}
      padding: {{ cp.top | default: 0 }}px {{ cp.right | default: 64 }}px {{ cp.bottom | default: 0 }}px {{ cp.left | default: 64 }}px;
    {%- else -%}
      padding: 0 64px;
    {%- endif -%}
  }
  @media (max-width: 991px) { .my-section.section-{{ section.id }} .my-section__container { padding-left: 24px; padding-right: 24px; } }
  @media (max-width: 767px) { .my-section.section-{{ section.id }} .my-section__container { padding-left: 16px; padding-right: 16px; } }
{%- endstyle -%}

Why split Container from Section Shell tip

Think of it as two nested boxes. Section Shell is the outer box — full-width colored area, section-to-section separator. Container is the inner box — bounded content area, optionally styled as a standalone card. Stack them for hero sections with image backgrounds + bordered content containers, or keep the container transparent for a clean centered layout.

Button

11 settings · text · link · font · checkbox · radio · range · padding · select ×2 · border · corner_radius

The canonical Button block. 11 settings — use the standalone blocks/button or copy inline. Font Size + Padding together determine visual size (no t-shirt preset). Style sets the base CSS class; everything else is inline-style override.

Fluid builder renders
Content
Text
Shop now
Link
/shop
Font Family
Inter
Open in new tab
Style
Style
FilledOutlineText only
Font Size
16
Padding
12
24
12
24
Background Color
Primary
Text Color
White
Shape
Border
0
Color Presets
#000000
Border Radius
8
8
8
8
Schema (inline in section's blocks) json
{
  "type": "button",
  "name": "Button",
  "settings": [
    { "type": "header", "content": "Content" },
    { "type": "text", "id": "text", "label": "Text", "default": "Shop now" },
    { "type": "url", "id": "link", "label": "Link" },
    { "type": "font_picker", "id": "font_family", "label": "Font Family", "default": "Inter" },
    { "type": "checkbox", "id": "open_new_tab", "label": "Open in new tab", "default": false },
    { "type": "header", "content": "Style" },
    { "type": "radio", "id": "style", "label": "Style", "default": "filled",
      "options": [
        { "value": "filled",  "label": "Filled" },
        { "value": "outline", "label": "Outline" },
        { "value": "text",    "label": "Text only" }
      ] },
    { "type": "range", "id": "font_size", "label": "Font Size",
      "min": 10, "max": 32, "step": 1, "default": 16, "unit": "px" },
    { "type": "padding", "id": "padding", "label": "Padding" },
    { "type": "select", "id": "background_color", "label": "Background Color",
      "options": "background_colors", "default": "var(--clr-primary)" },
    { "type": "select", "id": "text_color", "label": "Text Color",
      "options": "background_colors", "default": "var(--clr-white)" },
    { "type": "header", "content": "Shape" },
    { "type": "border", "id": "border", "label": "Border" },
    { "type": "corner_radius", "id": "border_radius", "label": "Border Radius" }
  ]
}
Liquid (render inside a section's case/when) liquid
Why Font Size + Padding instead of Size presets tip

T-shirt sizes (sm/md/lg) are opinionated — they bake in a specific relationship between font-size and padding. Font Size + Padding as independent controls let you dial in the exact button dimensions for the design, without being stuck with three preset shapes.

Image

11 settings · image_picker · text · url · checkbox · select ×3 · radio · range · border · corner_radius

The canonical Image block. Handles the image, link/clickability, aspect + fit + object-position (the three controls that actually frame a photo), an optional overlay (color + opacity), border, and radius. Always explicit alt text for accessibility.

Fluid builder renders
Content
Image
📁
Select or Upload image
Alt Text
Describe the image…
Link
/product
Open in new tab
Sizing
Aspect Ratio
Square (1:1)
Fit
CoverContainFill
Object Position
Center
Overlay
Overlay Color
Primary
Overlay Opacity
25
Shape
Border
0
Color Presets
#000000
Border Radius
12
12
12
12
Schema (inline in section's blocks) json
{
  "type": "image",
  "name": "Image",
  "settings": [
    { "type": "header", "content": "Content" },
    { "type": "image_picker", "id": "image", "label": "Image" },
    { "type": "text", "id": "alt", "label": "Alt Text",
      "info": "Leave blank to use the image's own alt." },
    { "type": "url", "id": "link", "label": "Link" },
    { "type": "checkbox", "id": "open_new_tab", "label": "Open in new tab", "default": false },
    { "type": "header", "content": "Sizing" },
    { "type": "select", "id": "aspect_ratio", "label": "Aspect Ratio", "default": "auto",
      "options": [
        { "value": "auto",  "label": "Auto (natural)" },
        { "value": "1/1",   "label": "Square (1:1)" },
        { "value": "4/3",   "label": "Standard (4:3)" },
        { "value": "16/9",  "label": "Widescreen (16:9)" },
        { "value": "3/2",   "label": "Classic (3:2)" },
        { "value": "3/4",   "label": "Portrait (3:4)" },
        { "value": "9/16",  "label": "Portrait mobile (9:16)" },
        { "value": "21/9",  "label": "Ultrawide (21:9)" }
      ] },
    { "type": "radio", "id": "fit", "label": "Fit", "default": "cover",
      "options": [
        { "value": "cover",   "label": "Cover" },
        { "value": "contain", "label": "Contain" },
        { "value": "fill",    "label": "Fill" }
      ] },
    { "type": "select", "id": "object_position", "label": "Object Position", "default": "center",
      "info": "Only affects Fit = Cover.",
      "options": [
        { "value": "center", "label": "Center" },
        { "value": "top",    "label": "Top" },
        { "value": "bottom", "label": "Bottom" },
        { "value": "left",   "label": "Left" },
        { "value": "right",  "label": "Right" }
      ] },
    { "type": "header", "content": "Overlay" },
    { "type": "select", "id": "overlay_color", "label": "Overlay Color",
      "options": "background_colors", "default": "transparent" },
    { "type": "range", "id": "overlay_opacity", "label": "Overlay Opacity",
      "min": 0, "max": 100, "step": 5, "default": 0, "unit": "%" },
    { "type": "header", "content": "Shape" },
    { "type": "border", "id": "border", "label": "Border" },
    { "type": "corner_radius", "id": "border_radius", "label": "Border Radius" }
  ]
}
Liquid (render inside a section's case/when) liquid
{% assign _img = block.settings.image %}
{% assign _alt = block.settings.alt | default: _img.alt | default: '' %}
{% capture _wrap %}
  {% if block.settings.aspect_ratio != 'auto' %}aspect-ratio: {{ block.settings.aspect_ratio }};{% endif %}
  {% assign br = block.settings.border_radius %}{% if br %}border-radius: {{ br.tl }}px {{ br.tr }}px {{ br.br }}px {{ br.bl }}px;{% endif %}
  {% assign bd = block.settings.border %}{% if bd.width %}border: {{ bd.width }} solid {{ bd.color }};{% endif %}
{% endcapture %}
{% if _img %} {{ _alt | escape }} {% endif %} {% if block.settings.overlay_color != blank and block.settings.overlay_opacity > 0 %} {% assign _a = block.settings.overlay_opacity | divided_by: 100.0 %}
{% endif %}
Alt text vs image_picker's built-in alt tip

The image_picker object carries its own .alt (set when the image was uploaded to the DAM). The explicit Alt Text field here overrides that when filled — useful when the image has a generic DAM alt but needs context-specific copy here. If left blank, the render falls back to image.alt.

Fluid Media

8 settings · media · select ×3 · checkbox · range · corner_radius

The canonical way to embed a Fluid DAM media asset (video, audio, PDF, etc.) using the <fluid-media-widget> web component. Picks from your media library, supports inline / popover / modal embed types, with theme-compliant border and radius.

Fluid builder renders
Media
Media
Embed Type
Inline (plays in place)
Responsive
Layout
Aspect Ratio
Widescreen (16:9)
Shape
Background Color
Transparent
Border Width
0
Border Color
Primary
Border Radius
12
12
12
12
Schema (inline in section's blocks) json
{
  "type": "fluid_media",
  "name": "Fluid Media",
  "settings": [
    { "type": "header", "content": "Media" },
    { "type": "media", "id": "media", "label": "Media" },
    { "type": "select", "id": "embed_type", "label": "Embed Type", "default": "auto",
      "options": [
        { "value": "auto",    "label": "Use media's default" },
        { "value": "inline",  "label": "Inline (plays in place)" },
        { "value": "popover", "label": "Popover (opens in overlay)" },
        { "value": "modal",   "label": "Modal (full-screen)" }
      ] },
    { "type": "checkbox", "id": "responsive", "label": "Responsive", "default": true },
    { "type": "header", "content": "Layout" },
    { "type": "select", "id": "aspect_ratio", "label": "Aspect Ratio", "default": "16/9",
      "options": [
        { "value": "auto", "label": "Auto (media's natural)" },
        { "value": "16/9", "label": "Widescreen (16:9)" },
        { "value": "1/1",  "label": "Square (1:1)" },
        { "value": "4/3",  "label": "Standard (4:3)" },
        { "value": "4/5",  "label": "Portrait (4:5)" },
        { "value": "9/16", "label": "Portrait mobile (9:16)" },
        { "value": "21/9", "label": "Ultrawide (21:9)" }
      ] },
    { "type": "header", "content": "Shape" },
    { "type": "select", "id": "background_color", "label": "Background Color",
      "options": "background_colors", "default": "transparent" },
    { "type": "range", "id": "border_width", "label": "Border Width",
      "min": 0, "max": 10, "step": 1, "default": 0, "unit": "px" },
    { "type": "select", "id": "border_color", "label": "Border Color",
      "options": "background_colors", "default": "var(--clr-primary)" },
    { "type": "corner_radius", "id": "border_radius", "label": "Border Radius" }
  ]
}
Liquid (render inside a section's case/when) liquid
{% assign _m = block.settings.media %}
{% assign _fmid = _m.fluid_media_id | default: _m.id %}
{% assign _embed = block.settings.embed_type %}
{% if _embed == 'auto' %}{% assign _embed = _m.embed_type | default: 'inline' %}{% endif %}
{% assign _ar = block.settings.aspect_ratio | default: 'auto' %}
{% capture _wrap %}
  {% if _ar != 'auto' and _embed == 'inline' %}aspect-ratio: {{ _ar }};{% endif %}
  background-color: {{ block.settings.background_color | default: 'transparent' }};
  {% assign br = block.settings.border_radius %}{% if br %}border-radius: {{ br.tl }}px {{ br.tr }}px {{ br.br }}px {{ br.bl }}px;{% endif %}
  {% if block.settings.border_width > 0 %}border: {{ block.settings.border_width }}px solid {{ block.settings.border_color }};{% endif %}
{% endcapture %}
{% if _fmid != blank %} {% endif %}
Embed types explained tip

Inline — the media renders in place and plays within its container. Needs an aspect ratio for videos to prevent layout shift.

Popover — the widget renders a thumbnail/trigger; clicking opens the media in an overlay panel. Good for video galleries.

Modal — similar to popover but full-screen. Good for long-form content.

Use media's default — defers to whatever embed_type was set when the media was uploaded to the DAM. Usually a safe default.

Category

Input

Text content controls. Use richtext for anything visible to visitors.

text

String

Short single-line text. Fluid attaches a full formatting toolbar even though the output is a plain string — avoid for visible copy unless you're sure users won't format it.

Fluid builder renders
Heading
Database Variable
Text Presets
Primary Color
BIUS 🔗
Welcome
Schema json
{ "type": "text", "id": "heading", "label": "Heading",
  "default": "Welcome" }
Liquid liquid
{{ section.settings.heading | escape }}

textarea

String

Multi-line plain text. Same toolbar as text, taller input. Preserve line breaks on output with | newline_to_br.

Fluid builder renders
Caption
Line one.
Line two.
Line three.
Schema json
{ "type": "textarea", "id": "caption", "label": "Caption",
  "default": "Line 1\nLine 2" }
Liquid liquid
{{ section.settings.caption | escape | newline_to_br }}

richtext

HTML string

Default for visible copy. Full WYSIWYG. Defaults must be wrapped in HTML tags. Output is HTML — don't escape.

Fluid builder renders
Body
Rich text with formatting and a link.
Schema json
{ "type": "richtext", "id": "body", "label": "Body",
  "default": "

Rich copy.

" }
Liquid liquid
{{ block.settings.body }}

html

HTML string

Raw HTML — monospace textarea, no toolbar. Use sparingly for embeds or custom widgets. Output directly; never escape.

Fluid builder renders
Custom HTML
<span style="padding:4px 8px;border-radius:6px;background:#0f172a;color:#fff;">Raw HTML</span>
Schema json
{ "type": "html", "id": "embed", "label": "Custom HTML" }
Liquid liquid
{{ section.settings.embed }}

url

String

Plain URL input with validation — no toolbar. Works for external links, DAM URLs, video sources.

Fluid builder renders
CTA URL
https://fluid.app
Schema json
{ "type": "url", "id": "cta_url", "label": "CTA URL",
  "default": "https://fluid.app" }
Liquid liquid
Category

Number & Selection

number is not supported — always use range. radio renders as pills, checkbox as a toggle switch.

range

Number

Horizontal slider with current value on the right. Requires min, max, step. Optional unit.

Fluid builder renders
Columns
3
Schema json
{ "type": "range", "id": "columns", "label": "Columns",
  "min": 1, "max": 12, "step": 1, "default": 3, "unit": "cols" }
Liquid liquid
grid-template-columns: repeat({{ section.settings.columns }}, 1fr);

select

String

Dropdown — use for 5+ choices. Requires options array or option-group reference.

Fluid builder renders
Layout
Grid
Schema json
{ "type": "select", "id": "layout", "label": "Layout", "default": "grid",
  "options": [
    { "value": "grid",     "label": "Grid" },
    { "value": "list",     "label": "List" },
    { "value": "carousel", "label": "Carousel" }
  ]
}
Liquid liquid

radio

String

Connected pill buttons — NOT actual radio dots. Use for 2–4 visual choices. Requires options.

Fluid builder renders
Size
SmallMediumLarge
Schema json
{ "type": "radio", "id": "size", "label": "Size", "default": "md",
  "options": [
    { "value": "sm", "label": "Small" },
    { "value": "md", "label": "Medium" },
    { "value": "lg", "label": "Large" }
  ]
}
Liquid liquid

checkbox

Boolean

Toggle switch (iOS-style) — NOT a checkbox. Great with visible_if to gate dependent settings.

Fluid builder renders
Show Divider
Schema json
{ "type": "checkbox", "id": "show_divider",
  "label": "Show Divider", "default": true }
Liquid liquid
{% if section.settings.show_divider %}
  
{% endif %}
Category

Visual & Media

Colors, fonts, and uploads. For theme-aware colors prefer the Theme Color Dropdown pattern over raw color.

color

Hex string

Raw hex color picker — swatch + hex input. Disconnected from theme tokens.

Fluid builder renders
Text Color
#6366f1
Schema json
{ "type": "color", "id": "text_color", "label": "Text Color",
  "default": "#111" }
Liquid liquid
color: {{ section.settings.text_color }};
Prefer theme-aware alternative tip

For colors that should respect theme tokens, use the Theme Color Dropdown pattern (select + option_group). That way editors pick from named theme colors and changes propagate automatically.

color_background

Hex or gradient

Tabs for Solid or Gradient. Gradient mode exposes angle, multiple colors, and 'Add Color'.

Fluid builder renders
Background
SolidGradient
Angle
135
Color 1
#6366f1
+ Add Color
Schema json
{ "type": "color_background", "id": "bg", "label": "Background",
  "default": "linear-gradient(135deg,#6366f1,#ec4899)" }
Liquid liquid
background: {{ section.settings.bg }};

select + option_group

Pattern · CSS value

This is how you get a theme-color dropdown in Fluid. There's no dedicated 'theme_color' control — you compose one: a select in your section schema pointed at an option_group defined in config/settings_schema.json.

Fluid builder renders
Background Color
Primary
1. Section schema — the select json
{ "type": "select", "id": "background_color",
  "label": "Background Color",
  "options": "background_colors",
  "default": "transparent" }
2. config/settings_schema.json — the option_group json
{
  "type": "color_background",
  "id": "color_primary",
  "label": "Primary Color",
  "default": "#023026",
  "option_group": {
    "id": "background_colors",
    "label": "Primary",
    "value": "var(--clr-primary)"
  }
}
Liquid liquid
background-color: {{ section.settings.background_color | default: 'transparent' }};
Critical bug to avoid warning

Never wrap the value as var(--clr-{{ '{{' }}…{{ '}}' }}). When the setting is empty that renders var(--clr-) — invalid CSS that silently breaks the entire rule block. The option_group's value already returns a CSS value (e.g. var(--clr-primary), transparent).

font_picker

String

Font-family dropdown. Plays nicely with | font_family.

Fluid builder renders
Heading Font
System UI
Schema json
{ "type": "font_picker", "id": "heading_font",
  "label": "Heading Font", "default": "Inter" }
Liquid liquid
font-family: {{ section.settings.heading_font | font_family }};

image_picker

Image object

Upload card — pulls from the DAM. Returns an object with .url, .alt, and more. Includes an alt-text field.

Fluid builder renders
Hero Image
📁
Select or Upload image
Schema json
{ "type": "image_picker", "id": "hero_image", "label": "Hero Image" }
Liquid liquid
{%- assign img = section.settings.hero_image -%}
{%- if img -%}
  {{ img.alt | escape }}
{%- endif -%}

video_picker

Video object

Upload card for DAM videos. Same alt-text field. Returns an object with source URLs.

Fluid builder renders
Demo Video
📁
Select or Upload video
Schema json
{ "type": "video_picker", "id": "demo_video", "label": "Demo Video" }
Liquid liquid
{%- assign v = section.settings.demo_video -%}
{%- if v -%}
  
{%- endif -%}

media_picker

Media object

Accepts either image or video — smaller 'Select media' link. Inspect .media_type to branch rendering.

Fluid builder renders
Hero Media
Schema json
{ "type": "media_picker", "id": "hero_media", "label": "Hero Media" }
Liquid liquid
{%- assign m = section.settings.hero_media -%}
{%- if m.media_type == 'image' -%}
  
{%- elsif m.media_type == 'video' -%}
  
{%- endif -%}

text_alignment

left | center | right

Pill buttons with alignment icons. Returns a string you can drop straight into CSS.

Fluid builder renders
Alignment
LeftCenterRight
Schema json
{ "type": "text_alignment", "id": "align",
  "label": "Alignment", "default": "center" }
Liquid liquid
text-align: {{ section.settings.align }};
Category

Layout

Structured objects — every section should ship with padding + corner_radius.

padding

{ top, bottom, left, right }

Four side icons with value + px unit, plus a link icon to chain them. Returns an object of the four sides.

Fluid builder renders
Section Padding
0
0
0
0
Schema json
{ "type": "padding", "id": "section_padding",
  "label": "Section Padding" }
Liquid wire-up liquid
{%- assign p = section.settings.section_padding -%}
{%- if p -%}
  padding: {{ p.top }}px {{ p.right }}px
           {{ p.bottom }}px {{ p.left }}px;
{%- endif -%}

corner_radius

{ tl, tr, br, bl }

Four corner icons with value + px, plus a link icon. Pair with padding.

Fluid builder renders
Section Border Radius
0
0
0
0
Schema json
{ "type": "corner_radius", "id": "section_border_radius",
  "label": "Section Border Radius" }
Liquid wire-up liquid
{%- assign r = section.settings.section_border_radius -%}
{%- if r -%}
  border-radius: {{ r.tl }}px {{ r.tr }}px
                 {{ r.br }}px {{ r.bl }}px;
{%- endif -%}

border

{ width, color }

Width input + 'Color Presets' dropdown + color swatch + hex. Width includes its unit (e.g. "2px").

Fluid builder renders
Section Border
0
Color Presets
#000000
Schema json
{ "type": "border", "id": "section_border",
  "label": "Section Border" }
Liquid liquid
{%- assign b = section.settings.section_border -%}
{%- if b.width -%}
  border: {{ b.width }} solid {{ b.color }};
{%- endif -%}

gradient_overlay

{ enabled, mode, colors, … }

Toggle switch — off by default. When on, expands to gradient/solid controls. Useful for hero media legibility.

Fluid builder renders
Overlay
Schema json
{ "type": "gradient_overlay", "id": "overlay", "label": "Overlay" }
Liquid liquid
{%- assign o = section.settings.overlay -%}
{%- if o.enabled -%}
  
{%- endif -%}

media_fit

String

Dropdown for how media fills its container — width / height / fit mode.

Fluid builder renders
Image Fit
Cover
Schema json
{ "type": "media_fit", "id": "image_fit", "label": "Image Fit" }
Liquid liquid
object-fit: {{ section.settings.image_fit | default: 'cover' }};
Category

Organization

Visual-only — purely UI. Renders no value in Liquid.

header

(none)

Creates a collapsible section in the editor sidebar that groups settings beneath it. Uses content, not label.

Fluid builder renders
Typography Settings
Schema json
{ "type": "header", "content": "Typography Settings" },
{ "type": "font_picker", "id": "heading_font", "label": "Heading Font" }
Liquid liquid
{# not rendered — UI only #}
Category

Resource (Single)

Chip-style pickers. Singular and plural aliases both work. link_list renders as a dropdown.

product · products

Product object

Pick a single product.

Fluid builder renders
Featured Product
Product #0
Schema json
{ "type": "product", "id": "featured_product",
  "label": "Featured Product" }
Liquid liquid
{% for p in products %}
  {% if p.id == section.settings.featured_product %}
    

{{ p.title }}

{% endif %} {% endfor %}

collection · collections

Collection object

Pick a single collection.

Fluid builder renders
Featured Collection
Collection #0
Schema json
{ "type": "collection", "id": "featured_collection",
  "label": "Featured Collection" }
Liquid liquid
{% assign c = section.settings.featured_collection %}
{{ c.title }}

category · categories

Category object

Pick a single category.

Fluid builder renders
Featured Category
Category #0
Schema json
{ "type": "category", "id": "featured_category",
  "label": "Featured Category" }
Liquid liquid
{{ section.settings.featured_category.title }}

post · posts

Post object

Pick a single blog post. Shopify migrants: use post, not article.

Fluid builder renders
Featured Post
Post #0
Schema json
{ "type": "post", "id": "featured_post",
  "label": "Featured Post" }
Liquid liquid
{{ section.settings.featured_post.title }}

enrollment_pack · enrollment

Enrollment object

Pick a single enrollment pack.

Fluid builder renders
Starter Pack
Enrollment #0
Schema json
{ "type": "enrollment_pack", "id": "starter_pack",
  "label": "Starter Pack" }
Liquid liquid
{{ section.settings.starter_pack.name }}

forms

Form object

Pick a single form from the store's forms library.

Fluid builder renders
Contact Form
Form #0
Schema json
{ "type": "forms", "id": "contact_form", "label": "Contact Form" }
Liquid liquid
{{ section.settings.contact_form.name }}

link_list

Linklist object

Pick a navigation menu. Renders as a dropdown (not a chip). Iterate .links to render.

Fluid builder renders
Main Menu
Select a menu
Schema json
{ "type": "link_list", "id": "main_menu", "label": "Main Menu" }
Liquid liquid
{% for link in section.settings.main_menu.links %}
  {{ link.title }}
{% endfor %}
Category

Resource (List)

Multi-resource pickers. Hard cap of 24. Iterate directly — return the full object array.

product_list · products_list

Array

Multi-product picker. Returns full product objects.

Fluid builder renders
Products
Product #0 Product #1
Schema json
{ "type": "product_list", "id": "grid_products",
  "label": "Products", "limit": 8 }
Liquid liquid
{% for product in section.settings.grid_products %}
  {{ product.title }}
{% endfor %}

collection_list · collections_list

Array

Multi-collection picker.

Fluid builder renders
Collections
Collection #0 Collection #1
Schema json
{ "type": "collection_list", "id": "grid_collections",
  "label": "Collections", "limit": 6 }
Liquid liquid
{% for c in section.settings.grid_collections %}
  {{ c.title }}
{% endfor %}

category_list · categories_list

Array

Multi-category picker.

Fluid builder renders
Categories
Category #0 Category #1
Schema json
{ "type": "category_list", "id": "grid_categories",
  "label": "Categories", "limit": 6 }
Liquid liquid
{% for cat in section.settings.grid_categories %}
  {{ cat.title }}
{% endfor %}

posts_list

Array

Multi-post picker.

Fluid builder renders
Posts
Post #0 Post #1
Schema json
{ "type": "posts_list", "id": "grid_posts",
  "label": "Posts", "limit": 6 }
Liquid liquid
{% for post in section.settings.grid_posts %}
  {{ post.title }}
{% endfor %}

enrollment_list · enrollments_list

Array

Multi-enrollment-pack picker.

Fluid builder renders
Enrollments
Enrollment #0 Enrollment #1
Schema json
{ "type": "enrollment_list", "id": "grid_enrollments",
  "label": "Enrollments", "limit": 6 }
Liquid liquid
{% for e in section.settings.grid_enrollments %}
  

{{ e.name }}

{% endfor %}
Category

Patterns

How controls combine for common section shapes. Copy the full recipe, then tune — these are the defaults every section should start from.

Divider Block (Cards / Steps)

Empty block · stateful walk in section liquid

Group repeating sub-blocks (image + heading + text + button …) into cards by walking blocks statelessly. An empty divider block opens a new card; subsequent canonical blocks render inside until the next divider closes/reopens. Used by feature_grid, testimonial_grid, process_steps.

Fluid builder renders
Pattern
See snippets below
Schema json
{
  "blocks": [
    { "type": "step", "name": "+ New Step", "settings": [
      { "type": "paragraph", "content": "Divider block. Add image / heading / text blocks AFTER this." }
    ] },
    { "type": "image",   "name": "Image",   "settings": [ /* canonical image */ ] },
    { "type": "heading", "name": "Heading", "settings": [
      { "type": "richtext", "id": "text", "label": "Title" }
    ] },
    { "type": "text", "name": "Text", "settings": [
      { "type": "richtext", "id": "text", "label": "Text" }
    ] }
  ]
}
Liquid liquid
{%- assign in_step = false -%}
{% for block in section.blocks %}
  {% case block.type %}
    {% when 'step' %}
      {% if in_step %}
{% endif %}
{% assign in_step = true %} {% when 'image' %} {% if in_step %}{% comment %}render canonical image{% endcomment %}{% endif %} {% when 'heading' %} {% if in_step %}

{{ block.settings.text }}

{% endif %} {% when 'text' %} {% if in_step %}
{{ block.settings.text }}
{% endif %} {% endcase %} {% endfor %} {% if in_step %}
{% endif %}
Why a divider block instead of nested blocks tip

Fluid doesn't support true block nesting. The divider pattern emulates it by walking the flat block list in order. Editors learn the rule once: "+ New Step" opens a card, blocks after it render inside until the next "+ New Step". Cards stay reorderable as a unit by moving the divider.

Theme-Driven Fonts

select + options: "font_families"

Never use font_picker in section/block settings — it's disconnected from the theme and lets editors pick any font. Always use a select with options: "font_families". Pulls from the 5-font theme palette: Body / Heading / Accent / Italic / Handwriting.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "select", "id": "font_family", "label": "Font Family",
  "options": "font_families",
  "default": "var(--ff-body)" }
Liquid liquid
{% if block.settings.font_family != blank %}
  font-family: {{ block.settings.font_family }};
{% endif %}

{# Value is already a CSS var like var(--ff-italic). #}
{# DO NOT pipe through | font_family — that's for raw strings only. #}
Theme exposes 5 fonts via option_group tip

For the dropdown to populate, config/settings_schema.json must declare each font with option_group: { id: "font_families", label: "Body", value: "var(--ff-body)" }. Without that metadata, the dropdown shows empty.

Split Border (width + color)

range + select · never native border type

Fluid's native border control uses a hex color picker internally — disconnected from the theme. Always split into two controls: a range for width, a select + background_colors for color. Same applies to nested borders (Container, items, cards).

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "range",  "id": "border_width", "label": "Border Width",
  "min": 0, "max": 10, "step": 1, "default": 0, "unit": "px" },
{ "type": "select", "id": "border_color", "label": "Border Color",
  "options": "background_colors", "default": "var(--clr-primary)" }
Liquid liquid
{% if section.settings.border_width > 0 %}
  border: {{ section.settings.border_width }}px solid {{ section.settings.border_color }};
{% endif %}

Canonical Block Primacy

Reuse blocks/image, blocks/button, blocks/fluid_media

Anywhere a section needs an image, button, video, or other primitive — use the canonical block, not a local one-off. Inline its full settings into the section's blocks array (Fluid's preset expansion requires inline copies, not refs). Standalone blocks/<name>/index.liquid files exist as the source-of-truth specs.

Fluid builder renders
Pattern
See snippets below
Schema json
{
  "blocks": [
    { "type": "image",  "name": "Image",  "settings": [ /* full canonical image schema */ ] },
    { "type": "button", "name": "Button", "settings": [ /* full canonical button schema */ ] }
  ]
}
Liquid liquid
{# Loop matched canonical blocks in section liquid #}
{% for block in section.blocks %}
  {% case block.type %}
    {% when 'image' %}  {% comment %}canonical image markup{% endcomment %}
    {% when 'button' %} {% comment %}canonical button markup{% endcomment %}
  {% endcase %}
{% endfor %}
Exception: data-driven images tip

If the image is bound to a data source (product.images[0].src, post.image_url) — render directly. Canonical blocks are for editor-uploaded images only.

Loop, don't `| first`

for + drop limit: 1 · enables block duplicate

If you write section.blocks | where: 'type', 'heading' | first, only the first instance of that block ever renders — even if the editor duplicates it. Symptom: duplicate appears in the editor list but does nothing on the page. Fix: drop limit: 1 from the schema and render via a {% for %} loop.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "heading", "name": "Heading",
  "settings": [
    { "type": "richtext", "id": "text", "label": "Text",
      "default": "

Heading

" } ] }
Liquid liquid
{# WRONG — duplicate doesn't render #}
{%- assign h = section.blocks | where: 'type', 'heading' | first -%}
{% if h %}
{{ h.settings.text }}
{% endif %} {# RIGHT — duplicate works #} {%- assign hs = section.blocks | where: 'type', 'heading' -%} {% for block in hs %}
{{ block.settings.text }}
{% endfor %}
When limit: 1 IS appropriate tip

For blocks with a structurally enforced single slot — hero image, locale dropdown, primary CTA in a navbar — keep limit: 1. Editor disables duplicate. Loop over the result anyway so the rendering pattern is consistent.

Phantom Default Antipattern

Wrap in {% if block %} · never `| default: "

..."`

Anti-pattern: {{ heading_block.settings.text | default: '<h2>Lorem...</h2>' }}. When no block of that type is present, the fallback STILL renders — editors see lorem text on the page they can't click or edit (no block = no fluid_attributes binding). Fix: conditionally render the wrapper.

Fluid builder renders
Pattern
See snippets below
Schema json
{# Schema: defaults belong on the BLOCK, not on the rendered output #}
{ "type": "heading", "name": "Heading",
  "settings": [
    { "type": "richtext", "id": "text", "label": "Text",
      "default": "

Default heading

" } ] }
Liquid liquid
{# WRONG — phantom lorem text appears with no editor binding #}
{{ heading_block.settings.text | default: "

Lorem...

" }}
{# RIGHT — only renders when the block exists #} {% if heading_block %}
{{ heading_block.settings.text }}
{% endif %}

Stat (Prefix + Value + Suffix + Label)

4 text fields · used in stats_bar / hero_split_stats

The atomic unit for any number-driven block: $ + 49 + + + "Free shipping over". Three text inputs for the value composition + one for the descriptive label. Stack them in a row or grid.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "stat", "name": "Stat",
  "settings": [
    { "type": "text", "id": "prefix", "label": "Prefix" },
    { "type": "text", "id": "value",  "label": "Value",  "default": "10K" },
    { "type": "text", "id": "suffix", "label": "Suffix", "default": "+" },
    { "type": "text", "id": "label",  "label": "Label",  "default": "Happy customers" }
  ] }
Liquid liquid
{% for block in stat_blocks %}
  
{%- if block.settings.prefix != blank -%}{{ block.settings.prefix | escape }}{%- endif -%} {{ block.settings.value | escape }} {%- if block.settings.suffix != blank -%}{{ block.settings.suffix | escape }}{%- endif -%}
{{ block.settings.label | escape }}
{% endfor %}

Rating Stars

range 0-5 + SVG loop · full + empty colors

0–5 star ratings as a range. Render filled stars for the rating, then empty stars to pad to 5. Both colors theme-driven.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "range", "id": "rating", "label": "Stars filled",
  "min": 0, "max": 5, "step": 1, "default": 5 },
{ "type": "select", "id": "star_color",       "label": "Star color",
  "options": "background_colors", "default": "var(--clr-warning)" },
{ "type": "select", "id": "star_empty_color", "label": "Empty star color",
  "options": "background_colors", "default": "var(--clr-gray)" }
Liquid liquid
{%- assign rating = block.settings.rating | default: 5 | plus: 0 -%}
{%- if rating > 5 -%}{% assign rating = 5 %}{%- endif -%}
{%- assign empty = 5 | minus: rating -%}
{% for i in (1..rating) %} {% endfor %} {% if empty > 0 %} {% for i in (1..empty) %}same star svg{% endfor %} {% endif %}

Trust Bar

Stars + score + count text · social proof under hero

Compact trust signal that pairs with hero or product CTAs. Combines the rating-stars pattern with a score number and review count text. One block, three visible elements.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "trust", "name": "Trust Bar",
  "settings": [
    { "type": "checkbox", "id": "show_stars", "label": "Show stars", "default": true },
    { "type": "range", "id": "rating", "label": "Stars filled",
      "min": 0, "max": 5, "step": 1, "default": 5 },
    { "type": "text", "id": "text", "label": "Text",
      "default": "4.9 from 2,400+ verified reviews" }
  ] }
Liquid liquid
{% for block in trust_blocks %}
  
{% if block.settings.show_stars %} {% comment %}render stars per rating{% endcomment %} {% endif %} {{ block.settings.text | escape }}
{% endfor %}

Tag Pill

Uppercase label · bordered or filled · for badges/categories

Small uppercase label with a border or fill, used for categories, status badges, sale flags, etc. Theme-driven background, text, and border colors.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "tag", "name": "Tag Pill",
  "settings": [
    { "type": "text", "id": "text", "label": "Text", "default": "New" },
    { "type": "select", "id": "background", "label": "Background",
      "options": "background_colors", "default": "transparent" },
    { "type": "select", "id": "text_color", "label": "Text color",
      "options": "background_colors", "default": "var(--clr-primary)" },
    { "type": "select", "id": "border_color", "label": "Border color",
      "options": "background_colors", "default": "var(--clr-primary)" }
  ] }
Liquid liquid
{% for block in tag_blocks %}
  
    {{ block.settings.text | escape }}
  
{% endfor %}

Card Grid (via divider)

CSS grid + divider blocks · responsive columns

The composition pattern for any card-driven section: CSS grid wrapper, configurable columns per breakpoint, and divider blocks that group canonical sub-blocks into cards. Used by feature_grid, testimonial_grid, process_steps.

Fluid builder renders
Pattern
See snippets below
Schema json
{
  "settings": [
    { "type": "range", "id": "columns_desktop", "label": "Columns (desktop)",
      "min": 1, "max": 4, "step": 1, "default": 3 },
    { "type": "range", "id": "columns_tablet",  "label": "Columns (tablet)",
      "min": 1, "max": 3, "step": 1, "default": 2 },
    { "type": "range", "id": "columns_mobile",  "label": "Columns (mobile)",
      "min": 1, "max": 2, "step": 1, "default": 1 },
    { "type": "range", "id": "gap", "label": "Gap (px)",
      "min": 8, "max": 48, "step": 2, "default": 24 }
  ]
}
Liquid liquid
.grid {
  display: grid;
  grid-template-columns: repeat({{ section.settings.columns_desktop }}, minmax(0, 1fr));
  gap: {{ section.settings.gap }}px;
}
@media (max-width: 991px) {
  .grid { grid-template-columns: repeat({{ section.settings.columns_tablet }}, minmax(0, 1fr)); }
}
@media (max-width: 767px) {
  .grid { grid-template-columns: repeat({{ section.settings.columns_mobile }}, minmax(0, 1fr)); gap: 16px; }
}

CSS Scroll-Snap Carousel

No JS lib · scroll-snap track + vanilla arrows/dots

Splide breaks Fluid theme rendering. Use a CSS scroll-snap track + tiny vanilla JS for arrows/dots/autoplay instead. Works for any horizontally-scrolling collection. Used by ugc_carousel.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "range", "id": "items_desktop", "label": "Items per view (desktop)",
  "min": 1, "max": 6, "step": 1, "default": 4 },
{ "type": "range", "id": "gap", "label": "Gap (px)",
  "min": 0, "max": 48, "step": 2, "default": 16 },
{ "type": "checkbox", "id": "loop",     "label": "Loop",     "default": true },
{ "type": "checkbox", "id": "autoplay", "label": "Autoplay", "default": false },
{ "type": "checkbox", "id": "show_arrows",     "label": "Arrows", "default": true },
{ "type": "checkbox", "id": "show_pagination", "label": "Dots",   "default": false }
Liquid liquid
.carousel__track {
  display: grid; grid-auto-flow: column;
  grid-auto-columns: calc((100% - ({{ items_desktop }} - 1) * {{ gap }}px) / {{ items_desktop }});
  gap: {{ gap }}px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}
.carousel__slide { scroll-snap-align: start; }

Why no Splide tip

Fluid's editor injects DOM mutations on save. Splide's lifecycle re-mounts on every mutation, double-binds events, and breaks scroll position. Pure CSS scroll-snap is mutation-safe.

Native Accordion

Native
/ · zero JS · fully accessible

For FAQ and any expandable list, use native HTML <details>/<summary>. Zero JS, fully accessible, keyboard navigation built in. Style the marker via ::after. Used by faq_accordion.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "faq_item", "name": "FAQ Item",
  "settings": [
    { "type": "text",     "id": "question", "label": "Question" },
    { "type": "richtext", "id": "answer",   "label": "Answer" },
    { "type": "checkbox", "id": "open_by_default", "label": "Open by default" }
  ] }
Liquid liquid
{% for block in faq_items %}
  
{{ block.settings.question | escape }}
{{ block.settings.answer }}
{% endfor %}

Drag Slider (Before/After)

Native input[type=range] overlay + clipped after image

Image-comparison slider with drag handle. Use a transparent native <input type="range"> spanning the full width as the drag affordance — it gives you free keyboard + touch + accessibility. CSS clips the "after" image based on the range value via vanilla JS.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "image_picker", "id": "before_image", "label": "Before image" },
{ "type": "image_picker", "id": "after_image",  "label": "After image" },
{ "type": "range", "id": "initial_position", "label": "Initial Position",
  "min": 5, "max": 95, "step": 5, "default": 50, "unit": "%" }
Liquid liquid

Cart Button

Icon choices + count badge · preserves Fluid JS hooks

Theme-driven cart trigger. 14 settings: icon choice (bag / cart / basket / minimal), style (filled / outline / icon-only), padding, radius, border, all colors theme-driven, count badge with own font-size + colors. Preserves #show-cart + #fluid-cart-count so Fluid's existing cart JS keeps working.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "cart_button", "name": "Cart Button", "limit": 1,
  "settings": [
    { "type": "radio", "id": "icon", "label": "Icon", "default": "bag",
      "options": [
        { "value": "bag",     "label": "Shopping Bag" },
        { "value": "cart",    "label": "Shopping Cart" },
        { "value": "basket",  "label": "Basket" },
        { "value": "minimal", "label": "Minimal" }
      ] },
    { "type": "range",  "id": "icon_size", "min": 14, "max": 32, "step": 1, "default": 20 },
    { "type": "select", "id": "icon_color",       "options": "background_colors", "default": "var(--clr-primary)" },
    { "type": "select", "id": "count_background", "options": "background_colors", "default": "var(--clr-accent)" }
    /* + style, padding, border, count_text_color, etc. */
  ] }
Liquid liquid

Locale Dropdown

Single component · desktop popover + mobile sheet · preserves Fluid JS

One unified component renders both the desktop popover and mobile slide-in sheet. Preserves existing Fluid JS hooks (#show-language-country-dropdown, #mobile-country-language, .saveLocaleBtn, .country-selector, .language-selector) so Fluid's built-in locale-switch logic keeps working without touching header.js.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "locale_dropdown", "name": "Locale Dropdown", "limit": 1,
  "settings": [
    { "type": "select", "id": "display_mode", "label": "Trigger display", "default": "flag_code",
      "options": [
        { "value": "flag_code", "label": "Flag + code" },
        { "value": "flag_only", "label": "Flag only" },
        { "value": "code_only", "label": "Code only" },
        { "value": "flag_name", "label": "Flag + country name" }
      ] },
    { "type": "select", "id": "trigger_color",   "options": "background_colors", "default": "var(--clr-body)" },
    { "type": "select", "id": "panel_background", "options": "background_colors", "default": "var(--clr-white)" }
    /* + 20+ more theme-driven settings */
  ] }
Liquid liquid
{# See components/navbar_locale_dropdown/index.liquid for full markup #}
{# Trigger uses Fluid globals: current_country_iso, current_country_name, current_selected_locale #}
{# Panel iterates: countries, language_options #}
{# Save button: class="saveLocaleBtn" inside class="locale-selector" #}



Hero Overlay (Solid + Gradient)

select color + range opacity + radio style

Heroes need an overlay over the background image to ensure text contrast. Three controls: color (theme-driven), opacity, and style (solid wash vs bottom-up gradient). Renders as a ::before pseudo-element to avoid extra DOM.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "select", "id": "overlay_color", "label": "Overlay color",
  "options": "background_colors", "default": "var(--clr-dark)" },
{ "type": "range",  "id": "overlay_opacity", "label": "Overlay opacity",
  "min": 0, "max": 100, "step": 5, "default": 35, "unit": "%" },
{ "type": "radio",  "id": "overlay_style", "label": "Overlay style", "default": "solid",
  "options": [
    { "value": "solid",    "label": "Solid color" },
    { "value": "gradient", "label": "Bottom gradient (transparent → color)" }
  ] }
Liquid liquid
{%- if section.settings.overlay_color != blank and section.settings.overlay_opacity > 0 -%}
  {%- assign _ov = section.settings.overlay_opacity | divided_by: 100.0 -%}
  .hero::before {
    content: ""; position: absolute; inset: 0;
    background: {% if section.settings.overlay_style == 'gradient' %}
      linear-gradient(180deg, transparent 0%, {{ section.settings.overlay_color }} 100%)
    {% else %}
      {{ section.settings.overlay_color }}
    {% endif %};
    opacity: {{ _ov }}; pointer-events: none; z-index: 0;
  }
{%- endif -%}

Scroll Indicator

Animated bouncing arrow at hero bottom

Subtle visual cue at the bottom of full-bleed heroes that says "there's more below". Pure CSS keyframe animation, theme-driven color.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "checkbox", "id": "show_scroll_indicator", "label": "Show scroll indicator", "default": true },
{ "type": "text",     "id": "scroll_label", "label": "Scroll label", "default": "Scroll" },
{ "type": "select",   "id": "scroll_color", "label": "Scroll indicator color",
  "options": "background_colors", "default": "var(--clr-white)" }
Liquid liquid
{% if section.settings.show_scroll_indicator %}
  
{{ section.settings.scroll_label | escape }}
{% endif %}

Comparison Row (Check / X / Text)

radio with 3 visual modes · for us-vs-them tables

Each row in a comparison table picks how it displays for each column: a checkmark (yes), an X (no), or free text. Used by comparison_table. The radio control switches markup, not just styling.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "row", "name": "Row",
  "settings": [
    { "type": "text", "id": "feature", "label": "Feature", "default": "Free shipping" },
    { "type": "radio", "id": "us_value", "label": "Our column", "default": "check",
      "options": [
        { "value": "check", "label": "Checkmark" },
        { "value": "x",     "label": "Cross" },
        { "value": "text",  "label": "Text" }
      ] },
    { "type": "text", "id": "us_text", "label": "Text (if Display = Text)", "default": "Yes" }
    /* + them_value, them_text */
  ] }
Liquid liquid
{% case block.settings.us_value %}
  {% when 'check' %}
    
  {% when 'x' %}
    
  {% else %}
    {{ block.settings.us_text | escape }}
{% endcase %}

Connector Line (Timeline)

CSS pseudo-element on .step:not(:last-child)

For process-step / timeline UIs. A single CSS pseudo-element on each step (except the last) draws the connecting line. No extra DOM. Vertical or horizontal, theme-driven color, hidden on mobile.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "checkbox", "id": "show_connector", "label": "Show connecting line", "default": true },
{ "type": "select",   "id": "connector_color", "label": "Connector color",
  "options": "background_colors", "default": "var(--clr-gray)" }
Liquid liquid
{# Horizontal #}
{%- if section.settings.show_connector -%}
  .step:not(:last-child)::after {
    content: "";
    position: absolute;
    top: 28px;  /* center of number circle */
    left: calc(50% + 36px);
    right: calc(-50% + 36px);
    height: 2px;
    background: {{ section.settings.connector_color }};
    z-index: 1;
  }
  @media (max-width: 767px) { .step:not(:last-child)::after { display: none; } }
{%- endif -%}

Glass Morphism (Backdrop Blur)

backdrop-filter pill · for badges over images

For floating badges, captions, or controls layered over images: semi-transparent background + backdrop-filter: blur() creates a frosted-glass effect. Use sparingly — great for one or two elements per page, distracting if overused.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "select", "id": "glass_background", "label": "Background",
  "options": "background_colors", "default": "var(--clr-white)" },
{ "type": "range",  "id": "glass_opacity", "label": "Opacity",
  "min": 0, "max": 100, "step": 5, "default": 90, "unit": "%" }
Liquid liquid
.glass-pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 14px;
  background: rgba(255, 255, 255, 0.15);
  color: var(--clr-white);
  border-radius: 999px;
  font-size: 12px; font-weight: 600; text-transform: uppercase;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
Don't blur over solid backgrounds tip

backdrop-filter has no visible effect over solid colors — it only blurs whatever's behind it. Reserve it for elements that float over images or video.

Editorial Meta Column

writing-mode: vertical-rl · sidebar of meta labels

Magazine-style sidebar of small meta labels ("Issue 04", "Spring 2026") rotated vertically beside the main content. Single CSS property turns horizontal text into vertical text — no DOM hacks. Used by hero_editorial.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "meta", "name": "Meta Label",
  "settings": [
    { "type": "text", "id": "text", "label": "Text", "default": "Issue 04" }
  ] }
Liquid liquid


Image Stretch Override

When canonical image fills a hero column, override aspect-ratio

The canonical image block has its own aspect_ratio control — great when the image is sized by ratio. But when the image needs to FILL a parent column (like in a 50/50 hero), the section CSS must override the ratio and let the image stretch.

Fluid builder renders
Pattern
See snippets below
Schema json
{# In hero_split_stats schema, the image block keeps its full canonical settings.
   The section's CSS overrides ratio/sizing instead. #}

{ "type": "image", "name": "Hero Image", "limit": 1,
  "settings": [ /* full canonical image settings */ ] }
Liquid liquid
/* In the hero section's CSS, override the image block's wrapper */
.hero__image-slot { align-self: stretch; position: relative; min-height: 360px; }

.hero__image-slot .media-wrap {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  aspect-ratio: unset !important;  /* override canonical block's setting */
  margin: 0 !important;
}
.hero__image-slot .media-wrap img {
  width: 100%; height: 100%; object-fit: cover;
}

Media Picker Fallback

Handle Fluid media + plain image upload

The media_picker control returns two different shapes depending on what the editor picked: a Fluid Media object with fluid_media_id (when picking a Fluid video / UGC asset) OR a plain image object (when uploading a JPG/PNG). Always handle both — fall through to | image_url when no media id is present.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "media_picker", "id": "media", "label": "Media" }
Liquid liquid
{%- assign _m = block.settings.media -%}
{%- assign _fmid = _m.fluid_media_id -%}
{%- assign _img_url = '' -%}
{%- if _fmid == blank and _m -%}
  {%- assign _img_url = _m | image_url -%}
  {%- if _img_url == blank -%}{%- assign _img_url = _m.src | default: _m.url -%}{%- endif -%}
{%- endif -%}

{% if _fmid != blank %}
  
{% elsif _img_url != blank %}
  
{% else %}
  
{% endif %}

Product Image Fallback Chain

product.images[0].src → product.image_url → product.image | image_url

Fluid product objects carry images in different shapes depending on context. Always check the chain in order: product.images[0].src (most explicit), then product.image_url, then product.image | image_url. Render a placeholder if all fail.

Fluid builder renders
Pattern
See snippets below
Schema json
{# No schema — reads from Fluid's product object #}
Liquid liquid
{%- assign _pimg_url = '' -%}
{%- if product.images and product.images.size > 0 -%}
  {%- assign _pimg_url = product.images[0].src -%}
{%- elsif product.image_url != blank -%}
  {%- assign _pimg_url = product.image_url -%}
{%- elsif product.image -%}
  {%- assign _pimg_url = product.image | image_url -%}
{%- endif -%}

{% if _pimg_url != blank %}
  {{ product.title | escape }}
{% else %}
  
{% endif %}

Collection Fallback Chain

collection.image_url → first product.image_url

For collection tiles / category cards: a collection's own image_url is often null (not all stores set it). Fall back to the first product's image so every tile renders something.

Fluid builder renders
Pattern
See snippets below
Schema json
{ "type": "collection", "id": "collection", "label": "Collection" }
Liquid liquid
{%- assign _col = block.settings.collection -%}
{%- assign _img_url = '' -%}
{%- if _col.image_url != blank -%}
  {%- assign _img_url = _col.image_url -%}
{%- elsif _col.image_path != blank -%}
  {%- assign _img_url = _col.image_path -%}
{%- elsif _col.product_collections -%}
  {%- for pc in _col.product_collections limit: 1 -%}
    {%- if pc.product.image_url != blank -%}{%- assign _img_url = pc.product.image_url -%}{%- endif -%}
  {%- endfor -%}
{%- elsif _col.products -%}
  {%- for p in _col.products limit: 1 -%}
    {%- if p.image_url != blank -%}{%- assign _img_url = p.image_url -%}{%- endif -%}
  {%- endfor -%}
{%- endif -%}

{# Also check canonical_url for the collection link, NOT .url #}
{%- assign _href = _col.canonical_url | default: _col.url | default: '#' -%}

Preset Expansion Workflow

Destroy template + re-push to refresh blocks

Section presets only expand on fresh template creation — not on resource updates. After changing a section's block structure (adding new block types, removing old ones), the existing template instance still has the old blocks. To refresh: destroy the template via API, then re-push the page template file. Fluid creates a new template record and runs preset expansion.

Fluid builder renders
Pattern
See snippets below
Schema json
{# No schema — it's an API workflow #}
Liquid liquid
# 1. Find the template's resource_id
curl -H "Authorization: Bearer $TOKEN" \
  "https://$COMPANY.fluid.app/api/application_themes/$THEME_ID/resources" \
  | jq '.application_theme_resources[] | select(.key == "home_page/default/index.liquid")'

# 2. Destroy the template (this clears the old block instances)
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  "https://$COMPANY.fluid.app/api/application_theme_templates/$TEMPLATE_ID"
# → "Template was successfully destroyed."

# 3. Re-push the template file
curl -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key":"home_page/default/index.liquid","content":"..."}' \
  "https://$COMPANY.fluid.app/api/application_themes/$THEME_ID/resources"
# → fresh template with preset blocks expanded
DELETE /resources alone is not enough tip

The DELETE /api/application_themes/{id}/resources endpoint with a key in the body returns 200 but only nulls the content — the underlying ApplicationThemeTemplate record persists. To fully destroy a template (so re-push triggers fresh preset expansion), use DELETE /api/application_theme_templates/{template_id}.

Liquid Comments

{% comment %} not {# #}

Fluid uses Liquid, not Jinja or Twig. Jinja-style {# comment #} tags render as literal text on the page — the parser doesn't recognize them. Always use {% comment %} / {% endcomment %}.

Fluid builder renders
Pattern
See snippets below
Schema json
{# No schema — syntax rule #}
Liquid liquid
{# WRONG — renders as text on the page #}
{# This is a Jinja-style comment #}

{# RIGHT — Liquid comment, fully stripped #}
{% comment %} This is a Liquid comment {% endcomment %}

{# Whitespace-stripping variant #}
{%- comment -%} Eats surrounding whitespace too {%- endcomment -%}