Using Polaris web components
Web components are the UI building blocks that you use to display data and trigger functionality in UI extensions. These components are native UI elements that follow Shopify's design system and are built with remote-dom, Shopify's library for building cross-platform user interfaces.
This guide covers essential concepts for working with web components across all UI extension surfaces.
Anchor to PreactPreact
UI extensions are scaffolded with Preact by default. This means you can use Preact patterns and principles within your extension.
Since Preact is included as a standard dependency, you have access to all of its features including hooks like useState and useEffect for managing component state and side effects. You can also use Preact Signals for reactive state management, and take advantage of standard web APIs just like you would in a regular Preact application.
Example: Using Preact hooks to manage state
Anchor to AccessibilityAccessibility
Web components have accessibility built in. They use semantic HTML, support keyboard navigation, include proper ARIA attributes, manage focus, and provide appropriate color contrast. Components also log warnings when required accessibility properties are missing.
To keep your app accessible:
- Always set
labelanderrorproperties on form elements. - Use appropriate heading levels with
s-headingor theheadingproperty. - Test keyboard navigation throughout your extension.
- Use
labelAccessibilityVisibilityto visually hide labels while keeping them available to assistive technologies. - Use
accessibilityRoleto specify the ARIA role of a component.
Example: Ensuring accessibility with labels
Anchor to CommandsCommands
Commands let you control components declaratively without writing JavaScript. Set commandFor to the ID of the target component, and command to the action you want to perform. The browser handles the interaction automatically.
Available commands:
--toggle: Toggle the target component's visibility.--show: Show the target component.--hide: Hide the target component.--auto: Perform the most appropriate action for the target component (default).--copy: Copy the targetClipboardItem.
Example: Controlling components with commands
Anchor to Execution modelExecution model
UI extensions execute the module's default export so it can render a user interface. UI extensions are powered by remote-dom, a fast and secure environment for custom (non-DOM) UIs.
Remote-dom enables UI extensions to run in an isolated sandbox while maintaining high performance and security. This architecture ensures that extensions can't access sensitive data or interfere with the host application, while still providing a rich, interactive user experience.
Example: Basic extension structure
Anchor to FormsForms
The s-form component manages form state and handles submissions to your app's backend or directly to Shopify using API access. It triggers callbacks when the form is submitted or reset.
Forms track whether inputs are "dirty" (changed from their original value) using the defaultValue property:
- With defaultValue set: An input is dirty when its current value differs from
defaultValue. UpdatedefaultValueafter submission to reset the dirty state. - Without defaultValue set: An input is dirty when its current value differs from the initial value or the last programmatic update.
Each input must have a name attribute for dirty state tracking to work.
Each input must have a name attribute for dirty state tracking to work.
Example: Managing form dirty state
Using defaultValue
import { render } from 'preact';
import { useState } from 'preact/hooks';
export default function extension() {
render(<Extension />, document.body);
}
const defaultValues = {
text: 'default value',
number: 50,
};
function Extension() {
const [textValue, setTextValue] = useState('');
const [numberValue, setNumberValue] = useState('');
return (
<s-form onSubmit={() => console.log('submit', {textValue, numberValue})}>
<s-stack gap="base">
<s-text-field
label="Default Value"
name="my-text"
defaultValue={defaultValues.text}
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
/>
<s-number-field
label="Percentage field"
name="my-number"
defaultValue={defaultValues.number}
value={numberValue}
onChange={(e) => setNumberValue(e.target.value)}
/>
</s-stack>
</s-form>
);
}Using implicit default
import { render } from 'preact';
import { useState } from 'preact/hooks';
export default function extension() {
render(<Extension />, document.body);
}
async function Extension() {
const data = await fetch('/data.json');
const {text, number} = await data.json();
return <App text={text} number={number} />;
}
function App({text, number}) {
// The initial values set in the form fields will be the default values
const [textValue, setTextValue] = useState(text);
const [numberValue, setNumberValue] = useState(number);
return (
<s-form onSubmit={() => console.log('submit', {textValue, numberValue})}>
<s-stack gap="base">
<s-text-field
label="Default Value"
name="my-text"
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
/>
<s-number-field
label="Percentage field"
name="my-number"
value={numberValue}
onChange={(e) => setNumberValue(e.target.value)}
/>
</s-stack>
</s-form>
);
}Anchor to Handling eventsHandling events
Web components use standard DOM events, making them work with your preferred framework. You can attach event handlers using the same patterns as with native HTML elements.
Anchor to Basic event handlingBasic event handling
Event handlers in Polaris components work just like standard HTML elements. In frameworks, use the familiar camelCase syntax (like onClick in Preact). In plain HTML, use lowercase attributes or addEventListener.
Example: Attaching event handlers
JSX
<s-button onClick={() => console.log('Clicked!')}>
Inline Handler
</s-button>
<s-button onClick={(event) => {
console.log('Event details:', event.type);
console.log('Target:', event.currentTarget);
}}>
With Event Object
</s-button>HTML
<s-button onclick="console.log('Button clicked!')">
Click me
</s-button>
<s-button id="eventButton">
Click me (addEventListener)
</s-button>
<script>
const eventButton = document.getElementById('eventButton');
eventButton.addEventListener('click', () => {
console.log('Button clicked using addEventListener!');
});
</script>Anchor to Form input eventsForm input events
Form components support two event types for tracking changes:
onInput: Fires on every keystroke or value change. Use this for real-time validation or character counting.onChange: Fires when the value is committed—on blur for text fields, or immediately after input for checkboxes and radio buttons. Use this to validate after the user finishes their input.
Example: Handling input and change events
JSX
// OnInput fires on every keystroke
<s-email-field
label="Email"
name="email"
onInput={(e) => console.log('Typing:', e.currentValue.value)}
></s-email-field>
// OnChange fires on blur or Enter press
<s-text-field
label="Name"
name="name"
onChange={(e) => console.log('Value committed:', e.currentValue.value)}
></s-text-field>
// Using both together
<s-email-field
label="Email"
name="email"
onInput={(e) => console.log('Real-time:', e.currentTarget.value)}
onChange={(e) => console.log('Final value:', e.currentTarget.value)}
></s-email-field>HTML
<s-email-field
label="Email"
name="email"
oninput="console.log('Typing:', this.value)"
></s-email-field>
<s-text-field
label="Name"
name="name"
onchange="console.log('Value committed:', this.value)"
></s-text-field>
<!-- Using addEventListener -->
<s-email-field
label="Email"
name="email"
></s-email-field>
<script>
const field = document.querySelector('s-email-field');
field.addEventListener('input', (e) => {
console.log('Real-time:', e.currentTarget.value);
});
field.addEventListener('change', (e) => {
console.log('Final value:', e.currentTarget.value);
});
</script>Anchor to Focus managementFocus management
Use onFocus and onBlur to track when users enter and leave form fields.
Example: Managing focus events
JSX
<s-email-field
label="email"
name="email"
onFocus={() => console.log('Field focused')}
onBlur={() => console.log('Field blurred')}
></s-email-field>
<s-text-field
label="Tab to next field to trigger blur"
name="name"
onFocus={(e) => {
e.currentTarget.setAttribute('label', 'Field is active!')
}}
onBlur={(e) => {
e.currentTarget.setAttribute('label', 'Tab to next field to trigger blur')
}}
></s-text-field>HTML
<s-email-field
label="Email"
name="email"
onfocus="console.log('Field focused')"
onblur="console.log('Field blurred')"
></s-email-field>
<s-text-field
label="Tab to next field to trigger blur"
name="name"
></s-text-field>
<script>
const field = document.querySelector('s-text-field');
field.addEventListener('focus', () => {
field.setAttribute('label', 'Field is active!');
});
field.addEventListener('blur', () => {
field.setAttribute('label', 'Tab to next field to trigger blur');
});
</script>Anchor to Form values and typesForm values and types
All form elements return string values in events, even numeric inputs—convert them if needed. Access single values using event.currentTarget.value, or use event.currentTarget.values (an array of strings) for multi-select components like s-choice-list.
Example: Working with form values and types
JSX
// Number field example - values are strings
<s-number-field
label="Quantity"
name="quantity"
onChange={(e) => {
// e.currentTarget.value is a string, convert if needed
const quantity = Number(e.currentTarget.value);
console.log('Quantity as number:', quantity);
}}
/>
// Multi-select example - values is an array of strings
<s-choice-list
name="colors"
label="Colors"
multiple
onChange={(e) => {
// e.currentTarget.values is an array of strings
console.log('Selected colors:', e.currentTarget.values);
}}
>
<s-choice label="Red" value="red" />
<s-choice label="Blue" value="blue" />
<s-choice label="Green" value="green" />
</s-choice-list>HTML
<!-- Number field example - values are strings -->
<s-number-field
label="Quantity"
name="quantity"
onchange="console.log('Value type:', typeof this.value, 'Value:', this.value)"
></s-number-field>
<!-- Multi-select example - values is an array of strings -->
<s-choice-list name="colors" label="Colors" multiple>
<s-choice value="red">Red</s-choice>
<s-choice value="blue">Blue</s-choice>
<s-choice value="green">Green</s-choice>
</s-choice-list>
<script>
const choiceList = document.querySelector('s-choice-list');
choiceList.addEventListener('change', (e) => {
// e.currentTarget.values is an array of strings
console.log('Selected colors:', e.currentTarget.values);
});
</script>Anchor to Controlled and uncontrolled componentsControlled and uncontrolled components
Form components can be uncontrolled (simpler) or controlled (more advanced):
- Uncontrolled: The component manages its own state. Set
defaultValuefor the initial value. - Controlled: Your code manages the state. Set
valueand update it in your event handler. Use this when you need to validate as the user types, format values, or synchronize multiple inputs.
Example: Controlled and uncontrolled components
JSX
// Uncontrolled component - internal state
<s-text-field
label="Comment"
name="comment"
defaultValue="Initial value"
onChange={(e) => console.log('New value:', e.currentTarget.value)}
/>
// Controlled component - external state
// In a real component, 'name' would be from framework state
const name = "John Doe";
<s-text-field
label="Name"
name="name"
value={name}
onChange={(e) => {
console.log('Would update state:', e.currentTarget.value)
}}
/>HTML
<!-- Uncontrolled component - internal state -->
<s-text-field
label="Comment"
name="comment"
value="Initial value"
onchange="console.log('New value:', this.value)"
></s-text-field>
<!-- Controlled component - external state -->
<s-text-field
id="nameField"
label="Name"
name="name"
value="John Doe"
></s-text-field>
<button onclick="updateName()">
Change Name
</button>
<script>
const nameField = document.getElementById('nameField');
// Listen for changes to update our "state"
nameField.addEventListener('input', (e) => {
console.log('Value changed:', e.currentTarget.value);
});
// Manually update the component value (controlled)
function updateName() {
nameField.value = "Jane Smith";
}
</script>Anchor to Technical implementationTechnical implementation
Web components register events using addEventListener rather than setting attributes.
Event names are automatically converted to lowercase. When you write <s-button onClick={handleClick}>, the component checks that "onClick" in element is true, then registers your handler with addEventListener('click', handler).
All event handlers receive standard DOM events as their first argument.
Example: Event handling implementation
JSX
<s-button onClick={() => console.log('Clicked!')}>
Click me
</s-button>
<s-email-field
label="Email"
name="email"
onChange={(e) => console.log('Value changed:', e.currentTarget.value)}
onFocus={() => console.log('Field focused')}
onBlur={() => console.log('Field blurred')}
></s-email-field>HTML
<s-button onclick="console.log('Clicked!')">
Click me
</s-button>
<s-text-field
label="Email"
name="email"
onchange="console.log('Value changed:', e.currentTarget.value)"
onfocus="console.log('Field focused')"
onblur="console.log('Field blurred')"
></s-text-field>
<!-- or -->
<script>
const textField = document.querySelector('s-text-field');
textField.addEventListener('change', (e) => {
console.log('Value changed:', e.currentTarget.value);
});
</script>Anchor to Interactive elementsInteractive elements
s-button, s-link, and s-clickable render as anchor elements when they have an href, or as button elements when they have an onClick without an href. Per the HTML spec, interactive elements can't contain other interactive elements.
Set target="auto" to automatically open internal links in the same tab (_self) and external URLs in a new tab (_blank).
Use s-clickable as an escape hatch when s-link or s-button can't achieve a specific design. Consider using s-link and s-button when possible.
Example: Interactive elements as buttons and links
Anchor to LayoutsLayouts
Use s-stack, s-grid, and s-box to build custom layouts.
s-stack and s-grid have no spacing between children by default. Use the gap property to add space. Setting direction="inline" on s-stack makes children wrap to a new line when space is limited. s-grid allows children to overflow unless you configure template rows or columns.
For shorthand properties like border, order matters: use size-keyword, color-keyword, style-keyword.
Example: Building a custom layout with stack and gap
Anchor to MethodsMethods
Methods are functions available on components for programmatic control. Components like Modal, Sheet, and Announcement provide hideOverlay() or dismiss() to control their behavior imperatively when needed.
Use methods when you need to trigger actions that can't be achieved through property changes alone, such as closing an overlay after an async operation or resetting component state.
Example: Calling component methods
JSX
function Methods() {
const modalRef = useRef(null);
return (
<>
<s-button
command="--show"
commandFor="modal-1"
>
Open modal
</s-button>
<s-modal
id="modal-1"
ref={modalRef}
heading="Test Modal"
>
<s-text>Modal content</s-text>
<s-button
onClick={() => {
modalRef.current.hideOverlay();
}}
>
Close modal
</s-button>
</s-modal>
</>
);
}JS
function Methods() {
const button =
document.createElement('s-button');
const modal = document.createElement('s-modal');
button.textContent = 'Open Modal';
button.commandFor = 'modal-1';
modal.id = 'modal-1';
modal.heading = 'Test Modal';
const closeButton =
document.createElement('s-button');
closeButton.textContent = 'Close modal';
closeButton.onclick = () => modal.hideOverlay();
modal.appendChild(closeButton);
document.body.appendChild(button);
document.body.appendChild(modal);
}Anchor to Properties and attributesProperties and attributes
Web components follow standard HTML patterns. Attributes appear in markup, while properties are accessed directly on the DOM element. Most attributes are reflected as properties, except value and checked which follow HTML's standard behavior.
In JSX, the framework checks if the element has a matching property name. If it does, the value is set as a property; otherwise, it's applied as an attribute:
Example: How JSX applies properties vs attributes
In practice, just use the property names as documented and everything will work as expected.
Example: Using documented property names
Anchor to Responsive valuesResponsive values
Some properties accept responsive values that change based on the parent container's inline size.
Anchor to SyntaxSyntax
Responsive values follow a ternary-like syntax: @container (inline-size > 500px) large, small means use large when the container exceeds 500px, otherwise use small.
The pattern is: @container [name] (condition) valueIfTrue, valueIfFalse
Use a mobile-first approach for browser compatibility. The fallback value (when the condition is false) should work at the smallest size. For example, <Stack direction="@container (inline-size > 300px) inline, block"> ensures browsers without container query support get a layout that works everywhere.
Example: Using responsive values with container queries
HTML
<s-query-container>
<s-box padding="@container (inline-size > 500px) large-400, small">
This padding will be "large-400" when the container is more than 500px.
Otherwise it will be "small".
</s-box>
</s-query-container>Pseudocode
<s-query-container>
<s-box padding="@container (inline-size > 500px) large-400, small">
This padding will be "large-400" when the
container is more than 500px. Otherwise it will
be "small".
</s-box>
</s-query-container>Anchor to Using s-query-containerUsing s-query-container
Wrap your content in <s-query-container> to enable responsive value queries. By default, queries target the closest container. To target a specific ancestor, add containername to that container and reference it in your query: @container outer (inline-size > 500px).
Example: Targeting named containers
HTML
<s-query-container containername="outer">
<s-section>
<s-query-container>
<s-box padding="@container outer (inline-size > 500px) large-400, small">
This padding will be "large-400" when the "outer" container is more than 500px.
Otherwise it will be "small".
</s-box>
</s-query-container>
</s-section>
</s-query-container>Pseudocode
<s-query-container containername="outer">
<s-box padding="@container outer (inline-size > 500px) large-400, small">
This padding will be "large-400" when the "outer" container is more than 500px.
Otherwise it will be "small".
</s-box>
</s-query-container>Anchor to Values with reserved charactersValues with reserved characters
If your values contain brackets () or commas ,, wrap them in quotes to escape them.
Example: Escaping reserved characters
Anchor to Advanced patternsAdvanced patterns
The syntax supports compound conditions, and/or logic, and nested conditions.
Example: Advanced responsive patterns
Compound
<s-query-container>
<s-box padding="@container (300px < inline-size < 500px) small, large-500">
The padding will be "small" when the container is between 300px and 500px
wide. Otherwise it will be "large-500".
</s-box>
</s-query-container>And | or
<s-query-container>
<s-box
padding="@container (inline-size > 500px) and (inline-size < 1000px) large-400, small"
>
This padding will be "large-400" when the container is more than 500px wide and
when the container is smaller than 1000px wide. Otherwise it will be
"small".
</s-box>
</s-query-container>Nested
<s-query-container>
<s-box
padding="@container (inline-size > 500px) base, (inline-size > 1000px) large-400, small"
>
This padding will be "base" when the container is greater than 500px wide,
"large-400" when the container is larger than 1000px wide, and "small" otherwise.
</s-box>
</s-query-container>Anchor to ScaleScale
Properties like padding, size, and gap use a middle-out scale.
Values radiate from base (the default): small-100 through small-500 get progressively smaller, while large-100 through large-500 get progressively larger.
Use small and large as shorthand for small-100 and large-100.
Example: Scale values from small to large
Anchor to SlotsSlots
Slots allow you to insert custom content into specific areas of web components. Use the slot attribute to specify where your content should appear.
- Named slots (for example,
slot="title") place content in designated areas. - Multiple elements can share the same slot name.
- Elements without a slot attribute go into the default (unnamed) slot.
Example: Using slots for custom content
Anchor to StylingStyling
Components automatically apply styling based on the properties you set and the context in which they're used. For example, headings display at progressively less prominent sizes based on nesting depth within sections. All components inherit merchant brand settings, and the CSS can't be altered or overridden.
Component styling is controlled by the merchant's branding settings and can't be overridden with custom CSS. Extensions render using Shopify's custom HTML elements (like <s-banner> or <s-button>) rather than standard DOM elements like <div> or <script> tags.
Example: Styling components with properties
Anchor to Tone, color, and variantTone, color, and variant
Use these properties to control component appearance:
tone: Applies a group of color design tokens to the component (critical,success,info)color: Adjusts the intensity of the tone (subdued,strong)variant: Changes how the component is rendered to match the design language (varies by component)
Example: Using tone, color, and variant properties
Anchor to TroubleshootingTroubleshooting
Review the following common issues and debugging tips for using web components.
Anchor to Common issuesCommon issues
- Properties not updating: Ensure you're using the property name as documented, not a different casing or naming convention.
- Event handlers not firing: Check that you're using the correct event name (for example,
onClickfor click events). - Form values not being submitted: Make sure your form elements have
nameattributes.
Anchor to Debugging tipsDebugging tips
- Inspect the element in your browser's developer tools to see the current property and attribute values.
- Use
console.logto verify that event handlers are being called and receiving the expected event objects. - Check for any errors in the browser console that might indicate issues with your component usage.