WebDYI
WebDYI
Deploy Your Integrations
Core Workflow

Schema to no-code customization

Schema defines editable values; Customize renders controls for clients automatically.

The Schema dropdown includes built-in helpers you can use, like generating schema from code, inserting sample schema, inserting section-with-blocks, and inserting all input types.

Accepted schema shapes

  • Bare array of settings
  • Shopify-style section object with settings, blocks, presets, and max_blocks

Supported setting types

text, textarea, richtext, html, color, range, checkbox/boolean, select, radio, number, url, image_picker, video/video_picker, icon_picker, font_picker.

For text, pick by how much control the client needs: textarea for plain, multi-line text; richtext for a formatted editor (bold, lists, links) with no code; and html for raw markup in a monospace code editor. All three render as HTML — see below.

Icon picker

Use icon_picker to let clients choose an SVG icon. Combine it with icon_size and icon_color settings for full control.

{
  "type": "icon_picker",
  "id": "btn_icon",
  "label": "Button Icon",
  "default": "heart"
}

Template usage:

{% render 'icon', icon: section.settings.btn_icon %}

The icon renders as an SVG. You can style it with CSS or inline styles as usual.

Rich text

Use richtext to give clients a no-code WYSIWYG editor (bold, italic, underline, lists, links). The value is stored as HTML and rendered as formatted markup — no code editor, unlike html.

{
  "type": "richtext",
  "id": "body",
  "label": "Body",
  "default": "<p>Edit <strong>me</strong></p>"
}

Template usage:

<div class="prose">{{ section.settings.body }}</div>

The default is HTML. Like html, the value runs inside the widget's sandboxed iframe — treat it with the same trust as your own code.

HTML

Use html to let clients add a block of raw HTML — an embed snippet, a custom layout, an SVG. The Customize panel shows a monospace code editor, and the value is output unescaped, so it renders as real markup.

{
  "type": "html",
  "id": "custom_html",
  "label": "Custom HTML",
  "default": "<p>Edit <strong>me</strong></p>"
}

Template usage:

<div class="custom">{{ section.settings.custom_html }}</div>

html vs textarea: use html when the value is markup, and textarea for plain prose. Liquid prints both raw, so to render a value as literal text (escaping any < / &) add the escape filter: {{ section.settings.bio | escape }}.

Note: the value is inserted into the already-rendered page, so Liquid tags typed into the field are not re-processed — it's HTML/CSS/JS, not a template. It runs inside the widget's sandboxed iframe (no access to the host page), but it is still arbitrary client-supplied markup — treat it with the same trust as your own code.

Image

Use image_picker to let clients add an image — upload a file or paste a URL. The value is the image URL, plus an alt text and an optional display name.

{
  "type": "image_picker",
  "id": "image",
  "label": "Image",
  "default": ""
}

The value is an object with these properties:

  • {{ section.settings.image }} — the image URL
  • {{ section.settings.image.alt }} — alt text (accessibility)
  • {{ section.settings.image.name }} — the optional display name

Template usage (caption from the display name):

{% if section.settings.image != blank %}
  <figure>
    <img src="{{ section.settings.image }}" alt="{{ section.settings.image.alt }}" loading="lazy">
    {% if section.settings.image.name != blank %}
      <figcaption>{{ section.settings.image.name }}</figcaption>
    {% endif %}
  </figure>
{% endif %}

Inside a repeatable block, use block.settings instead — e.g. {{ block.settings.image_1.name }}.

Video

Use video (or video_picker) to let clients add a video — a YouTube, Vimeo, Loom, Dailymotion, or Wistia link, a direct .mp4/.webm URL, or an uploaded file. WebDYI detects the source and turns the value into an object, so your template can drop in a ready-made player or build its own.

Schema example:

{
  "type": "video",
  "id": "video",
  "label": "Video",
  "default": ""
}

Easiest — drop in the ready-made responsive player (a 16:9 <iframe> for embed providers). It is empty for direct video files, so pair it with a <video> fallback:

