> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/contributor/contributor-zh/gong-xian-zhe-zhi-nan/web-ui-biao-zhun/styling-and-theming.md).

# 样式与主题

本页涵盖使用 styled-components 结合 Material UI 的样式解决方案为 Decentraland Web UI 制定的全面样式标准。

{% hint style="info" %}
本文档中的所有代码示例仅用于说明。它们不代表生产组件，而是展示模式和标准。
{% endhint %}

## 核心原则

1. **仅使用对象语法** - 使用对象记法以获得强大的 TypeScript 支持
2. **以主题为先** - 所有值都来自 UI2 主题
3. **不使用内联样式** - 所有样式均使用 styled 组件
4. **全部加类型** - 利用 TypeScript 处理 props 和 theme
5. **默认响应式** - 使用主题断点

***

## 对象语法标准

UI2 组件 **必须** 使用对象语法。这可确保强大的 TypeScript 支持、csstype 校验以及直接的主题集成。

{% hint style="warning" %}
模板字面量语法 **不允许**。始终使用对象记法。
{% endhint %}

### 基础示例

```tsx
// ✅ 好：对象语法
const Button = styled('button')({
  color: 'turquoise',
  padding: '8px 16px',
});

// ❌ 不好：模板字面量语法
const Button = styled.button`
  color: turquoise;
  padding: 8px 16px;
`;
```

### 结合 Theme 和 Props

```tsx
interface ButtonProps {
  primary?: boolean;
  size?: 'small' | 'medium' | 'large';
}

// ✅ 好：结合 theme 和 props 的对象语法
const Button = styled('button')<ButtonProps>(({ theme, primary, size = 'medium' }) => ({
  color: primary ? theme.palette.primary.main : theme.palette.text.primary,
  backgroundColor: primary ? theme.palette.primary.main : 'transparent',
  borderRadius: theme.shape.borderRadius,
  padding: {
    small: theme.spacing(0.5, 1),
    medium: theme.spacing(1, 2),
    large: theme.spacing(1.5, 3),
  }[size],
}));
```

{% hint style="danger" %}
**规则**：值必须始终来自 UI2 主题。不允许任意的十六进制颜色值或像素值。
{% endhint %}

***

## 元素语法

为原生 HTML 元素设置样式时，始终使用函数调用形式 `styled('tag')`.

### 正确语法

```tsx
// ✅ 好：函数调用形式
const Container = styled('div')({
  display: 'flex',
  flexDirection: 'column',
});

const Action = styled('button')(({ theme }) => ({
  color: theme.palette.primary.main,
  padding: theme.spacing(1, 2),
}));

const Label = styled('label')(({ theme }) => ({
  color: theme.palette.text.secondary,
  fontSize: theme.typography.caption.fontSize,
}));
```

### 错误语法

```tsx
// ❌ 不好：属性形式（旧语法）
const Container = styled.div`
  display: flex;
  flex-direction: column;
`;

const Action = styled.button`
  color: ${props => props.theme.palette.primary.main};
`;
```

***

## 不使用内联样式

内联样式（`style={...}`) **绝不** 可用于 UI2 组件。

### 为什么不使用内联样式？

* 绕过主题类型检查
* 更难维护
* 阻碍复用
* 无法优化
* 没有 TypeScript 校验

### 正确做法

```tsx
// ❌ 不好：内联样式
<Card 
  key={id} 
  style={{ backgroundColor: color } as React.CSSProperties}
>
  <CardHeader>
    <Title style={{ fontSize: 20 }}>{title}</Title>
  </CardHeader>
</Card>

// ✅ 好：带 props 的 styled 组件
interface CardProps {
  backgroundColor: string;
}

interface TitleProps {
  size: number;
}

const StyledCard = styled('div')<CardProps>(({ backgroundColor, theme }) => ({
  backgroundColor,
  borderRadius: theme.shape.borderRadius,
  padding: theme.spacing(2),
}));

const Title = styled('h2')<TitleProps>(({ size, theme }) => ({
  fontSize: theme.typography.pxToRem(size),
  color: theme.palette.text.primary,
}));

<StyledCard key={id} backgroundColor={color}>
  <CardHeader>
    <Title size={20}>{title}</Title>
  </CardHeader>
</StyledCard>
```

