How to make a Shopify section editable in the theme editor
If changing a word on your store means opening the code, the section isn't finished. This guide shows how to move every word, image and colour into settings the merchant can edit.
Key takeaways
- Anything typed directly into the markup can only be changed in code; a setting makes it editable in the theme editor.
- Pick the setting type for the job:
textfor short lines,richtextfor formatted paragraphs,image_pickerfor images,urlfor links. - Give text settings a
default, escape plain text with| escape, and wrap optional content in!= blankchecks. - Use
headerandparagraphentries andinfohints to keep a long sidebar readable.
Plenty of custom Shopify sections look right and still frustrate the people who run the store, because the heading, the button label and the background image are typed straight into the code. Every change becomes a developer task. Making a section editable means replacing each of those values with a setting: an input in the theme editor sidebar whose value the Liquid reads at render time.
Hard-coded text vs settings
A setting has two halves. In the section's {% schema %}, you declare it: its type, a unique id, a label the merchant sees, and usually a default. In the markup, you read it with section.settings.<id> (or block.settings.<id> inside a block). Shopify stores the merchant's value in the template's JSON, so the section file itself never changes when they edit.
The question to ask of every string and asset in a section is simple: would the store owner ever want to change this? Headings, body copy, button labels, links, images, colours and the occasional on/off toggle almost always qualify. Class names, ARIA roles and structural markup do not.
The setting types that matter
Shopify offers more input types than most sections need. These are the ones you will reach for most, and when each is the right choice.
| Type | Use it for | Notes |
|---|---|---|
text | Headings, labels, button text | Single line. Escape it when you output it. |
textarea | Plain multi-line text | No formatting; line breaks need newline_to_br. |
richtext | Paragraphs with bold, italics and links | Outputs HTML. A default must be wrapped in <p> or <ul>. |
inline_richtext | A heading with one bold or linked word | Formatting without paragraph tags. |
image_picker | Any image | No default; check != blank and render with image_url. |
url | Button and card links | Lets the merchant pick a page, product or collection. |
color | Backgrounds, text, accents | Returns a colour; use in inline CSS variables. |
range | Spacing, columns, overlay opacity | Needs min, max, step and a default. |
select | A fixed set of choices | Layout, alignment, size. Give options a value and label each. |
checkbox | Show or hide something | Returns true or false. |
product / collection | Featured product or collection | Returns the object, so you can read its title, price, images. |
video | A video uploaded to Shopify | Use video_url instead for YouTube or Vimeo links. |
font_picker | Choosing a typeface | Requires a default font handle. |
Default values
A default is what the setting holds when the section is first added. Without one, a freshly added section shows empty space and the merchant has to guess what goes where. Write defaults as realistic placeholder copy, not "Lorem ipsum" and not "Heading": "Free delivery over £50" tells the merchant what the field is for in a way a label alone does not.
A few types are strict about defaults: range requires one within its min/max, font_picker requires one, richtext defaults must be valid wrapped HTML, and image_picker cannot have one at all. An invalid default is one of the reasons Shopify refuses to save a section file.
Organise the sidebar with header, paragraph and info
Once a section has fifteen settings, order and grouping matter as much as the settings themselves. Two sidebar-only entries help:
- `header` adds a heading between groups of settings ("Content", "Button", "Colours", "Layout"). It stores nothing.
- `paragraph` adds a line of explanatory text, useful for a short instruction at the top of a group.
- `info` is an attribute on an ordinary setting that adds a hint under the input, such as "Recommended size: 2400 × 1000 px".
Put the settings the merchant will change most often first, usually the content, and leave layout and spacing at the bottom. Labels matter too: write them in the merchant's words ("Button link", not "cta_href"), keep them short, and use the same label for the same job across every section in the theme. A merchant who learns one section's sidebar should be able to find their way around the next one without guessing.
Be careful when renaming a setting's id after a section is in use. Shopify stores values against the id, so a renamed id starts empty on every page that already uses the section, and the old value is lost from the editor. Change labels freely; treat ids as permanent.
Translating labels with t: keys
Themes sold to many stores translate their editor labels. Instead of "label": "Heading", they write "label": "t:sections.banner.settings.heading.label" and put the text in locales/en.default.schema.json and its sibling files. For a section built for one store, plain labels are fine. If you add a section to a theme that uses t: keys everywhere, matching that convention keeps the editor consistent in every admin language.
Escaping and blank checks
Two habits keep editable sections safe and tidy:
- Escape plain text.
{{ section.settings.heading | escape }}turns a stray<or&into text instead of markup. Do not escaperichtextorinline_richtext; they are meant to output HTML. - Check before you wrap.
{% if section.settings.button_label != blank %}stops an empty setting from leaving an empty<a>or<h2>on the page, which looks odd and confuses screen readers.
Worked example: a hard-coded banner made editable
Here is a promotional banner as it often arrives: everything typed in, no settings at all.
<section class="promo-banner" style="background:#1f3a2e;">
<img src="{{ 'autumn-sale.jpg' | asset_url }}" alt="Autumn sale">
<h2>Autumn sale: 20% off outerwear</h2>
<p>Ends Sunday at midnight.</p>
<a href="/collections/outerwear" class="button">Shop outerwear</a>
</section>
{% schema %}
{
"name": "Promo banner",
"presets": [{ "name": "Promo banner" }]
}
{% endschema %}And the same banner with every shopper-facing value moved into settings, grouped in the sidebar, with defaults, escaping and blank checks:
<section
class="promo-banner promo-banner--{{ section.settings.alignment }}"
style="--promo-bg: {{ section.settings.background }}; --promo-text: {{ section.settings.text_color }};"
>
{%- if section.settings.image != blank -%}
{{
section.settings.image
| image_url: width: 2400
| image_tag: loading: 'lazy', widths: '600, 1200, 1800, 2400', alt: section.settings.image.alt
}}
{%- endif -%}
{%- if section.settings.heading != blank -%}
<h2>{{ section.settings.heading | escape }}</h2>
{%- endif -%}
{%- if section.settings.text != blank -%}
<div class="promo-banner__text">{{ section.settings.text }}</div>
{%- endif -%}
{%- if section.settings.button_label != blank and section.settings.button_link != blank -%}
<a href="{{ section.settings.button_link }}" class="button">
{{- section.settings.button_label | escape -}}
</a>
{%- endif -%}
</section>
{% schema %}
{
"name": "Promo banner",
"settings": [
{ "type": "header", "content": "Content" },
{ "type": "image_picker", "id": "image", "label": "Image", "info": "Recommended width: 2400 px" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "Autumn sale: 20% off outerwear" },
{ "type": "richtext", "id": "text", "label": "Text", "default": "<p>Ends Sunday at midnight.</p>" },
{ "type": "header", "content": "Button" },
{ "type": "text", "id": "button_label", "label": "Label", "default": "Shop outerwear" },
{ "type": "url", "id": "button_link", "label": "Link" },
{ "type": "header", "content": "Style" },
{ "type": "color", "id": "background", "label": "Background", "default": "#1f3a2e" },
{ "type": "color", "id": "text_color", "label": "Text", "default": "#ffffff" },
{
"type": "select",
"id": "alignment",
"label": "Alignment",
"default": "center",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Centre" }
]
}
],
"presets": [{ "name": "Promo banner" }]
}
{% endschema %}What changed: the image comes from the store's files rather than the theme's assets folder, so swapping it no longer needs a code change, and image_tag with widths serves a sensible size to each screen. The alt text comes from the image itself. Colours flow through CSS variables, so the stylesheet stays in one place. The button only renders when it has both a label and a link. Nothing on the page is fixed any more except the structure.
Check your work
- Add the section to a page in the theme editor and change every setting once. Each change should show in the preview straight away.
- Clear each optional setting and confirm nothing broken or empty is left behind.
- Paste text with a
<or an ampersand into atextfield and confirm it shows as text. - Run the schema through the section schema generator to catch duplicate IDs and invalid defaults before uploading.
Getting maximum editability without the typing
Doing this by hand for every section is tedious, which is why so many custom sections skip it. Klorr has an editability slider for exactly this: at Maximum, every shopper-facing word in the section it writes becomes a setting, grouped and given a default, alongside presets and blocks. You can start from the Shopify Liquid code generator, then follow how to add a custom section to Shopify to install the result.
Questions, answered
How do I make text editable in a Shopify section?
Declare a text setting in the section's schema with an id, a label and a default, then replace the hard-coded text in the markup with section.settings.your_id, escaped with | escape. The text then appears as an input in the theme editor sidebar.
What is the difference between text, textarea and richtext in Shopify?
text is a single plain line, ideal for headings and labels. textarea is plain multi-line text without formatting. richtext gives the merchant a small editor with bold, italics and links and outputs HTML, so it should not be escaped.
Why can't I set a default image in a Shopify section?
The image_picker setting doesn't accept a default value. Check whether the setting is blank in your Liquid and either render nothing or show a placeholder, for example with the placeholder_svg_tag filter.
How do I add a heading to group settings in the theme editor?
Add an entry with "type": "header" and a "content" value to the settings array where you want the group to start. It only appears in the sidebar and stores no value. A "paragraph" entry works the same way for a line of explanation.
Do I need to escape Shopify section settings?
Escape plain text settings such as text and textarea with | escape, so characters like < and & display as text. Don't escape richtext or inline_richtext, which are meant to output HTML.