Components
The starter kit ships interactive components as small custom elements in assets/theme.js (no framework, about 10 KB unminified). The modal, drawer and accordion have a snippet that writes their markup; tabs and the carousel are plain markup around a custom element. They work with keyboard and screen readers, follow the visitor's reduced-motion preference, and keep working when the theme editor re-renders a section.
Load the script at the end of your layout's <body>:
{{ 'theme.js' | asset_url | script_tag }}
Modal
{% capture body %}<p>Anything you like.</p>{% endcapture %}
{% render 'modal', id: 'info-modal', title: 'More information', content: body, width: '40rem' %}
<button type="button" class="btn" data-dialog-open="info-modal" aria-haspopup="dialog">Learn more</button>
Any element with data-dialog-open="<id>" opens it. It closes with Esc, a click on the backdrop, or any [data-dialog-close] inside. Focus returns to the button that opened it, and the page doesn't scroll behind it.
Embedded players should load only when the modal opens: give the <iframe> a data-src instead of a src. The modal sets it on open and removes it on close, which also stops the video. Counsel's Image with text section shows this with a Video setting:
{%- assign embed = section.settings.video_url | video_embed_url -%}
<iframe class="aspect-video w-full" data-src="{{ embed | escape }}" allow="autoplay; fullscreen" allowfullscreen></iframe>
Drawer
A panel that slides in from the side, used for the mobile menu in sections/header.liquid:
{% render 'drawer', id: 'menu-drawer', title: 'Menu', content: menu_html, side: 'left' %}
<button type="button" data-dialog-open="menu-drawer" aria-controls="menu-drawer" aria-expanded="false">Menu</button>
side is right (default) or left. Buttons that open it get aria-expanded kept up to date.
Accordion
Native <details>: it works without JavaScript, and rows with the same group open one at a time.
{% for block in section.blocks %}
{% capture answer %}<div class="rte">{{ block.settings.answer }}</div>{% endcapture %}
{% render 'accordion-item', title: block.settings.question, content: answer, group: section.id, attributes: block.theme_attributes %}
{% endfor %}
Also accepts meta (text before the icon) and open: true.
Tabs
<theme-tabs>
<div role="tablist" aria-label="Plans">
<button role="tab" aria-controls="tab-monthly">Monthly</button>
<button role="tab" aria-controls="tab-yearly">Yearly</button>
</div>
<div role="tabpanel" id="tab-monthly">…</div>
<div role="tabpanel" id="tab-yearly">…</div>
</theme-tabs>
Arrow keys, Home and End move between tabs. Without JavaScript, the tab buttons are hidden and every panel shows.
Carousel
<theme-carousel>
<div data-carousel-track class="flex snap-x snap-mandatory gap-6 overflow-x-auto" aria-label="Testimonials" tabindex="0">
{% for block in section.blocks %}
<div class="w-[82%] shrink-0 snap-start sm:w-[45%] lg:w-[30%]" {{ block.theme_attributes }}>…</div>
{% endfor %}
</div>
<button type="button" data-carousel-prev aria-label="Previous">‹</button>
<button type="button" data-carousel-next aria-label="Next">›</button>
</theme-carousel>
A scroll-snapping row: [data-carousel-track] is the row, and the [data-carousel-prev] and [data-carousel-next] buttons scroll it by one item and are disabled at either end. Without JavaScript it still scrolls by swipe, trackpad or keyboard. Counsel has no carousel snippet, so write the markup yourself.
Motion and animations
Animations are run by the platform, not by your theme's scripts, so they work in every theme, even with theme JavaScript switched off. The firm picks an effect per section and block in the editor (Platform settings), and the platform marks up the result:
| Markup | Where | Meaning |
|---|---|---|
data-animate="<effect>" |
Section wrapper, or a block's element through {{ block.theme_attributes }} |
Plays <effect> once as the element scrolls into view. |
data-animate-stagger="<effect>" |
Section wrapper | The section doesn't animate itself. Its animated blocks and its data-animate-item elements are revealed one after another, items with the section's effect. |
data-animate-item |
Your markup | A repeated item (a card, a testimonial) that takes part in a staggered section. Put it on each item's outer element. |
Effects are fade, fade-up, zoom-in, slide-start and slide-end (the slides follow the page's dir). Each plays as a keyframe animation that only touches opacity, translate and scale, so your own transition and transform rules keep working.
How it runs. The platform's script adds the class theme-motion to <html>. Animated elements stay hidden until they get the class is-in as they come into view. Without JavaScript, when the theme's Motion setting is off, or when the visitor prefers reduced motion, theme-motion is never added and everything shows straight away. Don't hide content with your own CSS in a way that depends on is-in.
Tuning. These CSS custom properties are set on :root, with larger values under html[data-motion="expressive"]. Override them in your stylesheet to change the feel:
| Property | Subtle | Expressive |
|---|---|---|
--motion-duration |
600ms |
900ms |
--motion-distance (how far fade-up and slides travel; slides go twice as far) |
16px |
40px |
--motion-scale (where zoom-in starts) |
0.96 |
0.88 |
--motion-step (delay between staggered items) |
80ms |
120ms |
--motion-ease |
cubic-bezier(0.22, 1, 0.36, 1) |
the same |
Staggered items get --motion-index (0, 1, 2… up to 12), and their delay is --motion-index × --motion-step.
To restyle one effect, or add an effect of your own to a section you control, target the same selectors:
@keyframes my-theme-rise {
from { opacity: 0; translate: 0 2rem; scale: 0.98; }
}
html.theme-motion .my-hero[data-animate='fade-up'].is-in {
animation-name: my-theme-rise;
}
Images. Lazy-loaded images (loading="lazy") inside sections fade in once they've loaded. Images already loaded are left alone.
Page transitions. With Page transitions on (and Motion not off), the platform adds @view-transition { navigation: auto; }, so browsers that support cross-document view transitions cross-fade between pages. Give elements a view-transition-name in your CSS to animate them between pages, such as a card's image into the hero of the page it links to.
In the theme editor, everything shows at once so the firm can edit. When the firm changes a section's or block's effect, that section plays its animation once. Your scripts don't need to do anything for this.
Your own components
Write custom elements the same way. They need no setup when the theme editor adds or replaces a section. For other scripts, the theme editor fires these events on the section or block element (they bubble to document):
| Event | When |
|---|---|
theme:section:load |
A section was added or re-rendered: set it up again. |
theme:section:unload |
A section is about to be removed or re-rendered: clean up. |
theme:section:reorder |
Sections were reordered. |
theme:section:select |
The firm selected a section in the editor. |
theme:block:select |
The firm selected a block: reveal it (open its slide, tab, modal…). |
document.addEventListener('theme:section:load', (event) => initSliders(event.target));
The starter kit's script already opens the modal, drawer, accordion row or tab that contains a selected block. request.design_mode is true inside the theme editor, if your Liquid needs to know.
Still need help?
Send a request to the Mattersimple team and we'll reply by email.