{% if section.settings.video.embed %}
  {{ section.settings.video.embed_html }}
{% elsif section.settings.video %}
  <video src="{{ section.settings.video }}" controls playsinline style="width:100%"></video>
{% endif %}

The video value is an object with these properties:

  • {{ section.settings.video }} — the original URL the client entered
  • .embed_html — a ready-to-drop, lazy-loaded responsive <iframe> player (embed providers only)
  • .embed — the canonical embed URL, e.g. https://www.youtube.com/embed/<id>
  • .type — the provider (youtube, vimeo, loom, dailymotion, wistia) for embeds, or video for a direct file
  • .embedId — the platform's video id
  • .controls, .autoplay, .loop, .muted — the playback options the client toggled (present when set)

The Controls, Autoplay, Loop, and Muted toggles appear automatically in the Customize panel for any video field — you don't declare them in the schema (Controls is on by default). They don't change the ready-made .embed_html player, but you can apply them to a direct <video> so the client's choices take effect:

<video src="{{ section.settings.video }}"
  {% if section.settings.video.controls %}controls{% endif %}
  {% if section.settings.video.autoplay %}autoplay{% endif %}
  {% if section.settings.video.loop %}loop{% endif %}
  {% if section.settings.video.muted %}muted{% endif %}
  playsinline style="width:100%"></video>

Browsers only allow autoplay when the video is also muted, so enable both together.

Poster image

A direct <video> can show a thumbnail before it plays via the poster attribute. Add a companion image_picker setting so the client can choose one:

{
  "type": "image_picker",
  "id": "video_poster",
  "label": "Video poster"
}
<video src="{{ section.settings.video }}"
  poster="{{ section.settings.video_poster }}"
  controls playsinline style="width:100%"></video>

Embedded players (YouTube, Vimeo…) show the provider's own thumbnail. To use a custom poster there, display the image with a play button and swap in {{ section.settings.video.embed_html }} on click.

Or build your own embed instead of using .embed_html:

<iframe src="{{ section.settings.video.embed }}" allowfullscreen
  style="border:0;width:100%;aspect-ratio:16/9"></iframe>

Note: third-party players (YouTube, Vimeo…) only play where the provider allows embedding from the hosting page, so some show a "configuration error" in the editor's preview pane but work on the published widget URL. A direct .mp4/.webm file played through <video> is the most reliable everywhere.

Schema variables in CSS and JavaScript

You can safely pass schema values like {{ section.settings.heading }} inside CSS and JS. The editor may show lint errors because it sees the Liquid-like syntax as invalid code.

If you want to avoid lint warnings, use these patterns:

  • CSS: use a CSS custom property injected from HTML:
    <style>
      .my-component {
        color: var(--heading-color);
      }
    </style>
    <div class="my-component" style="--heading-color: {{ section.settings.heading_color }}">
      ...
    </div>
  • JavaScript: read the value from a data-* attribute:
    <div id="widget" data-heading="{{ section.settings.heading }}"></div>
    <script>
      const heading = document.getElementById('widget').dataset.heading;
    </script>

Custom fonts

Use a font_picker setting to let clients choose a font. The picker includes a curated list of Google Fonts and system stacks, and clients can also add their own custom fonts by entering a Google font name, a stylesheet URL, or uploading a font file. The chosen font loads automatically in the rendered widget.

Schema example:

{
  "type": "font_picker",
  "id": "font",
  "label": "Font"
}

Template usage:

body {
  font-family: {{ section.settings.font.family }};
}

Or use the full stack:

body {
  font-family: {{ section.settings.font }};
}

The font_picker value is a font object with these properties:

  • {{ section.settings.font }} — the full CSS font-family stack (e.g. "Inter", sans-serif)
  • {{ section.settings.font.family }} — the primary family name (e.g. "Inter")
  • {{ section.settings.font.fallback_families }} — the fallback list (e.g. sans-serif)

Set a default font

