Building themes

Setting types

Every setting type you can put in a schema, its JSON options, and the value your template receives.

How settings work

Each entry in a section's "settings" list becomes a field in the visual editor's settings panel. The owner fills it in. Your template reads the value from section.settings:

plang
{%- assign s = section.settings -%}
<h2>{{ s.heading }}</h2>

Plangku checks every value before your template sees it, so a number setting always gives a number and a checkbox always gives true or false. A missing value becomes an empty text, 0, false, an empty list or an empty link.

The same setting types are used in config/settings_schema.json. See Theme settings for the differences.

Options every setting has

KeyMeaning
typeRequired. One of the types below
idRequired. Starts with a letter, then letters, numbers and _, up to 40 characters. Must be unique in the section. Reserved ids are not allowed
labelThe name in the settings panel, up to 80 characters. Defaults to the id
defaultThe starting value. What is valid depends on the type
infoA line of help text under the field, up to 300 characters
placeholderGrey hint text, up to 120 characters. Only text and textarea use it
show_ifShow the field only when another field has a given value

A new section starts with every default. A section already on a page has no stored value for a setting you add later, so it renders with that setting's default (and the settings panel shows the default too) until the owner changes it. A value the owner cleared stays empty; use the default filter when even an emptied setting needs a fallback: {{ s.heading | default: 'Selamat datang' }}.

The types

TypeJSON optionsThe template receives
textplaceholderText
textarea, richtextplaceholder, rows (2 to 20, default 4)Text, with line breaks kept
numbermin, max, stepA number. 0 when empty
rangemin, max (required), step, unitA number inside min and max
colornoneA hex colour like #E11D48, or empty text
select, radiooptions (required)The chosen option's value as text
checkbox, togglenonetrue or false
image, image_pickernoneA safe image address, or empty text
link, urlnoneA link object (see below), or empty text
iconnoneThe icon's name as text
collectionnoneA collection with its items, or nothing. Sections only
listfields, item_label, max, add_labelA list of items

richtext is the same as textarea. There is no rich text editor. radio is the same as select, toggle the same as checkbox, image_picker the same as image and url the same as link.

text

json
{ "type": "text", "id": "heading", "label": "Heading", "default": "Kenapa pilih kami", "placeholder": "Tulis judul" }
plang
<h2>{{ s.heading }}</h2>

Output is HTML-escaped. Only default text of the right type is kept; anything else becomes an empty text.

textarea and richtext

json
{ "type": "textarea", "id": "text", "label": "Text", "rows": 3, "default": "Pesan sebelum jam 3 sore, sampai hari ini juga." }
plang
{{ s.text | paragraphs }}

Use the paragraphs or lines filter to turn line breaks into markup, or markdown for bold and lists. See Filters.

number

json
{ "type": "number", "id": "columns", "label": "Columns", "min": 1, "max": 4, "step": 1, "default": 3 }
plang
<div class="grid grid-{{ s.columns }}">

range

A slider. min and max are required, and max must be larger. step is 1 if you leave it out. A default outside the range is pulled inside it. With no default, the value is min.

json
{ "type": "range", "id": "image_height", "label": "Photo height", "min": 200, "max": 600, "step": 20, "unit": "px", "default": 360 }
plang
<img style="height: {{ s.image_height }}px" src="{{ s.photo }}" alt="">

unit (up to 8 characters) is only a label in the panel. The template receives the plain number.

color

json
{ "type": "color", "id": "badge_color", "label": "Badge colour", "default": "#E11D48" }
plang
{% if s.badge_color != blank %}<span style="background: {{ s.badge_color }}">Promo</span>{% endif %}

A default must be #RGB or #RRGGBB. Otherwise the default is empty.

select and radio

options is a list of text values, or of { "value", "label" } objects. You can have up to 50. The default must match one of the values, or the first option is used.

json
{
	"type": "select",
	"id": "layout",
	"label": "Layout",
	"default": "left",
	"options": [
		{ "value": "left", "label": "Photo on the left" },
		{ "value": "right", "label": "Photo on the right" }
	]
}
plang
<div class="layout-{{ s.layout }}">

checkbox and toggle

