---
title: Using web components
description: >-
  Learn essential concepts for working with web components, including styling,
  layouts, scale, Preact integration, and interactive features across UI
  extension surfaces.
source_url:
  html: 'https://shopify.dev/docs/api/polaris/using-polaris-web-components'
  md: 'https://shopify.dev/docs/api/polaris/using-polaris-web-components.md'
---

# Using 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](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 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(<Extension />, document.body);
};


function Extension() {
  const [count, setCount] = useState(0);


  return (
    <>
      <s-text>Count: {count}</s-text>
      <s-button
        onClick={() => setCount(count + 1)}
      >
        Increment
      </s-button>
    </>
  );
}
```

***

## Accessibility

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 */}
<s-text-field label="Email address"></s-text-field>


{/* Bad - missing a label */}
<s-text-field></s-text-field>
```

***

## 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
<s-box>
  <s-modal id="example-modal" heading="Product Details">
    <s-text>
      This modal is controlled using the commands API. No JavaScript event
      handlers are needed—the browser handles all interactions automatically.
    </s-text>


    <s-stack slot="primaryAction">
      <s-button commandFor="example-modal" command="--hide">
        Close
      </s-button>
    </s-stack>
  </s-modal>


  <s-stack gap="small">
    <s-heading>Modal Controls</s-heading>


    <s-button
      commandFor="example-modal"
      command="--toggle"
      variant="primary">
      Toggle Modal
    </s-button>


    <s-button
      commandFor="example-modal"
      command="--show"
      tone="success">
      Show Modal
    </s-button>


    <s-button
      commandFor="example-modal"
      command="--hide"
      tone="critical">
      Hide Modal
    </s-button>


    <s-button
      commandFor="example-modal"
      command="--auto">
      Auto Command (Toggle)
    </s-button>
  </s-stack>
</s-box>
```

***

## 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(<Extension />, document.body);
};


function Extension() {
  return <s-banner>Your extension</s-banner>;
}
```

***

## 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(<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

```jsx
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>
  );
}
```

***

## Handling 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.

### 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
<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

```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>
```

### 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 or after `Enter` press for text fields, immediately after input for checkboxes and radio buttons, or after each step when using increment/decrement controls or the `Up Arrow` and `Down Arrow` keys. Use this to validate after the user finishes their input.

## Example: Handling input and change events

##### JSX

```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

```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>
```

### Focus management

Use `onFocus` and `onBlur` to track when users enter and leave form fields.

## Example: Managing focus events

##### JSX

```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

```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>
```

### 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
<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

```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>
```

### 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
<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

```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>
```

### Technical 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

```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

```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>
```

***

## Icons

Use `s-icon` to render an icon. Set `type` to the icon's name, `tone` to give it semantic meaning, and `size` to fit it into tighter layouts.

## Example: Rendering icons

```jsx
<s-stack direction="inline" gap="base">
  <s-icon type="home" />
  <s-icon type="check-circle" tone="success" />
  <s-icon type="alert-triangle" tone="caution" />
  <s-icon type="search" size="small" />
