Building themes

Styling

Theme CSS in assets/*.css and {% style %}: how selectors are scoped, what is removed, design tokens, helper classes and container queries.

Theme CSS comes in two places:

  • assets/*.css: styles for the whole website, loaded on every page in file-name order. A theme can have 12 CSS files.
  • {% style %} ... {% endstyle %} inside a section file: styles for that section only, loaded on the pages that use the section.

Both are plain CSS. Theme CSS can restyle the website but never the editor around it, because every selector is rewritten to sit under the website (see below). Snippets can't have {% style %}.

How selectors are scoped

Plangku rewrites your selectors when the theme is saved:

You writeIn assets/*.css it becomesIn a section's {% style %} it becomes
.card.site-root .card[data-code="faq"] .card
h2, .lead.site-root h2, .site-root .lead[data-code="faq"] h2, [data-code="faq"] .lead
:root, html, body.site-root[data-code="faq"] (the section itself)
:scope, &.site-root[data-code="faq"]
&:hover, :scope.is-open.site-root:hover, .site-root.is-open[data-code="faq"]:hover, [data-code="faq"].is-open
body > main.site-root > main[data-code="faq"] > main
.site-root .xleft as written[data-code="faq"] .site-root .x (matches nothing)

faq stands for the section's file name. The data-code attribute sits on the section's own wrapper element, so inside a section's CSS :scope or & means the whole section, and everything else is a descendant of it. A .promo rule never touches another section's .promo, so short class names are safe in section CSS. In assets/*.css use more specific names.

  • Nested rules (native CSS nesting) are left alone: only the outer rule is scoped, and the inner rules sit inside it.
  • Rules inside @media, @container and @supports are scoped the same way.
  • @keyframes bodies are not touched.
  • :root { --brand-soft: #fde8e8; } in an asset file is a handy way to declare your own variables for the whole website.
  • Theme CSS is not put in a cascade layer, so it normally wins over the platform's utility classes without !important.

Keyframes and fonts

Names you define are prefixed with t- so they can't collide with the editor's own spin animation or fonts, and every use inside your theme is renamed to match.

css
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.card { animation: rise .4s ease both; }   /* becomes t-rise */

