shopify-polaris-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThis skill ensures that interfaces are built using Shopify's Polaris Design System (React implementation v13.x), guaranteeing a native, accessible, and professional look and feel for Shopify Merchants.
Note (2025-2026): Polaris React () is in maintenance mode. Shopify has introduced Polaris Web Components for new development. However, Polaris React remains fully functional and supported for existing applications. This guide covers the React implementation.@shopify/polaris
本技能可确保界面基于Shopify的Polaris Design System(React实现版本v13.x)构建,为Shopify商家提供原生、无障碍、专业的外观和体验。
注意(2025-2026):Polaris React()目前处于维护模式。Shopify已推出Polaris Web Components用于新开发项目,但Polaris React对现有应用仍完全可用并提供支持。本指南覆盖React实现版本的相关内容。@shopify/polaris
Core Principles
核心原则
- Merchant-Focused: Design for efficiency and clarity. Merchants use these tools to run their business.
- Native Feel: The app should feel like a natural extension of the Shopify Admin. Do not introduce foreign design patterns (e.g. Material Design shadows, distinct bootstappy buttons) unless absolutely necessary.
- Accessibility: Polaris is built with accessibility in mind. Maintain this by using semantic components (e.g., ,
Button,Link) rather than customTextFieldimplementations.div - Predictability: Follow standard Shopify patterns. Save buttons go in the App Bridge Save Bar. Page actions go in the top right. Primary content is centered.
- 以商家为中心:设计优先考虑效率和清晰度,商家使用这些工具运营他们的业务。
- 原生体验:应用应该像是Shopify Admin的自然延伸,除非绝对必要,不要引入外来设计模式(例如Material Design阴影、风格突出的bootstrap类按钮)。
- 无障碍:Polaris原生支持无障碍特性,你可以通过使用语义化组件(例如、
Button、Link)而非自定义TextField实现来保留该特性。div - 可预测性:遵循标准的Shopify设计模式:保存按钮放在App Bridge保存栏中,页面操作放在右上角,主要内容居中展示。
Technical Implementation
技术实现
Dependencies (v13.x - 2025-2026)
依赖(v13.x - 2025-2026)
json
{
"@shopify/polaris": "^13.9.0",
"@shopify/polaris-icons": "^9.x",
"@shopify/app-bridge-react": "^4.x"
}json
{
"@shopify/polaris": "^13.9.0",
"@shopify/polaris-icons": "^9.x",
"@shopify/app-bridge-react": "^4.x"
}App Bridge Integration (Critical for v13.x)
App Bridge集成(v13.x版本核心要求)
Many UI components are now handled by App Bridge instead of Polaris React:
| Deprecated Polaris Component | Use App Bridge Instead |
|---|---|
| |
| App Bridge Navigation Menu API |
| App Bridge Toast API |
| App Bridge |
| App Bridge Title Bar API |
| App Bridge Loading API |
jsx
// Example: Using App Bridge for Modal (instead of deprecated Polaris Modal)
import { Modal, TitleBar } from '@shopify/app-bridge-react';
function MyComponent() {
return (
<Modal id="my-modal">
<TitleBar title="Confirm Action">
<button variant="primary" onClick={handleConfirm}>Confirm</button>
<button onClick={handleCancel}>Cancel</button>
</TitleBar>
<p>Are you sure you want to proceed?</p>
</Modal>
);
}
// Example: Using App Bridge for Toast
import { useAppBridge } from '@shopify/app-bridge-react';
function showToast() {
shopify.toast.show('Product saved successfully');
}
// Example: Using App Bridge Save Bar
import { useSaveBar } from '@shopify/app-bridge-react';
function SettingsForm() {
const saveBar = useSaveBar();
useEffect(() => {
if (hasChanges) {
saveBar.show();
} else {
saveBar.hide();
}
}, [hasChanges]);
}现在很多UI组件由App Bridge而非Polaris React提供:
| 已废弃的Polaris组件 | 替换为对应的App Bridge能力 |
|---|---|
| |
| App Bridge Navigation Menu API |
| App Bridge Toast API |
| App Bridge |
| App Bridge Title Bar API |
| App Bridge Loading API |
jsx
// 示例:使用App Bridge实现Modal(替代已废弃的Polaris Modal)
import { Modal, TitleBar } from '@shopify/app-bridge-react';
function MyComponent() {
return (
<Modal id="my-modal">
<TitleBar title="Confirm Action">
<button variant="primary" onClick={handleConfirm}>Confirm</button>
<button onClick={handleCancel}>Cancel</button>
</TitleBar>
<p>Are you sure you want to proceed?</p>
</Modal>
);
}
// 示例:使用App Bridge实现Toast
import { useAppBridge } from '@shopify/app-bridge-react';
function showToast() {
shopify.toast.show('Product saved successfully');
}
// 示例:使用App Bridge保存栏
import { useSaveBar } from '@shopify/app-bridge-react';
function SettingsForm() {
const saveBar = useSaveBar();
useEffect(() => {
if (hasChanges) {
saveBar.show();
} else {
saveBar.hide();
}
}, [hasChanges]);
}Fundamental Components
基础组件
-
AppProvider: All Polaris apps must be wrapped in.
<AppProvider i18n={enTranslations}> -
Page: The top-level container for a route. Always setand
title(if applicable).primaryActionjsx<Page title="Products" primaryAction={{content: 'Add product', onAction: handleAdd}} backAction={{content: 'Settings', url: '/settings'}} >v13.x Note:prop inbackActionis deprecated. Use App Bridge navigation instead for complex navigation patterns.Page.Header -
Layout: Useand
Layoutto structure content.Layout.Section- : For settings pages (Title/Description on left, Card on right).
Layout.AnnotatedSection - : Standard Full (default), 1/2 (
Layout.Section), or 1/3 (variant="oneHalf") width columns.variant="oneThird"
-
Card: The primary container for content pieces. Group related information in a Card.
- Use (vertical) or
BlockStack(horizontal) for internal layout within a Card.InlineStack - Do not use - it is deprecated. Use
LegacyCardwith layout primitives.Card
- Use
-
AppProvider:所有Polaris应用必须包裹在中。
<AppProvider i18n={enTranslations}> -
Page:路由的顶层容器,始终设置和
title(如果适用)。primaryActionjsx<Page title="Products" primaryAction={{content: 'Add product', onAction: handleAdd}} backAction={{content: 'Settings', url: '/settings'}} >v13.x注意事项:中的Page.Header属性已废弃,复杂导航场景请使用App Bridge导航能力替代。backAction -
Layout:使用和
Layout来组织内容结构。Layout.Section- :用于设置页面(左侧为标题/描述,右侧为卡片)。
Layout.AnnotatedSection - :标准全宽(默认)、1/2宽(
Layout.Section)或1/3宽(variant="oneHalf")列。variant="oneThird"
-
Card:内容块的主要容器,将相关信息分组放在Card中。
- 在Card内部使用(垂直布局)或
BlockStack(水平布局)做内部布局。InlineStack - 不要使用,它已被废弃,请使用
LegacyCard搭配布局原语。Card
- 在Card内部使用
Layout Primitives (Modern Pattern)
布局原语(现代模式)
jsx
import { Box, BlockStack, InlineStack, InlineGrid, Bleed, Divider } from '@shopify/polaris';
// Box - Low-level layout primitive with full token access
<Box padding="400" background="bg-surface-secondary" borderRadius="200">
Content here
</Box>
// BlockStack - Vertical stacking with gap
<BlockStack gap="400">
<Item1 />
<Item2 />
</BlockStack>
// InlineStack - Horizontal layout with alignment
<InlineStack gap="200" align="center" blockAlign="center">
<Icon />
<Text>Label</Text>
</InlineStack>
// InlineGrid - Responsive grid layout
<InlineGrid columns={{xs: 1, sm: 2, md: 3}} gap="400">
<Card>...</Card>
<Card>...</Card>
<Card>...</Card>
</InlineGrid>
// Bleed - Negative margin for edge-to-edge content
<Card>
<Bleed marginInline="400">
<img src="banner.jpg" style={{width: '100%'}} />
</Bleed>
</Card>jsx
import { Box, BlockStack, InlineStack, InlineGrid, Bleed, Divider } from '@shopify/polaris';
// Box - 支持完整令牌访问的底层布局原语
<Box padding="400" background="bg-surface-secondary" borderRadius="200">
Content here
</Box>
// BlockStack - 带间距的垂直堆叠布局
<BlockStack gap="400">
<Item1 />
<Item2 />
</BlockStack>
// InlineStack - 带对齐配置的水平布局
<InlineStack gap="200" align="center" blockAlign="center">
<Icon />
<Text>Label</Text>
</InlineStack>
// InlineGrid - 响应式网格布局
<InlineGrid columns={{xs: 1, sm: 2, md: 3}} gap="400">
<Card>...</Card>
<Card>...</Card>
<Card>...</Card>
</InlineGrid>
// Bleed - 负边距实现边到边的内容展示
<Card>
<Bleed marginInline="400">
<img src="banner.jpg" style={{width: '100%'}} />
</Bleed>
</Card>Data Display
数据展示
-
IndexTable: For lists of objects (Products, Orders) with bulk actions and filtering.jsx
import { IndexTable, Card, Text, Badge, useIndexResourceState } from '@shopify/polaris'; function ProductList({ products }) { const { selectedResources, allResourcesSelected, handleSelectionChange } = useIndexResourceState(products); const rowMarkup = products.map((product, index) => ( <IndexTable.Row id={product.id} key={product.id} selected={selectedResources.includes(product.id)} position={index} > <IndexTable.Cell> <Text variant="bodyMd" fontWeight="bold">{product.name}</Text> </IndexTable.Cell> <IndexTable.Cell>{product.sku}</IndexTable.Cell> <IndexTable.Cell> <Badge tone={product.status === 'active' ? 'success' : 'info'}> {product.status} </Badge> </IndexTable.Cell> </IndexTable.Row> )); return ( <Card padding="0"> <IndexTable resourceName={{singular: 'product', plural: 'products'}} itemCount={products.length} selectedItemsCount={allResourcesSelected ? 'All' : selectedResources.length} onSelectionChange={handleSelectionChange} headings={[ {title: 'Name'}, {title: 'SKU'}, {title: 'Status'}, ]} > {rowMarkup} </IndexTable> </Card> ); } -
DataTable: For simple, non-interactive data grids (e.g., analytics data).
-
ResourceList: For simpler lists without table structure (usefor each item).
ResourceItem
-
IndexTable:用于展示对象列表(商品、订单),支持批量操作和筛选。jsx
import { IndexTable, Card, Text, Badge, useIndexResourceState } from '@shopify/polaris'; function ProductList({ products }) { const { selectedResources, allResourcesSelected, handleSelectionChange } = useIndexResourceState(products); const rowMarkup = products.map((product, index) => ( <IndexTable.Row id={product.id} key={product.id} selected={selectedResources.includes(product.id)} position={index} > <IndexTable.Cell> <Text variant="bodyMd" fontWeight="bold">{product.name}</Text> </IndexTable.Cell> <IndexTable.Cell>{product.sku}</IndexTable.Cell> <IndexTable.Cell> <Badge tone={product.status === 'active' ? 'success' : 'info'}> {product.status} </Badge> </IndexTable.Cell> </IndexTable.Row> )); return ( <Card padding="0"> <IndexTable resourceName={{singular: 'product', plural: 'products'}} itemCount={products.length} selectedItemsCount={allResourcesSelected ? 'All' : selectedResources.length} onSelectionChange={handleSelectionChange} headings={[ {title: 'Name'}, {title: 'SKU'}, {title: 'Status'}, ]} > {rowMarkup} </IndexTable> </Card> ); } -
DataTable:用于简单的非交互式数据网格(例如分析数据)。
-
ResourceList:用于没有表格结构的简单列表(每个条目使用实现)。
ResourceItem
Form Design
表单设计
jsx
import {
Form, FormLayout, TextField, Select, Checkbox,
ChoiceList, RadioButton, RangeSlider, ColorPicker,
DropZone, Tag, Autocomplete
} from '@shopify/polaris';
function ProductForm() {
const [formState, setFormState] = useState({
title: '',
description: '',
status: 'draft',
tags: [],
});
const [errors, setErrors] = useState({});
return (
<Form onSubmit={handleSubmit}>
<FormLayout>
<TextField
label="Product title"
value={formState.title}
onChange={(value) => setFormState({...formState, title: value})}
error={errors.title}
autoComplete="off"
helpText="This will be displayed to customers"
/>
<TextField
label="Description"
value={formState.description}
onChange={(value) => setFormState({...formState, description: value})}
multiline={4}
autoComplete="off"
/>
<Select
label="Status"
options={[
{label: 'Draft', value: 'draft'},
{label: 'Active', value: 'active'},
{label: 'Archived', value: 'archived'},
]}
value={formState.status}
onChange={(value) => setFormState({...formState, status: value})}
/>
<FormLayout.Group>
<TextField label="Price" type="number" prefix="$" />
<TextField label="Compare at price" type="number" prefix="$" />
</FormLayout.Group>
<ChoiceList
title="Availability"
choices={[
{label: 'Online Store', value: 'online'},
{label: 'Point of Sale', value: 'pos'},
{label: 'Buy Button', value: 'buy_button'},
]}
selected={formState.channels}
onChange={(value) => setFormState({...formState, channels: value})}
allowMultiple
/>
</FormLayout>
</Form>
);
}jsx
import {
Form, FormLayout, TextField, Select, Checkbox,
ChoiceList, RadioButton, RangeSlider, ColorPicker,
DropZone, Tag, Autocomplete
} from '@shopify/polaris';
function ProductForm() {
const [formState, setFormState] = useState({
title: '',
description: '',
status: 'draft',
tags: [],
});
const [errors, setErrors] = useState({});
return (
<Form onSubmit={handleSubmit}>
<FormLayout>
<TextField
label="Product title"
value={formState.title}
onChange={(value) => setFormState({...formState, title: value})}
error={errors.title}
autoComplete="off"
helpText="This will be displayed to customers"
/>
<TextField
label="Description"
value={formState.description}
onChange={(value) => setFormState({...formState, description: value})}
multiline={4}
autoComplete="off"
/>
<Select
label="Status"
options={[
{label: 'Draft', value: 'draft'},
{label: 'Active', value: 'active'},
{label: 'Archived', value: 'archived'},
]}
value={formState.status}
onChange={(value) => setFormState({...formState, status: value})}
/>
<FormLayout.Group>
<TextField label="Price" type="number" prefix="$" />
<TextField label="Compare at price" type="number" prefix="$" />
</FormLayout.Group>
<ChoiceList
title="Availability"
choices={[
{label: 'Online Store', value: 'online'},
{label: 'Point of Sale', value: 'pos'},
{label: 'Buy Button', value: 'buy_button'},
]}
selected={formState.channels}
onChange={(value) => setFormState({...formState, channels: value})}
allowMultiple
/>
</FormLayout>
</Form>
);
}Filters & Search (IndexFilters)
筛选与搜索(IndexFilters)
jsx
import {
IndexFilters, useSetIndexFiltersMode, IndexFiltersMode,
ChoiceList, RangeSlider, TextField
} from '@shopify/polaris';
function FilteredList() {
const [queryValue, setQueryValue] = useState('');
const [status, setStatus] = useState([]);
const { mode, setMode } = useSetIndexFiltersMode(IndexFiltersMode.Filtering);
const filters = [
{
key: 'status',
label: 'Status',
filter: (
<ChoiceList
title="Status"
titleHidden
choices={[
{label: 'Active', value: 'active'},
{label: 'Draft', value: 'draft'},
{label: 'Archived', value: 'archived'},
]}
selected={status}
onChange={setStatus}
allowMultiple
/>
),
shortcut: true,
},
];
const appliedFilters = status.length > 0
? [{key: 'status', label: `Status: ${status.join(', ')}`}]
: [];
return (
<IndexFilters
queryValue={queryValue}
queryPlaceholder="Search products"
onQueryChange={setQueryValue}
onQueryClear={() => setQueryValue('')}
filters={filters}
appliedFilters={appliedFilters}
onClearAll={() => setStatus([])}
mode={mode}
setMode={setMode}
tabs={[
{content: 'All', id: 'all'},
{content: 'Active', id: 'active'},
{content: 'Draft', id: 'draft'},
]}
selected={0}
/>
);
}jsx
import {
IndexFilters, useSetIndexFiltersMode, IndexFiltersMode,
ChoiceList, RangeSlider, TextField
} from '@shopify/polaris';
function FilteredList() {
const [queryValue, setQueryValue] = useState('');
const [status, setStatus] = useState([]);
const { mode, setMode } = useSetIndexFiltersMode(IndexFiltersMode.Filtering);
const filters = [
{
key: 'status',
label: 'Status',
filter: (
<ChoiceList
title="Status"
titleHidden
choices={[
{label: 'Active', value: 'active'},
{label: 'Draft', value: 'draft'},
{label: 'Archived', value: 'archived'},
]}
selected={status}
onChange={setStatus}
allowMultiple
/>
),
shortcut: true,
},
];
const appliedFilters = status.length > 0
? [{key: 'status', label: `Status: ${status.join(', ')}`}]
: [];
return (
<IndexFilters
queryValue={queryValue}
queryPlaceholder="Search products"
onQueryChange={setQueryValue}
onQueryClear={() => setQueryValue('')}
filters={filters}
appliedFilters={appliedFilters}
onClearAll={() => setStatus([])}
mode={mode}
setMode={setMode}
tabs={[
{content: 'All', id: 'all'},
{content: 'Active', id: 'active'},
{content: 'Draft', id: 'draft'},
]}
selected={0}
/>
);
}Design Tokens & CSS (v13.x)
设计令牌与CSS(v13.x)
- Avoid Custom CSS: 95% of styling should be handled by Polaris props (,
gap,padding,align).justify - Design Tokens: If you MUST use custom CSS, use Polaris CSS Custom Properties (Tokens).
- 避免自定义CSS:95%的样式需求可以通过Polaris属性(、
gap、padding、align)实现。justify - 设计令牌:如果你必须使用自定义CSS,请使用Polaris CSS自定义属性(令牌)。
Spacing Tokens (4px base)
间距令牌(基础单位4px)
| Token | Value | Usage |
|---|---|---|
| 2px | Minimal spacing |
| 4px | Tight spacing |
| 8px | Compact spacing |
| 12px | Default small |
| 16px | Default standard |
| 20px | Medium spacing |
| 24px | Large spacing |
| 32px | Section spacing |
| 40px | Page spacing |
| 48px | Extra large |
| 令牌 | 取值 | 使用场景 |
|---|---|---|
| 2px | 极小间距 |
| 4px | 紧凑间距 |
| 8px | 紧凑型间距 |
| 12px | 默认小间距 |
| 16px | 默认标准间距 |
| 20px | 中等间距 |
| 24px | 大间距 |
| 32px | 区块间距 |
| 40px | 页面间距 |
| 48px | 超大间距 |
Color Tokens
颜色令牌
css
/* Backgrounds */
--p-color-bg /* Default page background */
--p-color-bg-surface /* Card/surface background */
--p-color-bg-surface-secondary /* Secondary surface */
--p-color-bg-surface-hover /* Hover state */
--p-color-bg-surface-selected /* Selected state */
--p-color-bg-fill-brand /* Primary brand fill */
--p-color-bg-fill-success /* Success background */
--p-color-bg-fill-warning /* Warning background */
--p-color-bg-fill-critical /* Critical/error background */
/* Text */
--p-color-text /* Default text */
--p-color-text-secondary /* Subdued text */
--p-color-text-disabled /* Disabled text */
--p-color-text-brand /* Brand colored text */
--p-color-text-success /* Success text */
--p-color-text-warning /* Warning text */
--p-color-text-critical /* Error text */
/* Borders */
--p-color-border /* Default border */
--p-color-border-hover /* Hover border */
--p-color-border-focus /* Focus ring */
--p-color-border-brand /* Brand border */css
/* 背景色 */
--p-color-bg /* 默认页面背景 */
--p-color-bg-surface /* 卡片/容器背景 */
--p-color-bg-surface-secondary /* 二级容器背景 */
--p-color-bg-surface-hover /* 悬停状态背景 */
--p-color-bg-surface-selected /* 选中状态背景 */
--p-color-bg-fill-brand /* 品牌主色填充 */
--p-color-bg-fill-success /* 成功状态背景 */
--p-color-bg-fill-warning /* 警告状态背景 */
--p-color-bg-fill-critical /* 严重/错误状态背景 */
/* 文本色 */
--p-color-text /* 默认文本色 */
--p-color-text-secondary /* 弱化文本色 */
--p-color-text-disabled /* 禁用状态文本色 */
--p-color-text-brand /* 品牌色文本 */
--p-color-text-success /* 成功状态文本 */
--p-color-text-warning /* 警告状态文本 */
--p-color-text-critical /* 错误状态文本 */
/* 边框色 */
--p-color-border /* 默认边框 */
--p-color-border-hover /* 悬停状态边框 */
--p-color-border-focus /* 聚焦环 */
--p-color-border-brand /* 品牌色边框 */Border Radius Tokens
圆角令牌
css
--p-border-radius-100 /* 4px - Small elements */
--p-border-radius-200 /* 8px - Cards, buttons */
--p-border-radius-300 /* 12px - Large cards */
--p-border-radius-full /* 9999px - Pills, avatars */css
--p-border-radius-100 /* 4px - 小型元素 */
--p-border-radius-200 /* 8px - 卡片、按钮 */
--p-border-radius-300 /* 12px - 大型卡片 */
--p-border-radius-full /* 9999px - 胶囊形状、头像 */Shadow Tokens
阴影令牌
css
--p-shadow-100 /* Subtle shadow */
--p-shadow-200 /* Card shadow */
--p-shadow-300 /* Elevated shadow */
--p-shadow-400 /* Modal shadow */css
--p-shadow-100 /* 淡阴影 */
--p-shadow-200 /* 卡片阴影 */
--p-shadow-300 /* 抬高元素阴影 */
--p-shadow-400 /* 弹窗阴影 */Typography
排版
jsx
// Use Text component with variants instead of HTML tags
<Text variant="headingXl">Page Title</Text> // 28px bold
<Text variant="headingLg">Section Title</Text> // 24px bold
<Text variant="headingMd">Card Title</Text> // 20px semibold
<Text variant="headingSm">Subsection</Text> // 16px semibold
<Text variant="headingXs">Small Header</Text> // 14px semibold
<Text variant="bodyLg">Large body</Text> // 16px regular
<Text variant="bodyMd">Default body</Text> // 14px regular
<Text variant="bodySm">Small text</Text> // 12px regular
// Tones for semantic meaning
<Text tone="subdued">Secondary information</Text>
<Text tone="success">Success message</Text>
<Text tone="critical">Error message</Text>
<Text tone="caution">Warning message</Text>jsx
// 使用带variant属性的Text组件替代原生HTML标签
<Text variant="headingXl">Page Title</Text> // 28px 粗体
<Text variant="headingLg">Section Title</Text> // 24px 粗体
<Text variant="headingMd">Card Title</Text> // 20px 半粗体
<Text variant="headingSm">Subsection</Text> // 16px 半粗体
<Text variant="headingXs">Small Header</Text> // 14px 半粗体
<Text variant="bodyLg">Large body</Text> // 16px 常规字重
<Text variant="bodyMd">Default body</Text> // 14px 常规字重
<Text variant="bodySm">Small text</Text> // 12px 常规字重
// 语义化tone属性
<Text tone="subdued">Secondary information</Text>
<Text tone="success">Success message</Text>
<Text tone="critical">Error message</Text>
<Text tone="caution">Warning message</Text>Deprecated Components (v13.x) - DO NOT USE
已废弃组件(v13.x)- 请勿使用
These components will be removed in future versions:
| Deprecated | Use Instead |
|---|---|
| |
| |
| |
| |
| App Bridge Modal API |
| App Bridge Navigation Menu |
| App Bridge Toast API |
| App Bridge |
| App Bridge Title Bar |
| App Bridge Loading API |
| App Bridge handles this |
| App Bridge Modal or custom |
| |
| |
| |
| |
| |
| |
| Custom with |
| |
| Use |
这些组件将在未来版本中被移除:
| 已废弃组件 | 替代方案 |
|---|---|
| |
| |
| |
| |
| App Bridge Modal API |
| App Bridge Navigation Menu |
| App Bridge Toast API |
| App Bridge |
| App Bridge Title Bar |
| App Bridge Loading API |
| 由App Bridge自动处理 |
| App Bridge Modal或自定义实现 |
| |
| |
| |
| |
| |
| 带gap的 |
| 自定义实现: |
| |
| 使用Text组件的 |
Code Style Example (v13.x Best Practices)
代码风格示例(v13.x最佳实践)
jsx
import {
Page, Layout, Card, BlockStack, InlineStack,
Text, Button, Badge, Box, Divider, Banner,
IndexTable, useIndexResourceState
} from '@shopify/polaris';
import { ExportIcon, PlusIcon } from '@shopify/polaris-icons';
export default function Dashboard({ products }) {
const { selectedResources, allResourcesSelected, handleSelectionChange } =
useIndexResourceState(products);
const promotedBulkActions = [
{ content: 'Export selected', icon: ExportIcon },
];
const rowMarkup = products.map((product, index) => (
<IndexTable.Row
id={product.id}
key={product.id}
selected={selectedResources.includes(product.id)}
position={index}
>
<IndexTable.Cell>
<Text variant="bodyMd" fontWeight="bold">{product.title}</Text>
</IndexTable.Cell>
<IndexTable.Cell>
<Badge tone={product.status === 'active' ? 'success' : 'info'}>
{product.status}
</Badge>
</IndexTable.Cell>
<IndexTable.Cell>${product.price}</IndexTable.Cell>
</IndexTable.Row>
));
return (
<Page
title="Dashboard"
primaryAction={{
content: 'Add product',
icon: PlusIcon,
onAction: () => {}
}}
secondaryActions={[
{content: 'Export', icon: ExportIcon, onAction: () => {}}
]}
>
<Layout>
<Layout.Section>
<Banner tone="info" onDismiss={() => {}}>
<p>New: Try our improved bulk editing features.</p>
</Banner>
</Layout.Section>
<Layout.Section>
<Card padding="0">
<IndexTable
resourceName={{singular: 'product', plural: 'products'}}
itemCount={products.length}
selectedItemsCount={allResourcesSelected ? 'All' : selectedResources.length}
onSelectionChange={handleSelectionChange}
headings={[
{title: 'Product'},
{title: 'Status'},
{title: 'Price', alignment: 'end'},
]}
promotedBulkActions={promotedBulkActions}
>
{rowMarkup}
</IndexTable>
</Card>
</Layout.Section>
<Layout.Section variant="oneThird">
<Card>
<BlockStack gap="400">
<Text as="h2" variant="headingMd">Quick Stats</Text>
<Divider />
<BlockStack gap="200">
<InlineStack align="space-between">
<Text tone="subdued">Total products</Text>
<Text fontWeight="semibold">{products.length}</Text>
</InlineStack>
<InlineStack align="space-between">
<Text tone="subdued">Active</Text>
<Text fontWeight="semibold">
{products.filter(p => p.status === 'active').length}
</Text>
</InlineStack>
</BlockStack>
</BlockStack>
</Card>
<Box paddingBlockStart="400">
<Card>
<BlockStack gap="300">
<Text as="h3" variant="headingSm">Quick Actions</Text>
<Button variant="plain" url="/settings">
View all settings
</Button>
</BlockStack>
</Card>
</Box>
</Layout.Section>
</Layout>
</Page>
);
}jsx
import {
Page, Layout, Card, BlockStack, InlineStack,
Text, Button, Badge, Box, Divider, Banner,
IndexTable, useIndexResourceState
} from '@shopify/polaris';
import { ExportIcon, PlusIcon } from '@shopify/polaris-icons';
export default function Dashboard({ products }) {
const { selectedResources, allResourcesSelected, handleSelectionChange } =
useIndexResourceState(products);
const promotedBulkActions = [
{ content: 'Export selected', icon: ExportIcon },
];
const rowMarkup = products.map((product, index) => (
<IndexTable.Row
id={product.id}
key={product.id}
selected={selectedResources.includes(product.id)}
position={index}
>
<IndexTable.Cell>
<Text variant="bodyMd" fontWeight="bold">{product.title}</Text>
</IndexTable.Cell>
<IndexTable.Cell>
<Badge tone={product.status === 'active' ? 'success' : 'info'}>
{product.status}
</Badge>
</IndexTable.Cell>
<IndexTable.Cell>${product.price}</IndexTable.Cell>
</IndexTable.Row>
));
return (
<Page
title="Dashboard"
primaryAction={{
content: 'Add product',
icon: PlusIcon,
onAction: () => {}
}}
secondaryActions={[
{content: 'Export', icon: ExportIcon, onAction: () => {}}
]}
>
<Layout>
<Layout.Section>
<Banner tone="info" onDismiss={() => {}}>
<p>New: Try our improved bulk editing features.</p>
</Banner>
</Layout.Section>
<Layout.Section>
<Card padding="0">
<IndexTable
resourceName={{singular: 'product', plural: 'products'}}
itemCount={products.length}
selectedItemsCount={allResourcesSelected ? 'All' : selectedResources.length}
onSelectionChange={handleSelectionChange}
headings={[
{title: 'Product'},
{title: 'Status'},
{title: 'Price', alignment: 'end'},
]}
promotedBulkActions={promotedBulkActions}
>
{rowMarkup}
</IndexTable>
</Card>
</Layout.Section>
<Layout.Section variant="oneThird">
<Card>
<BlockStack gap="400">
<Text as="h2" variant="headingMd">Quick Stats</Text>
<Divider />
<BlockStack gap="200">
<InlineStack align="space-between">
<Text tone="subdued">Total products</Text>
<Text fontWeight="semibold">{products.length}</Text>
</InlineStack>
<InlineStack align="space-between">
<Text tone="subdued">Active</Text>
<Text fontWeight="semibold">
{products.filter(p => p.status === 'active').length}
</Text>
</InlineStack>
</BlockStack>
</BlockStack>
</Card>
<Box paddingBlockStart="400">
<Card>
<BlockStack gap="300">
<Text as="h3" variant="headingSm">Quick Actions</Text>
<Button variant="plain" url="/settings">
View all settings
</Button>
</BlockStack>
</Card>
</Box>
</Layout.Section>
</Layout>
</Page>
);
}Anti-Patterns to AVOID
需要避免的反模式
- DO NOT use deprecated components (,
LegacyCard,Modal, etc.).Toast - DO NOT use Shadows or Borders manually. Cards handle this.
- DO NOT use . Use
style={{ margin: 10 }}or<Box padding="400">.<BlockStack gap="400"> - DO NOT create a "Save" button at the bottom of a form. Use App Bridge Save Bar.
- DO NOT use generic loading spinners. Use or
<SkeletonPage>for loading states.<SkeletonBodyText> - DO NOT use ,
<h1>, etc. directly. Use<h2>.<Text as="h2" variant="headingMd"> - DO NOT use inline styles. Use Box/BlockStack props or design tokens.
- DO NOT import from . Use named exports from
@shopify/polaris/build/esm/....@shopify/polaris
- 不要使用已废弃的组件(、
LegacyCard、Modal等)。Toast - 不要手动设置阴影或边框,Card组件会自动处理。
- 不要使用,请使用
style={{ margin: 10 }}或<Box padding="400">。<BlockStack gap="400"> - 不要在表单底部创建“保存”按钮,请使用App Bridge保存栏。
- 不要使用通用加载 spinner,请使用或
<SkeletonPage>实现加载状态。<SkeletonBodyText> - 不要直接使用、
<h1>等原生标题标签,请使用<h2>。<Text as="h2" variant="headingMd"> - 不要使用内联样式,请使用Box/BlockStack属性或设计令牌。
- 不要从导入组件,请使用
@shopify/polaris/build/esm/...的命名导出。@shopify/polaris
Internationalization Support (v13.10+)
国际化支持(v13.10+)
Polaris v13.10 added translations for 8 new languages: Hindi, Lithuanian, Bulgarian, Hungarian, Romanian, Russian, Indonesian, and Greek.
jsx
import enTranslations from '@shopify/polaris/locales/en.json';
import viTranslations from '@shopify/polaris/locales/vi.json';
// Use the appropriate translations based on merchant locale
<AppProvider i18n={merchantLocale === 'vi' ? viTranslations : enTranslations}>
<App />
</AppProvider>Polaris v13.10新增了8种语言的翻译:印地语、立陶宛语、保加利亚语、匈牙利语、罗马尼亚语、俄语、印度尼西亚语和希腊语。
jsx
import enTranslations from '@shopify/polaris/locales/en.json';
import viTranslations from '@shopify/polaris/locales/vi.json';
// 根据商家的地区设置使用对应翻译
<AppProvider i18n={merchantLocale === 'vi' ? viTranslations : enTranslations}>
<App />
</AppProvider>