Every time a merchant opens your embedded Shopify app, they're already inside the Shopify Admin — a UI they use dozens of times a day. If your app looks and behaves differently from the Admin, even minor inconsistencies create friction. Polaris is the design system Shopify uses to build the Admin itself, and it's publicly available for app developers to use. When you build with Polaris, your app inherits the visual language, interaction patterns, accessibility standards, and typography that merchants already know. This article explains what Polaris is, how it's structured, which components you'll use most, and how to avoid the common pitfalls that trip up new Polaris adopters.
What Polaris actually is
Polaris is three things: a React component library (@shopify/polaris), a set of design tokens (@shopify/polaris-tokens), and a set of guidelines and patterns published at polaris.shopify.com. The React components are what most developers interact with. As of Polaris v13 (2024), the components are built on top of CSS custom properties backed by design tokens, meaning you can theme them without overriding implementation details.
The library ships over 80 components covering every layer of Admin UI: layout (Page, Layout, Card, BlockStack, InlineStack), data display (DataTable, IndexTable, Badge, Tag, Thumbnail), forms (TextField, Select, ChoiceList, DatePicker, Form), feedback (Toast, Banner, Modal, ProgressBar, Spinner), navigation (Frame, Navigation, TopBar, Tabs), and actions (Button, ButtonGroup, ActionList, Popover). Knowing which category a component falls into helps you quickly find the right one.
Setting up Polaris in a Remix app
Install @shopify/polaris. It has a peer dependency on react and react-dom but no other required dependencies. Import the global CSS once at the top of your app — in a Remix app, this goes in the links export of your root.tsx file. Then wrap your app tree in the AppProvider component from @shopify/polaris, passing an i18n object for localization (use enTranslations from @shopify/polaris/locales/en for English).
// app/root.tsx
import polarisStyles from '@shopify/polaris/build/esm/styles.css?url';
import enTranslations from '@shopify/polaris/locales/en.json';
import { AppProvider } from '@shopify/polaris';
export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: polarisStyles },
];
export default function Root() {
return (
<html>
<head><Meta /><Links /></head>
<body>
<AppProvider i18n={enTranslations}>
<Outlet />
</AppProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}The components you'll use on every page
The Page component is your outermost layout wrapper for any full-page route. It accepts title, subtitle, primaryAction, secondaryActions, and breadcrumbs props that wire into the native Admin page header. Never build your own page header — Page renders the correct Admin chrome for you.
Card is the primary content container. It renders a white rounded box with a shadow that matches Admin cards. Wrap sections of your UI in Card; avoid custom div containers with inline styles. BlockStack arranges children vertically with configurable spacing tokens ('tight', 'base', 'loose', 'extraLoose'). InlineStack arranges children horizontally. These two components replace most layout grid work — use them before reaching for CSS.
import {
Page, Card, BlockStack, InlineStack,
Text, Button, Badge
} from '@shopify/polaris';
export default function OrdersPage() {
return (
<Page
title="Orders"
primaryAction={{ content: 'Create order', onAction: () => {} }}
>
<Card>
<BlockStack gap="400">
<InlineStack align="space-between">
<Text as="h2" variant="headingMd">Recent orders</Text>
<Badge tone="success">12 fulfilled</Badge>
</InlineStack>
<Text as="p" variant="bodyMd" tone="subdued">
Showing orders from the last 30 days.
</Text>
<Button variant="plain">View all orders</Button>
</BlockStack>
</Card>
</Page>
);
}Forms with TextField and ChoiceList
Polaris form components are controlled — they require value and onChange props. TextField is your workhorse: it handles text, number, email, password, and multiline inputs. Use the error prop to display field-level validation messages (pass a string). Use the helpText prop for contextual guidance below the field.
ChoiceList is the correct component for radio button groups and checkbox groups — not individual Checkbox components repeated in a loop. Pass choices as an array of { label, value } objects and set allowMultiple for checkboxes. This produces an accessible fieldset with legend, which native HTML inputs alone don't give you for free.
Common mistakes to avoid
The single most common Polaris mistake is wrapping Polaris components in custom div containers with hardcoded padding or margin. Polaris uses a spacing token system — the gap values on BlockStack and InlineStack ("100" through "1600" in 4px increments) should cover every layout need. Applying margin-top: 20px to a Card breaks the visual rhythm of the Admin page.
The second mistake is misusing Button variants. Polaris buttons have variant values: 'primary', 'secondary' (the default), 'tertiary', 'plain', and 'monochromePlain'. Only one primary button should appear per page section — it's the main call to action. Using multiple primary buttons violates the Admin's design language and will flag your app during review. The 'plain' variant is correct for in-content links that look like buttons.
The third mistake is building custom modals when Polaris's Modal component would do. Modal accepts open, onClose, title, and children. Its footer slot takes primaryAction and secondaryActions props — these render correctly-spaced, correctly-styled action buttons in the modal footer without any custom CSS. If you're ever writing z-index hacks or position:fixed elements in a Polaris app, you're almost certainly reinventing something the library provides.
Polaris and the App Store review
Using Polaris is not technically required for App Store submission, but Shopify's app design guidelines require that embedded apps be visually consistent with the Shopify Admin. In practice, this means using Polaris or a pixel-faithful reimplementation — and reimplementing Polaris from scratch is far more work than using it. Apps that fail the design review often do so because of incorrect heading hierarchy, non-Polaris input styling, or custom color palettes that clash with the Admin's design tokens.
Using Text and typography tokens correctly
Polaris v12+ has a unified Text component that replaces all the legacy DisplayText, Heading, Subheading, Caption, and TextStyle components. Pass variant and as props to control the rendered element and its visual style independently. The variant hierarchy is: heading3xl, heading2xl, headingXl, headingLg, headingMd, headingSm, headingXs for headings; bodyLg, bodyMd, bodySm for body text; and displayLg/displayXl for large display figures.
The tone prop on Text handles semantic color: 'success' renders green text, 'critical' renders red, 'caution' renders orange, and 'subdued' renders grey. Use tone='subdued' for secondary information like timestamps and metadata — not for body text you want people to actually read. The fontWeight prop accepts 'regular', 'medium', 'semibold', and 'bold'. Avoid using all four in the same component — pick two weights maximum for visual hierarchy.
IndexTable vs DataTable: choosing the right table component
Polaris provides two distinct table components and the choice between them is a common source of confusion. DataTable is for static data display where rows are not individually selectable — think a summary report, a pricing comparison, or a read-only analytics breakdown. It renders a standard HTML table with sortable column headers and is appropriate when the user's primary action is to read the data, not to act on it.
IndexTable is for resource lists — products, orders, customers, anything where the user might select one or more rows and perform a bulk action. It includes built-in support for row checkboxes, row click navigation, bulk action bar, empty states, and loading skeletons. Pair it with the useIndexResourceState hook to manage selection state without any Redux or custom context. If you're building a list of anything that a merchant might want to select and modify, use IndexTable, not DataTable.
Accessibility is non-negotiable
One of Polaris's most significant benefits is that every component meets WCAG 2.1 AA accessibility standards out of the box. Color contrast ratios, focus ring styles, keyboard navigation, screen reader announcements, and ARIA attributes are all handled internally. When you use a Polaris Modal, it traps focus correctly and announces itself to screen readers. When you use a Polaris Toast, it renders in an ARIA live region so screen reader users hear it. If you replace Polaris components with custom HTML, you inherit the responsibility of implementing all of this yourself — which is a significant ongoing maintenance burden.
The Shopify App Store review process does include accessibility checks for embedded apps. Reviewers use keyboard navigation and check for obvious accessibility failures like missing focus states, non-interactive elements receiving focus, or form labels that aren't associated with their inputs. Using Polaris throughout your app makes passing these checks automatic rather than a separate engineering task.
