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 write | In assets/*.css it becomes | In 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 .x | left 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,@containerand@supportsare scoped the same way. @keyframesbodies 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.
@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:
| What | Why |
|---|---|
@import, @page, @property, @charset, @namespace, @scope... any other at-rule | Not 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.
| Message | What 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
| Variable | Value |
|---|---|
--c-brand, --c-brand-fg | The brand colour, and a readable text colour on it |
--c-accent, --c-accent-fg | The accent colour, and text on it |
--c-base, --c-surface | The page colour and the soft background colour |
--c-ink | The main text colour |
--c-inverse, --c-inverse-fg | The dark colour, and text on it |
--f-heading, --f-body | The font stacks |
--w-heading | The heading weight: 500 to 800 |
--r | Corner radius for cards and photos: 0px, 6px, 12px, 20px or 28px |
--r-btn | Button 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.
| Variable | Value |
|---|---|
--s-bg | The section's background |
--s-fg | Text on it |
--s-muted | Softer text, still readable |
--s-line | A subtle border colour |
--s-card | The colour of cards inside the section |
--s-hl | A 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-fg | Text on top of --s-hl |
--s-btn, --s-btn-fg | Button background and its text |
--s-btn-soft | A tinted button background (used by the "soft" button style) |
.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 -:
| Setting | Variable | Value |
|---|---|---|
accent_color (color) | --ts-accent-color | #D62828 |
card_lift (range, unit px) | --ts-card-lift | 4px (the unit is added for ranges) |
columns (number) | --ts-columns | 3 |
font_style (select) | --ts-font-style | the chosen value, when it only has letters, digits, spaces and _ . # % ( ) , - |
rounded_images (checkbox) | --ts-rounded-images | 1 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:
| Class | What it does |
|---|---|
site-wrap | A centred content column, at most 72rem wide, with side padding (1.25rem, and 2rem when the website is 48rem or wider) |
site-heading | The theme's heading font and weight, tight line height. Capitals when the theme says so |
site-title | Put it on section titles. The theme's "accent bar under headings" option adds its bar here |
site-card | Put 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-media | Put it on feature photos so they follow the theme's photo style (arched or framed) |
site-btn-primary | Put it on a main button so it follows the theme's button style (tinted, outlined or bold). The button snippet already does |
site-keep-case | Keeps text in normal case when the theme makes headings capitals (for prices: "Rp", not "RP") |
rounded-site, rounded-btn | The theme's corner radius for cards and for buttons |
bg-s-bg, bg-s-card, bg-s-hl, bg-s-btn | Backgrounds from the section tokens |
text-s-fg, text-s-muted, text-s-hl, text-s-hl-fg, text-s-btn-fg | Text colours from the section tokens |
border-s-line | The section's border colour |
font-heading, font-body | The theme fonts |
Classes that exist
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:
.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>".
[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:
.site-root[data-cards="shadow"] .card { box-shadow: 0 10px 30px -15px rgb(0 0 0 / .3); }: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.