</s-stack>
```

Search the full set of icon names:

**Info:**

Not every surface supports every icon. App Home and Admin UI extensions support the full set above. Checkout, customer account, and POS UI extensions each support a smaller subset. For the icons available on the surface you're building for, see its icon reference: [App Home](https://shopify.dev/docs/api/app-home/web-components/media-and-visuals/icon), [Admin](https://shopify.dev/docs/api/admin-extensions/latest/web-components/media-and-visuals/icon), [Checkout](https://shopify.dev/docs/api/checkout-ui-extensions/latest/web-components/media-and-visuals/icon), [Customer accounts](https://shopify.dev/docs/api/customer-account-ui-extensions/latest/web-components/media-and-visuals/icon), and [POS](https://shopify.dev/docs/api/pos-ui-extensions/latest/web-components/media-and-visuals/icon).

***

## 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-stack gap="base">
  {/* s-button with onClick renders as a button element */}
  <s-button onClick={() => console.log('Action triggered')}>
    Trigger Action
  </s-button>


  {/* s-button with href renders as an anchor element */}
  <s-button href="/products" variant="primary">
    View Products
  </s-button>


  {/* s-link with href renders as an anchor element */}
  <s-link href="/settings">Go to Settings</s-link>


  {/* s-link with onClick renders as a button element */}
  <s-link onClick={() => console.log('Link action')}>
    Trigger Link Action
  </s-link>


  {/* target="auto" uses _self for internal, _blank for external URLs */}
  <s-link href="/internal-page" target="auto">Internal Link</s-link>
  <s-link href="https://shopify.com" target="auto">External Link</s-link>


  {/* s-clickable is an escape hatch for custom interactive designs */}
  <s-clickable onClick={() => console.log('Custom click')}>
    <s-box padding="base" background="subdued" borderRadius="base">
      <s-text>Custom Clickable Area</s-text>
    </s-box>
  </s-clickable>
</s-stack>
```

***

## 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(<Extension />, document.body);
}


function Extension() {
  return (
    <s-stack direction="inline">
      <s-image src="https://cdn.shopify.com/YOUR_IMAGE_HERE" />
      <s-stack>
        <s-heading>Heading</s-heading>
        <s-text type="small">Description</s-text>
      </s-stack>
      <s-button
        onClick={() => {
          console.log('button was pressed');
        }}
      >
        Button
      </s-button>
    </s-stack>
  );
}
```

***

## 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 (
    <>
      <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

```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

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 */}
<s-stack gap="base">...</s-stack>;


{/* This also works - the "checked" property accepts boolean values */}
<s-checkbox checked={true}>...</s-checkbox>;
```

***

## 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, `<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

```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>
```

### Using 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

```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>
```

### Values with reserved characters

If your values contain brackets `()` or commas `,`, wrap them in quotes to escape them.

## Example: Escaping reserved characters

```html
<s-query-container>
  <s-grid
    gridtemplatecolumns="@container (inline-size > 500px) 'repeat(4, 1fr)', 'repeat(2, 1fr)'"
  >
    ...
  </s-grid>
</s-query-container>
```

### Advanced patterns

The syntax supports compound conditions, `and`/`or` logic, and nested conditions.

## Example: Advanced responsive patterns

##### Compound

```html
<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

```html
<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

```html
<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>
```

***

## 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 web components. Use the `slot` attribute to specify where your content should appear.

* Named slots (for example, `slot="title"`) place content in designated areas.
* Some named slots accept one element, while others accept multiple elements. For example, the `primary-action` and `secondary-actions` slots on `s-admin-action` each support one element.
* Elements without a slot attribute go into the default (unnamed) slot.

## Example: Using slots for custom content

```jsx
<s-banner heading="Order created" status="success">
  The order has been created successfully.
  <s-button slot="primary-action">View order</s-button>
</s-banner>
```

***

## 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 `<s-banner>` or `<s-button>`) rather than standard DOM elements like `<div>` or `<script>` tags.

## Example: Styling components with properties

```jsx
<s-box
  padding="base"
  background="subdued"
  border="base"
  borderRadius="base"
>
  Content
</s-box>
```

***

## Tone, 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

```jsx
<s-button tone="critical" variant="primary">
  Primary Critical Button
</s-button>
<s-badge tone="success" color="strong">
  Success Strong Badge
</s-badge>
```

***

## Troubleshooting

Review the following common issues and debugging tips for using web components.

### Common issues

1. **Properties not updating**: Ensure you're using the property name as documented, not a different casing or naming convention.
2. **Event handlers not firing**: Check that you're using the correct event name (for example, `onClick` for click events).
3. **Form values not being submitted**: Make sure your form elements have `name` attributes.

### Debugging tips

1. Inspect the element in your browser's developer tools to see the current property and attribute values.
2. Use `console.log` to verify that event handlers are being called and receiving the expected event objects.
3. Check for any errors in the browser console that might indicate issues with your component usage.

***