***

## 断点

使用 `theme.breakpoints` 辅助方法，而不是硬编码像素值。

### 断点辅助方法

| 辅助方法                  | 用法                                      | 说明       |
| --------------------- | --------------------------------------- | -------- |
| `up(key)`             | `theme.breakpoints.up('md')`            | 最小宽度及以上  |
| `down(key)`           | `theme.breakpoints.down('md')`          | 最大宽度及以下  |
| `between(start, end)` | `theme.breakpoints.between('sm', 'lg')` | 介于两个断点之间 |
| `only(key)`           | `theme.breakpoints.only('md')`          | 仅在此断点    |

### 示例

```tsx
// ❌ 不好：硬编码断点
const Panel = styled('div')`
  @media (max-width: 768px) {
    width: 100%;
  }
`;

// ✅ 好：主题断点
interface PanelProps {
  expanded: boolean;
}

const Panel = styled('div')<PanelProps>(({ theme, expanded }) => ({
  width: expanded ? '400px' : '0',
  transition: theme.transitions.create('width'),
  
  [theme.breakpoints.down('sm')]: {
    width: expanded ? '100%' : '0',
  },
}));
```

### 多个断点

```tsx
const Layout = styled('div')(({ theme }) => ({
  display: 'grid',
  gridTemplateColumns: '1fr 320px',
  gap: theme.spacing(2),
  
  // 移动端：单列
  [theme.breakpoints.down('md')]: {
    gridTemplateColumns: '1fr',
  },
  
  // 大屏桌面：更宽的侧边栏
  [theme.breakpoints.up('xl')]: {
    gridTemplateColumns: '1fr 400px',
  },
  
  // 平板范围：不同的间距
  [theme.breakpoints.between('sm', 'lg')]: {
    gap: theme.spacing(3),
  },
  
  // 仅平板
  [theme.breakpoints.only('md')]: {
    padding: theme.spacing(2),
  },
}));
```

***

## 间距比例

使用 `theme.spacing` 专用于 margin、padding 和 gap。

### 间距约定

* `theme.spacing(n)` 其中 `n` 是一个数字
* 基础单位通常是 8px
* `spacing(1)` = 8px， `spacing(2)` = 16px，等等。
* 允许小数： `spacing(1.5)` = 12px

```tsx
// ✅ 好：主题间距
const Box = styled('div')(({ theme }) => ({
  padding: theme.spacing(2),           // 16px
  margin: theme.spacing(1, 0),         // 垂直 8px，水平 0
  gap: theme.spacing(1.5),             // 12px
  paddingInline: theme.spacing(3),     // 左右 24px
}));

// ❌ 不好：原始像素值
const Box = styled('div')({
  padding: '16px',
  margin: '8px 0',
  gap: '12px',
});
```

### 常见间距模式

```tsx
const Card = styled('div')(({ theme }) => ({
  // 四周间距一致
  padding: theme.spacing(3),
  
  // 垂直/水平不同
  padding: theme.spacing(2, 3),  // 垂直 16px，水平 24px
  
  // 四边不同
  padding: theme.spacing(1, 2, 3, 2),  // 上、右、下、左
  
  // 逻辑属性（RTL 支持优先推荐）
  paddingBlock: theme.spacing(2),      // 上下
  paddingInline: theme.spacing(3),     // 左右
  marginBlockStart: theme.spacing(1),  // margin-top
}));
```

***

## z-index 与层叠

遵循主题的 z-index 规模。绝不要使用任意的 z-index 值。

### 主题 z-index 值