@font-face {
  font-family: 'Gelora';                      /* becomes "t-Gelora" */
  src: url(https://fonts.example.com/gelora.woff2) format('woff2');
  font-display: swap;
}
.site-heading { font-family: 'Gelora', serif; }   /* follows the rename */

Uploaded fonts and images work through https:// or /uploads/ addresses. To change the theme's two main fonts (heading and body) use the Theme tab, which loads Google Fonts for you, since @import is not allowed.

Allowed and removed

Allowed at-rules: @media, @container, @supports, @keyframes (and @-webkit-keyframes), @font-face and @layer. A @layer block is unwrapped: its rules are kept and the layer is dropped, because a layer would sink your rules below the platform's own CSS. A bare @layer a, b; line is removed.

Removed, with a warning in the editor:

WhatWhy
@import, @page, @property, @charset, @namespace, @scope... any other at-ruleNot in the allowed list. For fonts use the Theme tab or @font-face
A declaration with url(...) that isn't https://..., /uploads/... or data:image/...Relative and http: addresses can't be trusted. The whole declaration is removed
behavior, -moz-binding, expression(...), javascript:Old script hooks

@import and the others are removed one at a time: the rest of the file works. The same URL rules apply to inline style="..." attributes in your markup (which are also cut to 4000 characters).

Warnings in the editor

Warnings are listed in the code editor's problem list with the file and line, and don't stop you from saving. A CSS syntax mistake is an error (CSS error: ...) and does stop the save.

MessageWhat happened
@import isn’t allowed in theme CSS and was left out (link fonts in theme settings, or add the CSS to a file here).The rule was removed
@page isn’t allowed in theme CSS and was left out.The same for any other at-rule
Images and fonts in CSS must use https:// addresses (or uploaded files). This line was left out.A url() was not allowed
“behavior: url(x.htc)” isn’t allowed and was left out.A script hook was removed

Design tokens

The website and each section expose their colours and settings as CSS variables. Use them instead of fixed colours and your theme follows whatever the owner picks in the Theme tab.

On the whole website

VariableValue
--c-brand, --c-brand-fgThe brand colour, and a readable text colour on it
--c-accent, --c-accent-fgThe accent colour, and text on it
--c-base, --c-surfaceThe page colour and the soft background colour
--c-inkThe main text colour
--c-inverse, --c-inverse-fgThe dark colour, and text on it
--f-heading, --f-bodyThe font stacks
--w-headingThe heading weight: 500 to 800
--rCorner radius for cards and photos: 0px, 6px, 12px, 20px or 28px
--r-btnButton corner radius: 4px, 12px or 999px

In each section

Every section sets its own colours from its background choice (page colour, soft colour, brand colour, dark, or a photo). They always read well together.

VariableValue
--s-bgThe section's background
--s-fgText on it
--s-mutedSofter text, still readable
--s-lineA subtle border colour
--s-cardThe colour of cards inside the section
--s-hlA highlight for prices, labels and icons: the brand colour, or a deeper shade when the brand colour is too light to read as text
--s-hl-fgText on top of --s-hl
--s-btn, --s-btn-fgButton background and its text
--s-btn-softA tinted button background (used by the "soft" button style)
css
.price { color: var(--s-hl); }
.card { background: var(--s-card); border: 1px solid var(--s-line); border-radius: var(--r); }
.card p { color: var(--s-muted); }
.cta { background: var(--s-btn); color: var(--s-btn-fg); border-radius: var(--r-btn); }

For a block inside a section that should have another background, write style="{{ 'inverse' | section_vars }}" on it (see filters).

Theme setting variables

Theme settings of the types color, number, range, select and checkbox (from config/settings_schema.json) are published as CSS variables named --ts-<id>, with _ in the id turned into -:

SettingVariableValue
accent_color (color)--ts-accent-color#D62828
card_lift (range, unit px)--ts-card-lift4px (the unit is added for ranges)
columns (number)--ts-columns3
font_style (select)--ts-font-stylethe chosen value, when it only has letters, digits, spaces and _ . # % ( ) , -
rounded_images (checkbox)--ts-rounded-images1 or 0

Text, image and link settings are not published; read them in templates with settings.<id>. Give variables a fallback so the CSS also works before the owner changes anything: var(--ts-card-lift, 4px).

Helper classes

The platform's own classes you can use in your markup so your sections match the built-in ones:

ClassWhat it does
site-wrapA centred content column, at most 72rem wide, with side padding (1.25rem, and 2rem when the website is 48rem or wider)
site-headingThe theme's heading font and weight, tight line height. Capitals when the theme says so
site-titlePut it on section titles. The theme's "accent bar under headings" option adds its bar here
site-cardPut it on cards so they follow the theme's card style (flat, outlined, or lifted with a shadow). It adds no background of its own, so give the card background: var(--s-card). When the owner picks outlined cards the platform removes the card's background
site-mediaPut it on feature photos so they follow the theme's photo style (arched or framed)
site-btn-primaryPut it on a main button so it follows the theme's button style (tinted, outlined or bold). The button snippet already does
site-keep-caseKeeps text in normal case when the theme makes headings capitals (for prices: "Rp", not "RP")
rounded-site, rounded-btnThe theme's corner radius for cards and for buttons
bg-s-bg, bg-s-card, bg-s-hl, bg-s-btnBackgrounds from the section tokens
text-s-fg, text-s-muted, text-s-hl, text-s-hl-fg, text-s-btn-fgText colours from the section tokens
border-s-lineThe section's border colour
font-heading, font-bodyThe theme fonts

Classes that exist

Warning: the website's stylesheet only contains Tailwind utility classes that the platform's own components already use. A class like grid-cols-7 or mt-13 has no CSS unless some built-in section uses it. Don't rely on utility classes you haven't seen working: write your own classes in {% style %} or assets/*.css. The helper classes in the table above are always there.

Container queries

The website's root element is a CSS container, so sizes follow the width of the website, not the browser window. This matters in the editor: the phone and tablet previews narrow the website, but not the browser, so @media (min-width: ...) would never change. Use @container:

css
.cards { display: grid; gap: 1rem; }

@container (min-width: 48rem) {
  .cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

The breakpoints the built-in sections use are 48rem (tablet and up), 56rem and 64rem. Mobile first: write the phone layout without a query, then add queries for wider sizes.

Restyling built-in sections

Every section's wrapper has data-section="<type>", so assets/*.css can restyle a built-in section without replacing it. The types are hero, about, features, services, products, menu, pricing, testimonials, gallery, faq, cta, contact, stats, steps, team, logos, marketplace, text, article and video, plus form, collection, entry, inquiry, ai, imported and code. Sections from your own files have data-section="code" and data-code="<handle>".

css
[data-section="hero"] h1 { letter-spacing: -0.03em; }
[data-section="features"] .site-card { transition: transform .2s; }
[data-section="features"] .site-card:hover { transform: translateY(-4px); }
[data-section="pricing"] .site-card { border-top: 4px solid var(--c-brand); }

Check the real class names with your browser's inspector on the live site. For anything a stylesheet can't do, replace the section with your own file of the same name (a hero.plang replaces the built-in hero everywhere; see sections).

The theme's style options are attributes on the website root, so you can react to them: data-heading-case, data-heading-style, data-buttons, data-cards and data-images:

css
.site-root[data-cards="shadow"] .card { box-shadow: 0 10px 30px -15px rgb(0 0 0 / .3); }
Note: :root, html and body all mean the website root, so :root[data-cards="shadow"] .card and .site-root[data-cards="shadow"] .card are the same; html body > main becomes .site-root > main.

See examples for a complete assets/theme.css.