Appmint ships a small set of web components, the wm-* elements. They are plain HTML tags: write the tag with the children it expects and it builds itself on the published page. This skill covers each one completely, the buttons and code that drive them, and the blocks Build Studio inserts.
It sits on top of Build a website page, which covers the page record, its screens and its URL. For what the page runtime itself can do (data-source, data-bind, data-action, window.appmint), see The Appmint JS engine on a page.
Ask first
- Which page, on which site? You edit that page's active screen. If it's a new page, make it first with Build a website page.
- What should the visitor be able to do? For example, open a menu, read an FAQ, flip through photos, filter cards, or pick a size and add to cart. Each need maps to one row of the table in step 1.
- What goes inside it? Use the person's own words, links and images. Don't invent menu items, prices or answers.
- Is the site dark or light? Drawers, dialogs and the product control default to a white panel (step 3).
- Header or page? A menu that should be on every page belongs in the site's
rootpage; see Shared header and footer. Read the warning in step 4 first, because the header renders differently from a page.
Steps
1. Choose: a component, a block, plain HTML, or the runtime
| The visitor needs | Use | Why |
|---|---|---|
| A panel that slides in (menu, filters, mini cart) | wm-drawer | Has an overlay, a scroll lock and open/close methods, and ui.drawer drives it |
| A popup (sign-up, notice, video) | wm-dialog | Centred panel with an X button, and ui.dialog drives it |
| Questions that open one at a time | wm-accordion + wm-accordion-item | Sets ARIA on header and content and rotates the icon |
| Switch between panels | wm-tabs | Matches labels to panels by order |
| A carousel | wm-slider | Arrows, dots, loop, autoplay, fade, several slides per view |
| A card grid, optionally filled from code | wm-grid | Responsive columns. Its search and filters are not reliable (see the reference) |
| A header menu with big panels | wm-mega-menu | Hover or click panels and a built-in mobile menu |
| Size/colour buttons, quantity, add to cart | wm-storefront-product-control | It only emits events. Your code adds to the cart (step 3) |
| A list or record from data, a cart count, a value shown or hidden | Runtime attributes (data-source, data-bind, data-show) | See The Appmint JS engine on a page |
| A button that saves, adds to cart, signs in, notifies | data-action | The same engine |
| Headings, text, images, columns, sections | Plain HTML + CSS in the screen | No component is needed. Build Studio's blocks are only starting HTML (step 5) |
| A top navigation bar | Plain links (+ wm-mega-menu or wm-drawer) | wm-navigation is not in the published bundle |
| Blog, booking, store, forms | Site Features at their own URLs | data-react presets are gone (step 5) |
Use a component only when it does something plain HTML can't: opening, switching, sliding. Static layout is plain HTML with the page's own CSS. Every component builds its own DOM when it starts, and that DOM is what you style and debug.
Hands on: for each live part, the element (or runtime attribute) to use.
Check: nothing in the list needs wm-navigation or a data-react block.
2. Build Studio: write the element with its slots
Put the markup in the page's active screen, data.screens[<active>].html, and in its data.html mirror. Use page_write with html, or repo_update with both. The page shape, the URL rules and the footguns (id="root", a literal </body>, size limits) are in Build a website page, step 3.
Every component finds its children by slot="…" attribute. They are not Shadow DOM slots: the children stay in the page DOM, and the component moves them into wrappers it builds. A missing slot means the component does nothing, or does it half-way, so copy the slot names exactly.
The four you will use most, all tested on a live local site:
<!-- A drawer opened from anywhere on the page. The trigger slot may be hidden, but it must exist. -->
<wm-drawer id="menu" direction="right" width="320px">
<span slot="trigger" hidden></span>
<nav slot="content" class="menu-panel">
<a href="/about">About</a>
<a href="/contact">Contact</a>
<button data-action="ui.drawer" data-payload='{"id":"menu","action":"close"}'>Close</button>
</nav>
</wm-drawer>
<button data-action="ui.drawer" data-payload='{"id":"menu","action":"open"}'>Menu</button>
<!-- A dialog. Same rule: both slots, even if the trigger is hidden. -->
<wm-dialog id="signup" size="small">
<span slot="trigger" hidden></span>
<div slot="content" class="signup-panel">
<h3>Join the list</h3>
<button data-action="ui.dialog" data-payload='{"id":"signup","action":"close"}'>Done</button>
</div>
</wm-dialog>
<button data-action="ui.dialog" data-payload='{"id":"signup","action":"open"}'>Sign up</button>
<!-- An FAQ: the first answer open; the icon class rotates on open. -->
<wm-accordion default-open="0">
<wm-accordion-item>
<button slot="header">Shipping <span class="accordion-icon">▾</span></button>
<div slot="content">Ships in 2 days.</div>
</wm-accordion-item>
<wm-accordion-item>
<button slot="header">Returns <span class="accordion-icon">▾</span></button>
<div slot="content">30 days.</div>
</wm-accordion-item>
</wm-accordion>
<!-- Tabs: labels and panels pair up by order; default-active is 0-based. -->
<wm-tabs default-active="0">
<button slot="label">Overview</button>
<button slot="label">Specs</button>
<div slot="content">Overview text</div>
<div slot="content">Specs text</div>
</wm-tabs>A slider, static or filled from the catalog. Both forms were tested; the runtime rebuilds the slider when its rows arrive:
<wm-slider slides-per-view="1" speed="400" autoplay="true" autoplay-delay="5000">
<div slot="slide"><img src="https://…/1.jpg" alt=""></div>
<div slot="slide"><img src="https://…/2.jpg" alt=""></div>
</wm-slider>
<wm-slider data-source="product.list" data-source-payload='{"limit":6}' show-dots="true">
<div slot="slide" data-bind-each>
<img data-bind-attr-src="data.image" alt="">
<span data-bind="data.name"></span>
</div>
</wm-slider>Every attribute, event and method of each element is listed under The wm-* elements below.
A wm-drawer or wm-dialog without both slot="trigger" and slot="content" children never builds. Its children sit in the page as ordinary visible content, for example a row of menu links. A ui.drawer "open" on it still runs open(), which sets open and locks page scrolling (body overflow: hidden) with nothing on screen. Tested: the links stayed inline, the drawer had no panel, and the page could not scroll.
Hands on: the screen HTML with the elements in it.
Check: read the page back with repo_find_one. Each wm-drawer and wm-dialog has one slot="trigger" and one slot="content" child. Each wm-accordion-item has slot="header" and slot="content". Each wm-slider has at least one slot="slide". Each wm-grid has a slot="item". Each id is unique on the page.
3. Build Studio: drive them, and make them look like the site
From buttons. These runtime actions find the element by id and call its method:
| Action | Payload | What it does |
|---|---|---|
ui.drawer | {"id":"<id>","action":"open"|"close"|"toggle"} (default toggle), or just "<id>" | Calls that wm-drawer's open() / close() / toggle(). If no element has that id, it opens or closes the site's own sidebar instead. |
ui.dialog | the same | Calls the wm-dialog's method. If no element has that id, it opens the storefront product quick view for a product with that id. |
ui.modal | the same | Same as ui.dialog, but the fallback quick view is always modal |
ui.nav | the same | Calls open/close/toggle on a wm-navigation or wm-mega-menu with that id. Neither has those methods on a published page (wm-navigation isn't loaded, wm-mega-menu has none), so it only sets a navOpen state value. Don't use it on these. |
The id is matched with document.getElementById(id), then <tag>[data-id="<id>"]. Write it without a #.
Don't use the open-dialog / close-dialog effects (data-on-success="open-dialog:#id"). They only set or remove the open attribute, and the elements read it once, at start. Tested: open-dialog left the dialog hidden. close-dialog removed open but left the dialog showing, and the next ui.dialog "open" then did nothing, because the dialog still believed it was open. Chain a second button, or call the method from page code.
Don't put data-action="ui.drawer" on the element's own slot="trigger". The trigger already toggles on click, so the action toggles it a second time. Use the trigger slot alone, or a hidden trigger and a separate button.
From an onclick. onclick="document.getElementById('signup').open()" works on an ordinary page. It does not work in the shared header and footer, or on a page with a data-react block (step 4).
From page code. For anything conditional, call the element's methods from Page settings → Style → JavaScript (page_write javascript), guarded as in Build a website page, step 4. The elements are defined by /custom-elements.js, which the site loads after the page. Wait for them first:
(function boot() {
if (document.body && document.body.getAttribute('data-design-mode') === 'true') return;
var app = document.getElementById('shop-app');
if (!app || !window.appmint || !window.customElements.get('wm-storefront-product-control')) return void setTimeout(boot, 30);
if (app.dataset.bound) return;
app.dataset.bound = '1';
// The product control only announces the click. Adding to the cart is ours to do.
document.getElementById('pdp-control').addEventListener('add-to-cart', function (e) {
var d = e.detail; // { product, quantity, attributes: { size: 'M', color: 'black' } }
window.appmint.cart.add({ sk: d.product.sk, data: d.product.data, quantity: d.quantity, attributes: d.attributes });
});
// React to a drawer opening, e.g. for analytics or focus.
document.addEventListener('drawer-open', function (e) { /* e.target.id */ });
})();Tested: the selected size and colour reached the cart line as {"size":"M","color":"black"}. appmint.cart.add takes one object: a product record (sk + data) or a cart line, plus quantity and the chosen attributes.
Their look. None of them use Tailwind. Each element injects one stylesheet into <head> the first time it is used (<style id="wm-drawer-style"> and so on), and everything it generates has a wm-* class you can style. Some styles they set inline:
- tab wrapper borders;
- tab and mega-menu panel visibility;
- the mega menu's mobile button and full-screen menu, which are white with fixed styles.
Override those in your CSS with !important, or restyle the classes. Your slotted content keeps the site's CSS: fonts, colours and link styles reach it as usual.
On a dark site, set the panel colours. wm-drawer, wm-dialog and the product control use a white panel, but text colour is inherited. On a dark site, light text lands on white. Tested on a dark site: the drawer and dialog panels were rgb(255,255,255) with text rgb(238,234,226), so the content was unreadable. The reverse also happens: a visitor whose device is in dark mode gets a near-black #111827 panel on a light site, because the defaults change under prefers-color-scheme: dark. Set the variables to the site's own colours, which fixes both cases:
wm-drawer, wm-dialog {
--wm-drawer-bg: #14161c; --wm-drawer-fg: #eeeae2;
--wm-dialog-bg: #14161c; --wm-dialog-fg: #eeeae2;
}Hands on: nothing new.
Check: repo_find_one shows style.javascript at the top level, and the script parses.
4. Build Studio: where the element lives decides how it renders
How a component renders depends on where it sits:
| Where | How it reaches the browser | Effect on components |
|---|---|---|
A page's screen, with no data-react in it | The body HTML is written as is | Everything works: wm-*, onclick, <style> in the body |
A page's screen with a data-react placeholder anywhere | Rebuilt element by element through React | wm-* elements still build (tested). onclick and other on… attributes are removed. A <style> in the body is sent from the server as the text [object Object]; the browser only repairs it after a hydration error. <link>, <script>, <meta> and <title> are dropped. Every element gains a data-node-id. |
The root page's header/footer screens, and a parent page's screens | Always rebuilt the same way | The same losses as the row above, on every page of the site |
Tested on local, on a page with one dead data-react block:
- an
onclickbutton lost itsonclick; - a body
<link>disappeared; - a body
<style>arrived as[object Object]and React threw a hydration error before repainting it; - a
wm-accordionon the same page still built and opened.
In the shared header and footer, drive components only with data-action (ui.drawer) and the trigger slot, and put CSS in root's Page settings → Style. Never use onclick. See Shared header and footer.
/custom-elements.js is loaded by the page shell on every site page, and on the built-in login, account and checkout pages, which use the same shell. You never add a <script> tag for it.
Stop: a component in root is on every page of the site, and on a site that serves drafts a new page is live from its first save. Show the person what will appear and where, and wait for yes.
Hands on: nothing new.
Check: the page holds no data-react (unless that is intended), and the header holds no onclick.
5. Build Studio: blocks and presets (what Studio inserts)
The page's component tree is the active screen's HTML. Build Studio parses the body into its tree of nodes and stamps each element with a data-node-id. On save it writes the body back into screens[<active>].html, keeps the stored <head> as it was, and mirrors the active screen into data.html. Animations and variants are stored per node in screens[].config.animations and screens[].config.variants, keyed by data-node-id. Keep those ids when editing HTML by hand, or the animation loses its element. See Animate elements on a page.
The insert palette (Web tab) inserts plain HTML. Every item is a template string, added as the last child of the drop target. The groups:
- Interactive: Accordion, Tabs, Drawer, Dialog, Slider (variants
cards,fade,vertical), Grid (minimal,blog,team), MegaMenu, Navigation. - Plain HTML:
- Blank, Headings, Paragraph, Content, Div, TextBoxes, HR, Link, Image, SVG, Video;
- HTMLForm, Input, Button, Select, Textarea, HTMLTable, HTMLList;
- Gallery, Navigation (a plain
<nav>), ImageAndText, Columns, Accents; - Blog, Contact, Ecommerce (including a static CartIcon), Feature, Footer, Header, Hero (including CTA), Pricing, Statistic, Step, Team, Testimonial, Card.
When an agent writes a page through the API, it writes the HTML itself. These blocks are just a starting point for a person on the canvas. Know what they carry:
| What a palette block brings | What to do |
|---|---|
Tailwind utility classes (px-6 py-3 bg-blue-600 dark:bg-gray-900, theme names like bg-primary) | They work on the published page too: every site page loads the Tailwind v4 browser build (tested: md:grid-cols-3, lg:px-12, bg-primary). But the theme names are Build Studio's sample palette (bg-primary is #0cd6a0), not the site's colours, and dark: classes switch on for visitors whose device is in dark mode. Replace the colour classes with the site's own; layout classes can stay. See Make a page work on phones and tablets. |
| Sample text and picsum/w3schools sample media | Replace all of it with the person's content |
Hard-coded ids (the Navigation block) | Rename them; ids must be unique on the page |
The Interactive Slider cards variant has a debug attribute | Remove it (it logs to the console) |
The Interactive Navigation block is a wm-navigation | Don't use it. It stays an inert tag on the published site |
- The panel writes each setting as an attribute named after its camelCase key. The browser lowercases it, but the element reads the kebab-case name. These settings therefore do nothing: Dialog Close on overlay and Show close button; Drawer Close on overlay; Grid Columns sm/md/lg/xl, Show search, Search placeholder, Filters position, Empty message, Sort options.
- Setting Open to off writes
open="false". The element only checks whetheropenis present, so it opens on load. - After a reload, the panel shows defaults, not the saved values.
- Slider and Mega menu settings do work.
To change any of these, edit the attribute in code view with its kebab-case name: close-on-overlay="false", columns-lg="3".
data-react presets. A preset placeholder looks like this:
<div class="wm-react-block" data-react="preset,web,Basic,VideoPlayer" data-react-config='{"url":"…"}'></div>The site resolves the name through its preset registry, and that registry now returns nothing for every name. So:
- the placeholder renders nothing, and it switches the whole body to the React path described in step 4;
- today they come only from converting legacy pages: no palette item writes one;
- on the canvas, an unresolved one shows an amber "Component not found" notice and is saved with
data-preset-missing.
A page whose only content is a dead placeholder has no body. The site treats that page as empty. On a site with a usable root page, it renders root's header and footer with nothing between them (tested: 200, header and footer only). With no root or parent that draws anything, the URL is a 404. A <title> text or any other visible element counts as content. Replace the placeholder with HTML: for a booking, store, blog or form, link to that Site Feature's URL instead. See Site page routing.
Hands on: nothing new.
Check: search the screen HTML for data-react= and wm-navigation; none should be left. No Tailwind colour class (bg-primary, text-secondary, bg-blue-600) should stand in for the site's own colours.
6. Check it on the site
Open the page in a real browser. The page source carries the HTML in a script payload, so grepping it proves nothing. Each element should have built its wrappers:
| Element | Built when |
|---|---|
wm-drawer | has a .wm-drawer-container child |
wm-dialog | has a .wm-dialog-overlay child |
wm-tabs | has .wm-tabs-labels and .wm-tabs-contents |
wm-accordion-item | the header has aria-expanded |
wm-slider | has .wm-slider-track and .wm-slider-dots |
wm-grid | has .wm-grid-grid |
wm-mega-menu | has .wm-mega-menu-triggers holding the triggers |
wm-storefront-product-control | has .wm-storefront-product-control and an "Add to Cart" button |
Then click once:
- every opener opens, every close button closes;
- after closing a drawer, the page scrolls again;
- the tab changes, the slide moves;
- one add to cart makes one cart line.
In the console, window.__WEBSITEMINT_CUSTOM_ELEMENTS_VERSION shows the bundle version (1.2.0 today), and customElements.get('wm-drawer') shows the element is defined. A "wm-drawer requires both…" or "wm-slider requires at least one slide" warning names the broken element.
Hands on: nothing new; the page is done. Check: the list above, at phone width (390 px) as well as desktop.
The wm-* elements
These are the elements in the bundle the site loads (/custom-elements.js, version 1.2.0). The source is in websitemint packages/ui, under preset/web/custom-elements/. A change to them ships only when base-app is rebuilt and redeployed.
Rules shared by all of them:
- They read their attributes once, two animation frames after they connect.
wm-slider,wm-gridandwm-storefront-product-controlrebuild when a watched attribute changes later.wm-drawer,wm-dialog,wm-tabs,wm-accordionandwm-mega-menudon't. - They move your slotted children into wrappers they build. Don't style a slot child by its position under the tag (
wm-tabs > button). Style it by its own class. - Rows from
data-sourceare fine. When rows arrive inside awm-*element, the runtime takes the wrappers apart and re-attaches the element so it builds again.wm-sliderandwm-gridhave a publicrebuild. Tested withwm-sliderand withwm-tabs(data-source="product.list"on the tag, adata-bind-eachlabel template and adata-bind-eachcontent template: one tab per product). Lists inside a drawer or dialog are a different case; see What it can't do yet. - Events bubble, so you can listen on
document. - Booleans are
"true"/"false"strings, except where the table says "present".
wm-drawer
A panel that slides in from an edge, with an optional dimmed backdrop.
| Attribute | Values | Default | Notes |
|---|---|---|---|
direction | left right top bottom | right | |
width | CSS length | 400px | left/right only; capped at 100vw |
height | CSS length | 400px | top/bottom only |
overlay | "false" hides it | shown | No backdrop: the page stays clickable (tested) |
close-on-overlay | "false" | closes | Backdrop click closes |
open | present | closed | Opens at start without animating |
| Slot | Required | Role |
|---|---|---|
trigger | yes (may be hidden) | Click toggles the drawer |
content | yes | Moved into the sliding panel |
| Method / event | |
|---|---|
open() close() toggle() isOpen() | open() also sets document.body.style.overflow = 'hidden'; close() clears it |
setDirection(dir) | Rebuilds in a new position |
drawer-open / drawer-close | detail: { drawer } |
| CSS variable | Default |
|---|---|
--wm-drawer-bg | #fff (dark scheme: #111827) |
--wm-drawer-fg | inherit (dark scheme: #f9fafb) |
--wm-drawer-overlay-bg | rgba(0,0,0,.5) |
--wm-drawer-shadow | soft shadow |
--wm-drawer-z | 50; the backdrop is z - 1 |
--wm-drawer-duration | 300ms |
Classes it builds: .wm-drawer-overlay, .wm-drawer-container, .wm-drawer--<direction>, .is-open.
- There is no Escape-key close, no focus trap and no close button. Add your own close button with
ui.drawer"close". - The default z-index is 50. A site header with a higher z-index shows above the panel, so raise
--wm-drawer-z.
wm-dialog
A centred modal panel with an X button.
| Attribute | Values | Default | Notes |
|---|---|---|---|
size | small (24rem) medium (42rem) large (64rem) full | medium | Not fullscreen (the Studio block's comment says so, wrongly) |
overlay | "false" | shown | No dimming; clicks pass through to the page |
close-on-overlay | "false" | closes | Backdrop click closes |
show-close-button | "false" | shown | The X in the corner |
open | present | closed |
| Slot | Required | Role |
|---|---|---|
trigger | yes (may be hidden) | Click toggles |
content | yes | Moved into the panel |
| Method / event | |
|---|---|
open() close() toggle() isOpen() | No scroll lock (unlike the drawer) |
setSize(size) | |
dialog-open / dialog-close | detail: { dialog } |
CSS variables: --wm-dialog-bg (#fff, or #111827 in a dark scheme), --wm-dialog-fg (inherit), --wm-dialog-overlay-bg, --wm-dialog-shadow, --wm-dialog-radius (.5rem), --wm-dialog-z (50), --wm-dialog-duration (300ms), --wm-dialog-close-color, --wm-dialog-close-hover.
Classes it builds: .wm-dialog-overlay (.wm-dialog--no-overlay), .wm-dialog-content, .wm-dialog--<size>, .wm-dialog-close, .is-open. The panel has no padding of its own, so pad your content.
wm-accordion and wm-accordion-item
| On | Attribute | Default | Notes |
|---|---|---|---|
wm-accordion | allow-multiple (present) | one at a time | |
wm-accordion | default-open | none | 0-based index of the item to open |
wm-accordion-item | open (present) | closed |
| Slot (on each item) | Required | Role |
|---|---|---|
header | yes | Click toggles; gets role="button", aria-expanded, aria-controls |
content | yes | Shown with display:block, hidden with display:none; gets role="region" and an id |
| Method / event | |
|---|---|
item: open() close() toggle() isOpen() | |
accordion-open / accordion-close | Fired on the item; detail: { item }. default-open fires one at start. |
- An element with class
accordion-iconinside the header getsis-rotated(rotated 180°) when the item is open.--wm-accordion-icon-durationsets the speed (200ms). - An open item gets the class
active; style it yourself. - An item works on its own without a
wm-accordionaround it, but then nothing closes the others.
wm-tabs
| Attribute | Default | Notes |
|---|---|---|
default-active | 0 | 0-based |
| Slot | Role |
|---|---|
label (one per tab) | Click activates; gets role="tab", aria-selected, class active when active |
content (one per tab) | Matched to labels by order; display:block or none; role="tabpanel" |
| Method / event | |
|---|---|
getActiveIndex() | There is no method to switch tabs from code. Click the label: el.querySelectorAll('[slot=label]')[i].click(). |
tab-change | detail: { index, label, content }. Fires at start too. |
It builds .wm-tabs-labels (inline display:flex, border-bottom: 1px solid #e5e7eb) and .wm-tabs-contents. It injects no stylesheet, so all other styling is yours.
One tab per record works by putting the source on the tag itself and giving it two row templates. Tested: three products gave three tabs, and clicking the second label showed the second panel.
<wm-tabs data-source="product.list" data-source-payload='{"limit":3}'>
<button slot="label" data-bind-each data-bind="data.name"></button>
<div slot="content" data-bind-each>
<p data-bind="data.name"></p>
<p data-bind="data.price" data-format="currency"></p>
</div>
</wm-tabs>wm-slider
| Attribute | Values | Default |
|---|---|---|
effect | slide fade | slide |
direction | horizontal vertical | horizontal |
slides-per-view | number, fractions allowed (1.2) | 1 |
space-between | px | 0 |
centered-slides | true false | false |
speed | ms | 500 |
autoplay | "true" turns it on | off |
autoplay-delay | ms | 3000 |
show-arrows / show-dots | "false" hides | shown |
loop | "false" stops at the ends | on |
debug | present | logs its config |
The camelCase spellings that Build Studio writes (slidesperview, autoplaydelay, showarrows, and so on) are read too. All of these attributes are watched, and a change rebuilds the slider after 100 ms.
| Slot | Role |
|---|---|
slide (one or more) | Each slide. With loop on, copies of the first and last slides are added (class wm-slider-clone, without the slot) |
| Method / event | |
|---|---|
next() prev() | |
goToSlide(i) | i counts the copies. With loop on, goToSlide(1) is the first real slide (tested: goToSlide(3) showed slide 3 of 3). |
setAutoplay(bool) setAutoplayDelay(ms) rebuild() | |
slide-change | detail: { index }, the same internal index |
- CSS variables:
--wm-slider-arrow-bg,--wm-slider-arrow-bg-hover,--wm-slider-arrow-fg,--wm-slider-arrow-shadow,--wm-slider-dot-bg,--wm-slider-dot-bg-hover,--wm-slider-dot-bg-active. - Classes:
.wm-slider-container,.wm-slider-track,.wm-slider-slide,.wm-slider-arrow--prev/--next,.wm-slider-dots,.wm-slider-dot.is-active. - Give the slides (or the slider) a height. Arrows and dots sit over the slide.
- There is no swipe, keyboard control or pause-on-hover.
- In Build Studio's canvas it does not build at all (
data-design-mode), so the slides show stacked for editing.
wm-grid
A responsive grid of slot="item" children. Search and filters are built in, but only half work (warning below).
| Attribute | Default | Notes |
|---|---|---|
columns | 1 | below 640px; 1–6 |
columns-sm / -md / -lg / -xl | 2 / 3 / 4 / 4 | from 640 / 768 / 1024 / 1280px |
gap | 16 | px |
show-search | shown | "false" hides the search box |
filters-position | top | none removes the filter area |
search-placeholder | Search items... | |
empty-message | No items found | |
animation | fade | slide, none |
sort-options | — | parsed, but no sort control is ever drawn |
| Slot | Role |
|---|---|
item | The cards, or the template for setItems (data-field="path" children are filled: img gets src, input gets value, others get text) |
search | Your own search UI; its first <input> is used |
filters | Your own filter UI |
| Method / event | |
|---|---|
setItems(rows) | Builds one card per row from the first slot="item" (tested) |
setFilters(configs) | Filter controls (display: select, checkbox, radio, color, range-input, range-slider) built from row data |
clearFilters() getFilteredItems() | |
items-filtered | detail: { total, filtered }. Fires at start too. |
CSS variables: --wm-grid-accent, --wm-grid-border, --wm-grid-input-bg, --wm-grid-label, --wm-grid-muted. Classes: .wm-grid-grid, .wm-grid-search-input, .wm-grid-empty, .wm-grid-filter-*.
- Set
columns-xlas well ascolumns-lg. Above 1280 px the grid usescolumns-xl, which defaults to 4. Tested at 1440 px:columns-lg="3"gave four columns. - Its search box is broken. With static cards it filters nothing; tested, typing "beta" left all three cards. With
setItemsrows, typing throwsCannot read properties of undefined (reading 'length')(an undeclaredsearchFields), and nothing filters. Setshow-search="false". For a searchable list, use adata-sourcelist with a{{searchTerm}}payload (see The Appmint JS engine on a page).
wm-mega-menu
| Attribute | Default | Notes |
|---|---|---|
trigger-type | hover | click |
hover-delay / hide-delay | 100 / 200 ms | |
animation | fade | slide, none |
mobile-breakpoint | 768 | below it, a hamburger button replaces the triggers |
| Slot | Role |
|---|---|
trigger (one per panel) | Moved into .wm-mega-menu-triggers. On hover (or click) it shows the panel at the same index and gets class active. |
panel | position:absolute; top:0 (inline), shown with visibility/opacity. You place it: left, width, and a top below the triggers with !important, and position:relative on the menu. |
| Method / event | |
|---|---|
getActiveIndex() | No open or close method |
mega-menu-change | detail: { index, trigger, panel } |
- You may write the
.wm-mega-menu-triggersand.wm-mega-menu-panelswrappers yourself; it uses them if present. - It injects no stylesheet.
- Below the breakpoint it adds a
.wm-mega-menu-mobile-button. Tapping it opens a white, full-screen.wm-mega-menu-mobileappended to<body>, with copies of the triggers and panels and a close X (tested at 390 px).
trigger-type="click" never closes a panel. Tested: clicking the same trigger again, or clicking elsewhere, left it open. Only another trigger replaces it. Use hover (the panel hides after hide-delay), or close it from page code.
wm-storefront-product-control
Size and colour buttons, a quantity stepper, and Add to Cart / Buy Now buttons. It never touches the cart itself.
| Attribute | Default | Notes |
|---|---|---|
data-product | — | The product as JSON, {"sk":"…","data":{…}}. The way to give it a product on a published page (tested). |
product-id | — | Loads the product only where window.requestQueueInstance exists (Build Studio), not on the site |
show-quantity / show-buy-now / show-wishlist / show-share | shown | "false" hides |
size | md | sm, lg |
actions-layout | stacked | inline, grid |
Where its choices come from, in product.data:
| Field | What it draws |
|---|---|
sizes | strings, or {value,label,available}. Required. |
colors | strings, or {value,label,param|color,available} swatches. Required. |
variants | [{name,label,options,required}] |
stock, else available | shown as "<n> available" |
It does not read the catalog's own variant model (data.attributes). For a real product you must map that model into sizes, colors or variants yourself (see Catalog pages from your products). Also, an available: true value prints as "true available".
| Method / event | |
|---|---|
setProduct(product) setProductAttributes(attrs) getSelection() | |
attribute-changed | detail: { attribute, value, selectedAttributes } |
quantity-changed | detail: { quantity } (1–999) |
add-to-cart / buy-now | detail: { product, quantity, attributes }. Add to Cart stays disabled until every required choice is made. |
add-to-wishlist / share | detail: { product } |
- It clears its own children each time it renders, so don't put markup inside it.
- CSS variables:
--wm-spc-accent,--wm-spc-accent-hover,--wm-spc-accent-soft,--wm-spc-fg,--wm-spc-muted,--wm-spc-border,--wm-spc-surface,--wm-spc-surface-hover,--wm-spc-buy-now-bg,--wm-spc-buy-now-hover,--wm-spc-danger,--wm-spc-radius,--wm-spc-duration. All have dark-scheme defaults. - Classes:
.wm-spc-option.is-selected,.wm-spc-swatch,.wm-spc-btn--cart,.wm-spc-validation.
<wm-storefront-product-control id="pdp-control" show-wishlist="false" show-share="false"
data-product='{"sk":"<product sk>","data":{"name":"Field Jacket","price":120,"sizes":["S","M","L"],"colors":[{"value":"black","label":"Black","param":"#000"}]}}'>
</wm-storefront-product-control>Wire add-to-cart in page code as in step 3. Apostrophes in the JSON must be escaped as ' inside the single-quoted attribute.
wm-navigation (not available)
Build Studio offers it, and it has a schema and a source file, but it is not in the published bundle. On the site it is an unknown tag that shows its raw slot="nav-item" links. Build menus from plain links, wm-mega-menu and wm-drawer.
When it's done
Tell the person:
- each component on the page, what opens or changes it, and what it shows;
- any page code, and which events it listens to;
- which colours you set for drawers and dialogs;
- anything left out because it can't work yet (grid search,
wm-navigation,data-reactblocks).
Open the page on the site at desktop and phone width.
What it can't do yet
- No lists inside a drawer or dialog. A
data-loopordata-sourceinside awm-drawerorwm-dialogmakes the runtime rebuild the element around its rows. A loop of cart items inside a drawer froze the page, and inside a dialog it stripped the content (Custom elements). Put the list beside the element, or use the site's own cart drawer (cart.toggle). wm-gridsearch and sort don't work (see the warning in its section), and its filters only work on rows added bysetItems.wm-tabscan't be switched from code except by clicking a label.wm-mega-menuhas no open or close method.wm-drawerandwm-dialoghave no Escape-key close and no focus trap.wm-sliderhas no swipe.wm-storefront-product-controldoesn't read the catalog's variant model, doesn't load a product byproduct-idon the site, and doesn't add to the cart by itself.wm-navigationand everydata-reactpreset render nothing on the published site.- Build Studio's property panel doesn't apply several wm-* settings, and its "Open: off" opens the element (step 5).
- Appmint's own AI instructions describe some of these wrongly. Build Studio's skill describes
wm-drawerwithpositionandtrigger-textand children without slots, and calls the product control a "product listing/filter widget". The pages skill says drawers and dialogs default tooverlay="false". The tables on this page are what the elements read.
Don'ts
- Don't write a
wm-drawerorwm-dialogwithout both atriggerand acontentslot. - Don't open or close them with
open-dialog/close-dialogeffects or by setting theopenattribute. Useui.drawer/ui.dialogor the methods. - Don't put
ui.draweron the drawer's own trigger. - Don't use
onclick, a body<style>or<link>in the shared header and footer, or on a page with adata-reactblock. - Don't leave a white panel on a dark site. Set
--wm-drawer-bg/fgand--wm-dialog-bg/fg. - Don't rely on
wm-gridsearch,sort-options,wm-navigation,product-id, or anydata-reactblock. - Don't publish Build Studio's sample colours (Tailwind theme classes), sample text or sample media as they are.
- Don't add a
<script src="/custom-elements.js">. The site loads it.