```tsx
theme.zIndex.mobileStepper  // 1000
theme.zIndex.fab            // 1050
theme.zIndex.speedDial      // 1050
theme.zIndex.appBar         // 1100
theme.zIndex.drawer         // 1200
theme.zIndex.modal          // 1300
theme.zIndex.snackbar       // 1400
theme.zIndex.tooltip        // 1500
```

### 正确用法

```tsx
// ✅ 好：主题 z-index
const StickyBar = styled('div')(({ theme }) => ({
  position: 'sticky',
  top: 0,
  zIndex: theme.zIndex.appBar,
  backgroundColor: theme.palette.background.paper,
}));

const Overlay = styled('div')(({ theme }) => ({
  position: 'fixed',
  inset: 0,
  zIndex: theme.zIndex.modal,
  backgroundColor: 'rgba(0, 0, 0, 0.5)',
}));

// ❌ 不好：任意 z-index
const StickyBar = styled('div')({
  position: 'sticky',
  top: 0,
  zIndex: 999,  // 不要这样做
});
```

### 层叠上下文最佳实践

* 注意不要创建新的层叠上下文
* 避免不必要的 `position: relative` 在父元素上使用
* 记录为什么需要 z-index
* 如果需要新层，先把它添加到主题中

***

## 颜色令牌

始终从以下来源获取颜色： `theme.palette` 和 `dclColors`。不要使用临时的十六进制值。

### 调色板结构

```tsx
// 文本颜色
theme.palette.text.primary
theme.palette.text.secondary
theme.palette.text.disabled

// 背景颜色
theme.palette.background.default
theme.palette.background.paper

// 主色/次色/错误色
theme.palette.primary.main
theme.palette.primary.light
theme.palette.primary.dark
theme.palette.primary.contrastText

// 动作颜色
theme.palette.action.active
theme.palette.action.hover
theme.palette.action.selected
theme.palette.action.disabled
theme.palette.action.disabledBackground

// 分隔线
theme.palette.divider
```

### Decentraland 颜色

```tsx
import { dclColors } from 'decentraland-ui2';

// 稀有度颜色
dclColors.rarity.unique
dclColors.rarity.mythic
dclColors.rarity.legendary
dclColors.rarity.epic
dclColors.rarity.rare
dclColors.rarity.uncommon
dclColors.rarity.common
```

### 示例

```tsx
// ✅ 好：主题颜色
const Chip = styled('span')(({ theme }) => ({
  color: theme.palette.text.secondary,
  backgroundColor: theme.palette.background.paper,
  borderColor: theme.palette.divider,
  
  '&:hover': {
    backgroundColor: theme.palette.action.hover,
  },
}));

const RarityBadge = styled('span')<{ rarity: string }>(({ rarity }) => ({
  backgroundColor: dclColors.rarity[rarity],
  color: '#FFFFFF', // 对比色——如有说明则可接受
  padding: '4px 8px',
  borderRadius: '4px',
}));

// ❌ 不好：任意十六进制值
const Chip = styled('span')({
  color: '#666666',
  backgroundColor: '#FFFFFF',
  borderColor: '#E0E0E0',
});
```

***

## 交互状态

所有交互控件都必须显示可见的悬停、聚焦、激活和禁用状态。

### 完整的交互组件

```tsx
const InteractiveButton = styled('button')(({ theme }) => ({
  // 基础状态
  padding: theme.spacing(1, 2),
  backgroundColor: theme.palette.primary.main,
  color: theme.palette.primary.contrastText,
  border: 'none',
  borderRadius: theme.shape.borderRadius,
  cursor: 'pointer',
  outline: 'none',
  transition: theme.transitions.create([
    'background-color',
    'transform',
    'box-shadow',
  ]),
  
  // 悬停状态（鼠标）
  '&:hover': {
    backgroundColor: theme.palette.primary.dark,
  },
  
  // 聚焦状态（键盘导航）
  '&:focus-visible': {
    outline: `2px solid ${theme.palette.primary.main}`,
    outlineOffset: 2,
    boxShadow: theme.shadows[2],
  },
  
  // 激活/按下状态
  '&:active': {
    backgroundColor: theme.palette.primary.dark,
    transform: 'scale(0.98)',
  },
  
  // 禁用状态
  '&:disabled': {
    backgroundColor: theme.palette.action.disabledBackground,
    color: theme.palette.action.disabled,
    cursor: 'not-allowed',
    transform: 'none',
  },
}));
```