json
{ "type": "checkbox", "id": "show_badge", "label": "Show the badge", "default": true }
plang
{% if s.show_badge %}<span class="badge">Terlaris</span>{% endif %}

Only "default": true turns it on. Anything else is off.

image and image_picker

The owner picks or uploads a photo. A default can be an https:// address, an uploaded path, or assets/<file> for an image that came with a theme package.

json
{ "type": "image", "id": "photo", "label": "Photo" }
plang
{% render 'image', src: s.photo, alt: s.heading, class: 'aspect-[4/3] w-full rounded-site' %}

The image platform snippet draws a placeholder when the photo is empty. Only https:// and /uploads/ addresses pass. Anything else becomes empty text.

The owner types or picks a destination. The stored value can be wa:Halo, saya mau pesan (WhatsApp with a message), page:menu (a page), #kontak (an anchor), tel:, mailto: or a web address.

json
{ "type": "link", "id": "button_link", "label": "Button link", "default": "wa:Halo, saya mau pesan" }

The template gets a link object. Writing {{ s.button_link }} prints the address. You can also read:

PropertyValue
.kindwhatsapp, page, anchor, phone, email or url
.externaltrue for WhatsApp links and http(s) addresses
.hrefThe address
plang
{% if s.button_link != blank %}
<a href="{{ s.button_link }}"{% if s.button_link.external %} target="_blank" rel="noopener noreferrer"{% endif %}>Pesan lewat WhatsApp</a>
{% endif %}

The button platform snippet does this for you: {% render 'button', label: 'Pesan', link: s.button_link %}.

icon

json
{ "type": "icon", "id": "icon", "label": "Icon", "default": "ShieldCheck" }
plang
{{ s.icon | icon: class: 'size-6' }}

The value is the icon's name, as text. The icon filter turns it into an inline SVG.

collection

Lets the owner choose one of the website's collections, such as products or menu items. It cannot be used inside a list or in theme settings. The template gets the collection (name, slug, url and items).

json
{ "type": "collection", "id": "products", "label": "Products to show" }
plang
{% for item in s.products.items %}
<h3>{{ item.title }}</h3>
<p>{{ item.price | money }}</p>
{% endfor %}

See Collections and data for the fields on an item.

list

A repeatable group of fields, such as cards or FAQ rows. fields lists the fields of one item.

KeyMeaning
fieldsRequired. Up to 20 simple settings. A list cannot contain a list or a collection
item_labelThe id of the field whose value names each item in the panel. Defaults to the first text field
maxThe most items allowed, 1 to 60. Default 24
add_labelThe "add" button text, up to 40 characters. Default "Add item"
defaultA list of items to start with
json
{
	"type": "list",
	"id": "faqs",
	"label": "Questions",
	"item_label": "question",
	"max": 12,
	"add_label": "Add question",
	"fields": [
		{ "type": "text", "id": "question", "label": "Question", "default": "Apakah bisa kirim ke luar kota?" },
		{ "type": "textarea", "id": "answer", "label": "Answer", "default": "" }
	],
	"default": [
		{ "question": "Apakah bisa kirim ke luar kota?", "answer": "Bisa. Kami kirim lewat JNE dan J&T." },
		{ "question": "Bagaimana cara bayar?", "answer": "Transfer bank atau QRIS." }
	]
}
plang
{% for faq in s.faqs %}
<details>
	<summary>{{ faq.question }}</summary>
	<p>{{ faq.answer }}</p>
</details>
{% endfor %}

Each item has the fields you defined, already checked by type. Items added in the editor start with each field's default. An item saved before you added a field has no value for it: the template gets empty text (or 0, false), never the sample default, so a placeholder price can't appear on a real item.

Showing a field only sometimes

show_if hides a field until another field has one of the listed values. The listed values are text or booleans.

json
{ "type": "select", "id": "layout", "label": "Layout", "default": "split", "options": ["split", "centered"] },
{ "type": "image", "id": "photo", "label": "Photo", "show_if": { "field": "layout", "is": ["split"] } },
{ "type": "checkbox", "id": "show_price", "label": "Show price", "default": true },
{ "type": "text", "id": "price_note", "label": "Price note", "show_if": { "field": "show_price", "is": [true] } }

The hidden field's value is still stored and still reaches the template. Use {% if %} there as well when it matters.