Add a default with a CSS font stack. Match one of the built-in options so the picker shows it pre-selected (an unmatched stack still works, but appears as "Custom"):

{
  "type": "font_picker",
  "id": "heading_font",
  "label": "Heading font",
  "default": "'Poppins', sans-serif"
}

Common stacks: 'Inter', sans-serif, 'Poppins', sans-serif, Georgia, 'Times New Roman', serif, ui-monospace, SFMono-Regular, Menlo, monospace.

Add a fallback font

Define the fallback chain once in the schema with fallback_families. It overrides the picked font's generic fallback, so every template that uses the value gets it automatically — no per-template wiring:

{
  "type": "font_picker",
  "id": "heading_font",
  "label": "Heading font",
  "default": "'Poppins', sans-serif",
  "fallback_families": "Georgia, 'Times New Roman', serif"
}

Now {{ section.settings.heading_font }} renders 'Poppins', Georgia, 'Times New Roman', serif, and .fallback_families becomes Georgia, 'Times New Roman', serif.

Or handle it per-template instead. The picked value already ends in a generic fallback (e.g. 'Poppins', sans-serif), so the simplest version is to use the full stack — it carries the fallback through:

h2 {
  font-family: {{ section.settings.heading_font }};
}

To set your own fallback chain, use .family (the chosen font) followed by your fallbacks:

h2 {
  font-family: {{ section.settings.heading_font.family }}, Georgia, 'Times New Roman', serif;
}

Since .family is the chosen font name and .fallback_families is its built-in fallback, this is equivalent to the full stack — handy when you build the declaration by hand:

h2 {
  font-family: {{ section.settings.heading_font.family }}, {{ section.settings.heading_font.fallback_families }};
}

Google Fonts load automatically. System font stacks require no external request.

Forms with storage schema

WebDYI supports form submissions through a storage schema type. When a user submits a form, the data is stored and can be viewed in the dashboard.

Important

You must claim the form in the dashboard for submissions to be collected. An unclaimed form will not store responses.

Schema example:


 "storage": [
    {
      "id": "contacts",
      "fields": [
        {
          "id": "name",
          "type": "text",
          "maxLength": 120
        },
        {
          "id": "email",
          "type": "email"
        },
        {
          "id": "phone",
          "type": "tel"
        },
        {
          "id": "subject",
          "type": "text",
          "maxLength": 120
        },
        {
          "id": "message",
          "type": "textarea",
          "maxLength": 2000
        },
        {
          "id": "consent",
          "type": "checkbox"
        }
      ]
    }
  ]
            

Template usage with {% form %}:

{% form "contact_form", "contact-form", class: "contact-form",
 success-message: "Thanks! We'll be in touch.", error-message: "Couldn't send.
  Please try again." %}
  <input type="text" name="name" placeholder="Name" required />
  <input type="email" name="email" placeholder="Email" required />
  <textarea name="message" placeholder="Message"></textarea>
  <button type="submit">Send</button>

  {{ form.success }}
  {{ form.error }}
{% endform %}

Form tag options

  • First argument (required): the collection id — must match a storage schema id
  • success-message / success_message: message shown after a successful submission
  • error-message / error_message: preset error text (overridden with the actual reason on failure)
  • redirect: URL to navigate to on success instead of showing an inline message
  • class: extra CSS classes (.webdyi-form is always added)
  • id: form element id

{{ form.success }} and {{ form.error }}

Inside a {% form %} block you can place {{ form.success }} and {{ form.error }} where you want the messages to appear. They start hidden and are revealed in place after submission. If you omit them, a message is appended next to the form automatically.

After publishing the widget, go to Dashboard → Widgets → [your widget] → Claim Form to start collecting submissions.

Block behavior

  • Instances reconcile against current block definitions.
  • Unknown removed block types are dropped.
  • Compatible saved values are kept; new fields get defaults.
  • Preset blocks seed first render.

Client customize-only mode

Tokenized edit URLs can open with customize-only view that hides code tabs and surfaces Save/Share actions.