### 焦点可见模式

始终使用 `:focus-visible` 而不是 `:focus` 以避免在鼠标点击时显示焦点环：

```tsx
// ✅ 好：仅在键盘操作时显示焦点环
'&:focus-visible': {
  outline: `2px solid ${theme.palette.primary.main}`,
  outlineOffset: 2,
}

// ❌ 不好：每次点击都显示焦点环
'&:focus': {
  outline: `2px solid ${theme.palette.primary.main}`,
}
```

***

## 排版

使用主题的排版变体，而不是自定义字体属性。

```tsx
// ✅ 好：排版变体
const Heading = styled('h1')(({ theme }) => ({
  ...theme.typography.h1,
  marginBottom: theme.spacing(2),
}));

const Body = styled('p')(({ theme}) => ({
  ...theme.typography.body1,
  color: theme.palette.text.secondary,
}));

// 使用 MUI Typography 组件（推荐）
<Typography variant="h1">标题</Typography>
<Typography variant="body1">正文文本</Typography>

// ❌ 不好：自定义排版
const Heading = styled('h1')({
  fontSize: '32px',
  fontWeight: 700,
  lineHeight: 1.2,
});
```

***

## 性能优化

### 不要在渲染中创建样式组件

```tsx
// ❌ 不好：每次渲染都会创建新组件
function Component({ color }) {
  const Box = styled('div')({
    backgroundColor: color,
  });
  return <Box />;
}

// ✅ 好：只创建一次，传递 props
const Box = styled('div')<{ color: string }>(({ color }) => ({
  backgroundColor: color,
}));

function Component({ color }) {
  return <Box color={color} />;
}
```

### 对派生 props 进行记忆化

```tsx
// ❌ 不好：每次渲染都会创建新对象
<StyledComponent style={{ color: isDark ? 'white' : 'black' }} />

// ✅ 好：传递原始类型 props
const StyledComponent = styled('div')<{ isDark: boolean }>(({ isDark, theme }) => ({
  color: isDark ? theme.palette.common.white : theme.palette.common.black,
}));

<StyledComponent isDark={isDark} />
```

***

## 命名约定

### 组件名称

* **帕斯卡命名法** 用于组件
* 描述性名称

```tsx
// ✅ 好的名称
const UserCard = styled('div')({...});
const PrimaryButton = styled('button')({...});
const NavigationList = styled('ul')({...});

// ❌ 不好的名称
const card = styled('div')({...});
const btn = styled('button')({...});
const list1 = styled('ul')({...});
```

### 文件结构

```
Component.tsx        # 主组件
Component.styles.ts  # 样式化组件
Component.test.tsx   # 测试
Component.stories.tsx # Storybook
```

***

## 下一步

* 复习 [自定义组件](/contributor/contributor-zh/gong-xian-zhe-zhi-nan/web-ui-biao-zhun/custom-components.md) 用于组件创建指南
* 参见 [迁移指南](https://github.com/decentraland/docs/blob/main/contributor/contributor-guides/web-ui-standards/broken-reference/README.md) 用于从 UI1 迁移到 UI2
* 查看 [流程概览](/contributor/contributor-zh/gong-xian-zhe-zhi-nan/web-ui-biao-zhun/process-overview.md) 以查看完整工作流程


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/contributor/contributor-zh/gong-xian-zhe-zhi-nan/web-ui-biao-zhun/styling-and-theming.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
