---
title: Using Polaris web components
description: >-
Learn essential concepts for working with Polaris web components, including
styling, layouts, scale, Preact integration, and interactive features across
UI extension surfaces.
api_name: app-surfaces
source_url:
html: 'https://shopify.dev/docs/api/app-surfaces/using-polaris-web-components'
md: 'https://shopify.dev/docs/api/app-surfaces/using-polaris-web-components.md'
---
# Using Polaris web components
Polaris 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](https://shopify.dev/docs/apps/design) and are built with [remote-dom](https://github.com/Shopify/remote-dom), Shopify's library for building cross-platform user interfaces.
This guide covers essential concepts for working with Polaris web components across all UI extension surfaces.
***
## Preact
UI extensions are scaffolded with [Preact](https://preactjs.com/) 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](https://preactjs.com/guide/v10/hooks/) like `useState` and `useEffect` for managing component state and side effects. You can also use [Preact Signals](https://preactjs.com/guide/v10/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
```jsx
import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useState} from 'preact/hooks';
export default async () => {
render(, document.body);
};
function Extension() {
const [count, setCount] = useState(0);
return (
<>
Count: {count} setCount(count + 1)}
>
Increment
>
);
}
```
***
## Accessibility
Polaris 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 `label` and `error` properties on form elements.
* Use appropriate heading levels with `s-heading` or the `heading` property.
* Test keyboard navigation throughout your extension.
* Use `labelAccessibilityVisibility` to visually hide labels while keeping them available to assistive technologies.
* Use `accessibilityRole` to specify the ARIA role of a component.
## Example: Ensuring accessibility with labels
```jsx
{/* Good - provides a label */}
{/* Bad - missing a label */}
```
***
## Commands
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 target `ClipboardItem`.
## Example: Controlling components with commands
```jsx
This modal is controlled using the commands API. No JavaScript event
handlers are needed—the browser handles all interactions automatically.
Close
Modal Controls
Toggle Modal
Show Modal
Hide Modal
Auto Command (Toggle)
```
***
## Execution model
UI extensions execute the module's default export so it can render a user interface. UI extensions are powered by [remote-dom](https://github.com/Shopify/remote-dom/), a fast and secure environment for custom ([non-DOM](#styling)) 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
```jsx
import '@shopify/ui-extensions/preact';
import {render} from 'preact';
export default async () => {
render(, document.body);
};
function Extension() {
return Your extension;
}
```
***
## Forms
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`. Update `defaultValue` after 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.
**Note:**
Each input must have a `name` attribute for dirty state tracking to work.
## Example: Managing form dirty state
##### Using defaultValue
```jsx
import { render } from 'preact';
import { useState } from 'preact/hooks';
export default function extension() {
render(, document.body);
}
const defaultValues = {
text: 'default value',
number: 50,
};
function Extension() {
const [textValue, setTextValue] = useState('');
const [numberValue, setNumberValue] = useState('');
return (
console.log('submit', {textValue, numberValue})}>
setTextValue(e.target.value)}
/>
setNumberValue(e.target.value)}
/>
);
}
```
##### Using implicit default
```jsx
import { render } from 'preact';
import { useState } from 'preact/hooks';
export default function extension() {
render(, document.body);
}
async function Extension() {
const data = await fetch('/data.json');
const {text, number} = await data.json();
return ;
}
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 (
console.log('submit', {textValue, numberValue})}>
setTextValue(e.target.value)}
/>
setNumberValue(e.target.value)}
/>
);
}
```
***
## Handling events
Polaris 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.
### Basic 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
```jsx
console.log('Clicked!')}>
Inline Handler
{
console.log('Event details:', event.type);
console.log('Target:', event.currentTarget);
}}>
With Event Object
```
##### HTML
```html
Click me
Click me (addEventListener)
```
### Form 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
```jsx
// OnInput fires on every keystroke
console.log('Typing:', e.currentValue.value)}
>
// OnChange fires on blur or Enter press
console.log('Value committed:', e.currentValue.value)}
>
// Using both together
console.log('Real-time:', e.currentTarget.value)}
onChange={(e) => console.log('Final value:', e.currentTarget.value)}
>
```
##### HTML
```html
```
### Focus management
Use `onFocus` and `onBlur` to track when users enter and leave form fields.
## Example: Managing focus events
##### JSX
```jsx
console.log('Field focused')}
onBlur={() => console.log('Field blurred')}
> {
e.currentTarget.setAttribute('label', 'Field is active!')
}}
onBlur={(e) => {
e.currentTarget.setAttribute('label', 'Tab to next field to trigger blur')
}}
>
```
##### HTML
```html
```
### Form 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
```jsx
// Number field example - values are strings
{
// 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
{
// e.currentTarget.values is an array of strings
console.log('Selected colors:', e.currentTarget.values);
}}
>
```
##### HTML
```html
RedBlueGreen
```
### Controlled and uncontrolled components
Form components can be **uncontrolled** (simpler) or **controlled** (more advanced):
* **Uncontrolled**: The component manages its own state. Set `defaultValue` for the initial value.
* **Controlled**: Your code manages the state. Set `value` and 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
```jsx
// Uncontrolled component - internal state
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";
{
console.log('Would update state:', e.currentTarget.value)
}}
/>
```
##### HTML
```html
```
### Technical implementation
Polaris web components register events using `addEventListener` rather than setting attributes.
Event names are automatically converted to lowercase. When you write ``, 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
```jsx
console.log('Clicked!')}>
Click me
console.log('Value changed:', e.currentTarget.value)}
onFocus={() => console.log('Field focused')}
onBlur={() => console.log('Field blurred')}
>
```
##### HTML
```html
Click me
```
***
## Interactive 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
```jsx
{/* s-button with onClick renders as a button element */}
console.log('Action triggered')}>
Trigger Action
{/* s-button with href renders as an anchor element */}
View Products
{/* s-link with href renders as an anchor element */}
Go to Settings
{/* s-link with onClick renders as a button element */}
console.log('Link action')}>
Trigger Link Action
{/* target="auto" uses _self for internal, _blank for external URLs */}
Internal LinkExternal Link
{/* s-clickable is an escape hatch for custom interactive designs */}
console.log('Custom click')}>
Custom Clickable Area
```
***
## Layouts
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
```jsx
import '@shopify/ui-extensions/preact';
import {render} from 'preact';
export default function extension() {
render(, document.body);
}
function Extension() {
return (
HeadingDescription {
console.log('button was pressed');
}}
>
Button
);
}
```
***
## Methods
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
```jsx
function Methods() {
const modalRef = useRef(null);
return (
<>
Open modal
Modal content {
modalRef.current.hideOverlay();
}}
>
Close modal
>
);
}
```
##### JS
```javascript
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);
}
```
***
## Properties and attributes
Polaris 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
```jsx
if (propName in element) {
// Set as a property
element[propName] = propValue;
} else {
// Set as an attribute
element.setAttribute(propName, propValue);
}
```
In practice, just use the property names as documented and everything will work as expected.
## Example: Using documented property names
```jsx
{/* This works as expected - the "gap" property accepts string values */}
...;
{/* This also works - the "checked" property accepts boolean values */}
...;
```
***
## Responsive values
Some properties accept responsive values that change based on the parent container's inline size.
### Syntax
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, `` ensures browsers without container query support get a layout that works everywhere.
## Example: Using responsive values with container queries
##### HTML
```html
This padding will be "large-400" when the container is more than 500px.
Otherwise it will be "small".
```
##### Pseudocode
```
This padding will be "large-400" when the
container is more than 500px. Otherwise it will
be "small".
```
### Using s-query-container
Wrap your content in `` 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
```html
This padding will be "large-400" when the "outer" container is more than 500px.
Otherwise it will be "small".
```
##### Pseudocode
```
This padding will be "large-400" when the "outer" container is more than 500px.
Otherwise it will be "small".
```
### Values with reserved characters
If your values contain brackets `()` or commas `,`, wrap them in quotes to escape them.
## Example: Escaping reserved characters
```html
...
```
### Advanced patterns
The syntax supports compound conditions, `and`/`or` logic, and nested conditions.
## Example: Advanced responsive patterns
##### Compound
```html
The padding will be "small" when the container is between 300px and 500px
wide. Otherwise it will be "large-500".
```
##### And | or
```html
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".
```
##### Nested
```html
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.
```
***
## Scale
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
```ts
export type Scale =
| 'small-500'
| 'small-400'
| 'small-300'
| 'small-200'
| 'small-100'
| 'small' // alias of small-100
| 'base'
| 'large' // alias of large-100
| 'large-100'
| 'large-200'
| 'large-300'
| 'large-400'
| 'large-500';
```
***
## Slots
Slots allow you to insert custom content into specific areas of Polaris 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
```jsx
The order has been created successfully.
View order
```
***
## Styling
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 `` or ``) rather than standard DOM elements like `