Skip to content

Theme editor

The theme editor is where sellers actually meet your theme. A theme that renders perfectly on the storefront but fights the editor is a theme sellers abandon.

Mark up your blocks

Output block.youcan_attributes on the outermost element you render for each block. It emits the data-youcan-editor-block attribute the editor uses to map a DOM node back to a block, which is what makes a block selectable and updatable in place.

liquid
{% for block in section.blocks %}
  {% case block.type %}
    {% when 'slide' %}
      <li class="slide" {{ block.youcan_attributes }}>
        {{ block.settings.heading }}
      </li>
  {% endcase %}
{% endfor %}

Without it, clicking the block in the preview does nothing and every settings change forces a full section reload.

The section wrapper is handled for you. The renderer emits it with the section's id, class, and editor attributes already attached, so there's nothing to add at the section level.

Render an empty state

A section is added before it's configured. If your section renders nothing when it has no blocks or an unset image, the seller sees blank space with nothing to click, and no way to select the section they just added.

liquid
{% if section.blocks.size == 0 %}
  <p class="placeholder">{{ 'sections.slideshow.empty' | t }}</p>
{% else %}
  {% for block in section.blocks %}
    ...
  {% endfor %}
{% endif %}

Do the same for individual settings. Fall back to a placeholder image rather than a broken <img>:

liquid
{% if section.settings.image.src %}
  <img src="{{ section.settings.image.src }}" alt="{{ section.settings.image.alt }}">
{% else %}
  {% render 'misc.image-fallback' %}
{% endif %}

Write JavaScript that survives re-rendering

Sections are torn down and re-rendered as a seller edits. Code that runs once on DOMContentLoaded and caches element references will be pointing at detached nodes within seconds.

  • Scope initialization to the section's wrapper, not the document.
  • Guard every lookup. The element may not exist yet, or may have just been removed.
  • Unsubscribe from any pub/sub or global listener when the element goes away.
  • Don't assume your section appears once. The same section can be on the page twice with different settings.
liquid
{% javascript %}
  const root = document.getElementById('youcan-section--{{ section.id }}');
  if (!root) return;

  const trigger = root.querySelector('[data-toggle]');
  trigger?.addEventListener('click', () => root.classList.toggle('is-open'));
{% endjavascript %}

Detect the editor when you need to

request.design_mode is true while the theme is being customized. Use it sparingly, to suppress things that get in the seller's way:

liquid
{% unless request.design_mode %}
  <script>startAutoplayCarousel()</script>
{% endunless %}

Autoplaying carousels, countdown timers, one-time popups, and scroll-triggered animations are the usual candidates. What you shouldn't do is change what the page looks like, since the whole point of the preview is that it matches the storefront.

Name settings for sellers

Setting labels are UI. Write them the way you'd write any interface copy: concrete, short, and in the seller's vocabulary rather than yours. "Products per row" beats "grid_cols".

Group related settings with header display settings, and use info for the caveat that would otherwise become a support question:

json
{ "type": "header", "content": "t:common.headers.layout" },
{
  "type": "range",
  "id": "products_per_row",
  "label": "t:sections.collection.products_per_row",
  "info": "t:sections.collection.products_per_row_info",
  "min": 2, "max": 5, "step": 1, "default": 4
}

Use t: locale keys throughout so the editor is usable in the seller's language. See schema locale files.

Constrain the configuration space

Every setting is a way for a seller to make the theme look wrong. Prefer a select with the three layouts you've designed and tested over a set of free-number inputs that combine into layouts you haven't.

Use visible_if to hide settings that don't apply, so the panel only ever shows what's currently relevant:

json
{
  "type": "color",
  "id": "overlay_color",
  "label": "t:common.colors.overlay",
  "visible_if": { "setting_id": "show_overlay", "value": true }
}

Set templates, limit, and max_blocks on the section schema so a section can't be added where it makes no sense or repeated until the page breaks. See Sections and blocks.