GS Loom UI components
Shared components, variants and theme styling.
GS Loom UI: one behavior, theme-owned appearance
GS Loom UI v1 is the shared component layer for storefront themes. It is separate from the admin/customer panel library. A theme uses ordinary HTML with data-gs-ui; GS Loom adds common CSS and interactive behavior. The theme supplies colors, radii and any custom variants. Existing HTML, animations, schemas and plugin permissions continue to work.
This page explains components through source code and configuration examples. Theme packages also contain snippets/ui-demo.html for optional testing on a private development page.
Contract and variants
<button type="button" data-gs-ui="button" data-variant="primary" data-size="md">
Get started
</button>
<a href="/contact" data-gs-ui="button" data-variant="outline">Contact</a>Use a real button for an action and a real link for navigation. Variants change appearance, not semantics. data-size changes size independently of data-variant. Existing theme classes can coexist with these attributes. The contract is additive: migration does not require replacing every existing element or removing animation hooks.
| Component | HTML contract | Built-in variants |
|---|---|---|
| Button | button or a with data-gs-ui="button" |
primary (default), secondary, outline, ghost, link, danger; sizes sm, md (default), lg |
| Card | article or div with data-gs-ui="card" |
surface (default), outline, elevated, ghost |
| Badge | span with data-gs-ui="badge" |
primary (default), secondary, outline |
| Field | wrapper with data-gs-ui="field" |
shared 8 px label/control spacing |
| Input / Textarea | input data-gs-ui="input" / textarea data-gs-ui="textarea" |
outline (default), filled, underline |
| Select | div data-gs-ui="select" containing a single native select |
outline (default), filled |
| Dropdown | details data-gs-ui="dropdown", summary, content wrapper |
secondary (default), outline, primary |
| Tabs | div data-gs-ui="tabs", tab list, buttons and panels |
line (default), pills; horizontal or vertical |
| Accordion | details data-gs-ui="accordion" with summary |
surface (default), outline, ghost |
| Modal | dialog data-gs-ui="modal" |
default, surface; sizes md (default), lg |
A custom variant such as brand is allowed. It does not gain behavior automatically: define its appearance in the theme CSS. A variant meaningful for a button is not automatically meaningful for an input or modal.
Theme tokens and custom variants
Put tokens in assets/ui.css. GS Loom loads shared styles before theme assets. In a Loom theme the following rule is scoped to that theme; document themes receive the same CSS in their isolated document.
:scope {
--gs-ui-accent: #e59d02;
--gs-ui-on-accent: #161006;
--gs-ui-text: #f8f8f8;
--gs-ui-background: #040000;
--gs-ui-surface: #191715;
--gs-ui-border: #ffffff29;
--gs-ui-focus: #e59d02;
--gs-ui-radius: 20px;
--gs-ui-button-radius: 50px;
--gs-ui-shadow: 0 16px 40px #0003;
}
[data-gs-ui="button"][data-variant="brand"] {
background: #5347ce;
color: #ffffff;
border-color: #5347ce;
}You may also override --gs-ui-danger and --gs-ui-on-danger. Bind tokens to your theme settings when colors are editable. Choose foreground and background together and test contrast. Skeleton uses yellow on dark surfaces, Paylio dark buttons on a light surface, and Victorie orange buttons with light/dark surfaces. Imported themes inherit the platform's base components; their token file is part of the exported theme package.
Editable button variants
Declare a select setting in the block schema, then bind it to data-variant:
{
"type": "select",
"id": "button_variant",
"label": "Button variant",
"default": "primary",
"options": [
{"value":"primary","label":"Primary"},
{"value":"secondary","label":"Secondary"},
{"value":"outline","label":"Outline"}
]
}<a data-gs-ui="button" data-variant="[[ block.settings.button_variant ]]"
href="[[ safe_url(block.settings.url) ]]"
data-text-field="[[ block.settings_path ]].label"
gs-text="block.settings.label"></a>Skeleton's existing Card block exposes this setting. The same pattern works for card or other applicable variants. A schema select configures the editor; it is different from a storefront Select component. The shared UI does not automatically add a block to the builder: define a block schema, preset and parent targeting as usual.
Inputs and Select
<div data-gs-ui="field">
<label for="contact-topic">Topic</label>
<div data-gs-ui="select" data-variant="filled">
<select id="contact-topic" name="topic" required>
<option value="">Choose a topic</option>
<option value="sales">Sales</option>
<option value="support">Support</option>
<option value="other" disabled>Unavailable</option>
</select>
</div>
</div>After initialization the platform presents a custom combobox/listbox. The native select remains the form value and validation source. Arrow keys move between enabled options, Home/End reach the first/last option, typing finds a label, Enter/Space select, Escape cancels and Tab closes the list. Clicking outside closes it. change and input events and form reset retain standard behavior. Without JavaScript the native select remains usable. Multiple selection is not enhanced in v1.
Keep a real label associated with the native select's id; the runtime transfers its accessible name to the trigger. Use unique IDs per instance, for example derived from block.id, rather than copying example IDs into repeated blocks. Put an Input or Textarea in the same Field wrapper, with a matching label. Use disabled, required and aria-invalid as appropriate. UI primitives provide no submission endpoint: use the existing Forms plugin for sending data and server validation. The library does not replace plugin-owned field markup or bypass access checks.
Navigation dropdown and Languages
<details data-gs-ui="dropdown" data-variant="outline">
<summary>Explore</summary>
<nav data-gs-dropdown-content aria-label="Explore">
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</details>Enter/Space opens the native disclosure. Tab moves through links. Escape closes and returns focus to the summary; leaving the control or clicking outside also closes it. This is a disclosure with links, not an ARIA menu. Do not add menu roles without the corresponding keyboard behavior.
<gs-languages variant="dropdown"></gs-languages> uses the same dropdown behavior and retains the language plugin's navigation/editor contract. Each Languages-capable theme must supply its own switcher appearance. Skeleton's assets/languages.css extends its shared UI tokens. The plain link variant remains available. See Languages.
Tabs and Accordion
<div data-gs-ui="tabs" data-variant="pills" data-value="overview">
<div data-gs-tab-list aria-label="Product information">
<button data-gs-tab="overview">Overview</button>
<button data-gs-tab="details">Details</button>
</div>
<section data-gs-panel="overview">Overview content</section>
<section data-gs-panel="details">Detailed content</section>
</div>
<details data-gs-ui="accordion" data-variant="outline">
<summary>How does this work?</summary>
<p>Your answer.</p>
</details>Tab keys match panel keys inside their component. The runtime generates IDs, roles and relationships, hides inactive panels and manages roving keyboard focus. Arrow keys, Home and End select enabled tabs automatically; data-orientation="vertical" uses up/down arrows. Tab and panel definitions should remain stable during a mounted instance; remount when replacing the whole set. Before JavaScript initializes, all panel content remains readable.
Accordion uses native details behavior. Add open for an initially expanded item. Use the same name on related details when you want an exclusive group; choose a unique group name per block instance.
Modal
<button type="button" data-gs-ui="button" data-gs-open="shipping-dialog">Shipping</button>
<dialog data-gs-ui="modal" data-size="lg" id="shipping-dialog" aria-labelledby="shipping-title">
<h2 id="shipping-title">Shipping information</h2>
<p>Content shown on demand.</p>
<button type="button" data-gs-ui="button" data-variant="secondary" data-gs-close autofocus>Close</button>
</dialog>The runtime opens native dialog with showModal(), providing modal focus containment, Escape and focus return. The close control and a click outside the dialog rectangle close it. Give it an accessible title and an obvious close button. Targets are resolved within the current theme wrapper; repeated blocks need unique target IDs. A modal is not a confirmation or permission system and does not submit anything by itself.
Focus, active and disabled states
Focus is visible without changing the component's shape. :active describes a pressed control; persistent selection uses aria-selected, aria-current, or details[open]. Native disabled buttons do not activate. For disabled links use aria-disabled="true" and tabindex="-1"; the shared runtime prevents activation. Do not use a disabled-looking class as access control. See focus rules for composite search fields and dropdowns.
Migration and verification
The three bundled themes now include UI tokens and a reusable UI demo. Existing Skeleton/Paylio button and card markup and Victorie CTA buttons opt into the shared contract while retaining their original classes and animation hooks. Specialized navigation, sliders and video players keep their existing behavior. These are not silently replaced with generic controls.
Test variants and themes, keyboard navigation, labels, validation, disabled options, form reset, modal Escape/focus return, two instances on one page, mobile overflow and reduced motion. Keep CSS and UI code changes in the theme/runtime, not in saved customer content. Rebuild with npm run loom:build; the developer portal download is rebuilt with npm run build:developers. Do not copy platform runtime files into every theme: GS Loom supplies them in rendering and preview.
Equal height and grow
Use data-gs-equal-height on a flex row or grid. Each direct child becomes a flex wrapper; put data-gs-grow on the card or button inside it. Items stretch to the tallest content in their row, including after translation or live editing. The theme still defines columns, gaps and responsive breakpoints. No fixed heights or JavaScript measurements are needed.
<div data-gs-equal-height class="feature-grid">
<div>
<article data-gs-ui="card" data-gs-grow data-gs-stack>
<h3>Feature</h3>
<p>Short or long translated content.</p>
<div data-gs-footer>
<a data-gs-ui="button" href="/details">Details</a>
</div>
</article>
</div>
<!-- Repeat the wrapper for each card. -->
</div>.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
gap: 24px;
}data-gs-stack creates a vertical flex layout; data-gs-footer pushes actions to the bottom. Keep normal spacing above actions (for example a gap or footer padding). data-gs-grow also works independently inside flex layouts. On mobile, separate rows retain natural heights. For equal heights across ALL grid rows, explicitly add grid-auto-rows: 1fr to the grid. These attributes work with any component variant and do not replace its colors, borders or animation.
Header and menu editing
GS Loom uses data-gs-header="header" for headers, data-gs-logo for logos and data-gs-menu="header" or "footer" for navigation. <gs-menu location="header"> emits this marker automatically and reads the site menu. Preserve semantic header, nav and links. Themes own appearance, breakpoints and dropdowns.
For a schema-owned header, add data-gs-header-group="header". Clicking the header, logo or menu item opens that group in the builder; edits persist in loom.groups.header (the legacy data property name used by GS Loom). Skeleton uses this variant to retain its nested navigation and translations. Without this attribute, logos open the shared logo editor and menus the site navigation manager. Do not mix both data sources for the same items. Public links behave normally.
Dropdowns, nested navigation and megamenus
GS Loom separates the menu data from its appearance. The builder stores a tree of items; <gs-menu> renders that tree, and the theme CSS controls its appearance. The examples below are source code, not interactive previews.
1. Place the menu in the header
<gs-menu location="header" label="Main navigation"></gs-menu>Put this markup in the theme header section or snippet. location="header" selects the site menu assigned to that location; label is its accessible name. Do not paste the JSON below into this HTML: it illustrates items stored in the site menu. In the builder, use Navigation → Header, expand an item and choose Add child item.
Editing the header menu enables use_site_menu. Paylio uses the shared header; Skeleton and Victorie Vending keep their original demo navigation until this setting is enabled. Use one data source for each menu, rather than maintaining both hardcoded links and site menu items.
2. Dropdown with children and grandchildren
{
"id": "services",
"label": "Services",
"href": "/services",
"menu_layout": "dropdown",
"children": [
{
"id": "websites",
"label": "Websites",
"href": "/websites",
"icon": "globe",
"children": [
{
"id": "design",
"label": "Design",
"href": "/design",
"icon": "star"
},
{
"id": "development",
"label": "Development",
"href": "/development",
"icon": "code"
}
]
}
]
}Services → Websites → Design / Development creates three levels. Each object in children follows the same contract, so a child can contain its own children. Without children, an item renders as a link. Omitting menu_layout uses dropdown. This object belongs in the header menu’s items array.
| Field | Meaning |
|---|---|
id |
Stable item identifier; keep it when renaming or reordering. |
label |
Visible link or submenu trigger text. |
href |
Destination URL. Use # for a parent that only opens its submenu. |
children |
Array of child items; omit it for a leaf link. |
menu_layout |
dropdown for a list, mega for columns of immediate children. |
icon |
Library icon key or image:<URL>; omit for no icon. |
3. Megamenu with icons
{
"id": "explore",
"label": "Explore",
"href": "#",
"menu_layout": "mega",
"children": [
{
"id": "products",
"label": "Products",
"href": "/products",
"icon": "star",
"children": [
{
"id": "skeleton",
"label": "Skeleton",
"href": "/products/skeleton"
},
{
"id": "paylio",
"label": "Paylio",
"href": "/products/paylio"
}
]
},
{
"id": "support",
"label": "Support",
"href": "/support",
"icon": "image:/media/support.svg",
"children": [
{
"id": "contact",
"label": "Contact",
"href": "/contact"
},
{
"id": "docs",
"label": "Documentation",
"href": "/docs"
}
]
}
]
}Set menu_layout: "mega" on the parent that opens the wide panel. Here Products and Support form two columns; their children contain the links below each heading. star selects a library icon. image:/media/support.svg illustrates an uploaded image: replace it with the real URL returned by the file manager. The property accepts an image URL, not raw SVG markup. You can change either column’s children without changing the renderer.
4. Match the theme appearance
[data-gs-menu="header"] {
--gs-menu-background: #17201c;
--gs-menu-color: #ffffff;
--gs-menu-border: #ffffff33;
--gs-menu-radius: 16px;
--gs-menu-gap: 24px;
}Add these rules to the theme stylesheet. The selector limits the tokens to the header menu. Background, text, border, corner radius and spacing can differ between themes while retaining shared behavior. Keep focus indicators visible and ensure panels fit the viewport.
5. Behavior, translations and limits
Click or Enter/Space opens a submenu; Escape closes the current level and returns focus to its trigger. If a parent has a real URL, the renderer adds that link at the start of its panel, so opening the panel does not navigate away. On small screens nested panels expand vertically. A megamenu is not a separate page or plugin.
With Languages enabled, edit labels per language in the builder. Keep item IDs stable so translations remain associated with the correct items; icons, URLs and layout are language-independent. Limits are 5 levels including the root, 30 siblings per branch and 100 items per menu. Check long translations, keyboard operation and mobile layouts. See Menus for the site navigation data contract and Languages for localization.
Visual menu editing in the builder
Click the Skeleton header to open the menu tree. Only existing items are listed; expand a parent to see its children. Select an item and click its name to edit it directly. Its link, icon and submenu layout are shown only for that selection. Add, remove and reorder items without editing schema fields. The Appearance tab controls text/background/active colors, font, font size and spacing. Apply writes a draft and preserves localized labels; publication remains a separate action.