expo-design-system
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExpo Design Systems
Expo 设计系统
Make every screen in an app draw from one visual source of truth: a token theme and a small set of reusable components. This skill defines where tokens live, what they cover, how reusable components are shaped, and when a repeated view earns promotion into the system.
Sibling skills own the layers around this one:
- - platform styling rules (HIG, semantic colors, controls, shadows syntax). Follow it for what values look native; follow this skill for where values live and how they're reused.
expo-native-ui - - if the project uses Tailwind, tokens live in
expo-tailwind-setupas CSS variables instead of TypeScript. The scales and naming in this skill still apply; only the storage format changes.global.css - - folder skeleton for new apps.
expo-project-structure
让应用中的每一个界面都遵循统一的视觉单一数据源:令牌主题与少量可复用组件。本技能定义了令牌的存放位置、覆盖范围、可复用组件的结构,以及何时将重复视图升级到系统中。
相关的同级技能负责本技能之外的层面:
- - 平台样式规则(HIG、语义化颜色、控件、阴影语法)。遵循该技能了解原生样式的取值;遵循本技能了解值的存放位置与复用方式。
expo-native-ui - - 如果项目使用Tailwind,令牌将以CSS变量形式存放在
expo-tailwind-setup中,而非TypeScript文件。本技能中的刻度与命名规则仍然适用,仅存储格式有所变化。global.css - - 新应用的文件夹骨架。
expo-project-structure
References
参考资料
Consult these resources as needed:
references/
audit.md Audit an existing app for design-system drift: grep checks,
scoring rubric, incremental adoption plan, and templates for
documenting or extending components按需查阅以下资源:
references/
audit.md 检查现有应用是否存在设计系统偏差:grep检查、评分标准、渐进式采用方案,以及用于文档化或扩展组件的模板Adopt Before You Build
构建前先采用
In an app that already has screens, the first move is detection, not construction. Before writing any token file:
- Look for a declared system. Check for a styling library - NativeWind/Tailwind (use
package.json), Tamagui, Restyle, Unistyles, styled-components. Then look for a token file:expo-tailwind-setup,theme.ts,src/theme/, orconstants/theme.ts(the create-expo-app default).constants/Colors.ts - If one exists, it is the source of truth. Extend it in its own idiom - its names, its scale, its storage format. Audit drift against that system, not against the examples below.
- If only de facto values exist - the same greys and paddings repeated across screens, no theme file - there is no system yet. Those values are the input to the scales, not the authority: derive tokens from the most frequent ones, snapped to the 4-point grid (§5).
references/audit.md - Never introduce a second system beside an existing one. A fresh next to a Tamagui config is design-system drift, not adoption.
src/theme/
Only when nothing exists do the defaults below apply as written.
对于已有界面的应用,第一步是检测而非构建。在编写任何令牌文件前:
- 查找已声明的系统。检查中的样式库——NativeWind/Tailwind(请使用
package.json)、Tamagui、Restyle、Unistyles、styled-components。然后查找令牌文件:expo-tailwind-setup、theme.ts、src/theme/或constants/theme.ts(create-expo-app的默认文件)。constants/Colors.ts - 若存在已声明的系统,则它就是唯一数据源。以其自身风格(命名、刻度、存储格式)进行扩展。针对该系统检查偏差,而非以下面的示例为标准。
- 若仅存在实际使用的值——相同的灰色与内边距在界面中重复出现,但没有主题文件——则说明还没有系统。这些值是刻度的输入,而非权威标准:从最常用的值中派生令牌,并对齐4点网格(见第5节)。
references/audit.md - 绝不要在现有系统之外引入第二个系统。在Tamagui配置旁新建属于设计系统偏差,而非采用。
src/theme/
只有当完全没有现有系统时,以下默认规则才完全适用。
The Theme
主题
In an app without an existing system, all design tokens live under . In a project without a folder (the default template has , , and at the root), use the equivalent top-level location - typically or the existing - and keep the same file layout. Start small and split by token class as it grows:
src/theme/src/create-expo-appapp/components/constants/theme/constants/src/theme/
colors.ts # see expo-native-ui "Colors" for the palette pattern
spacing.ts
typography.ts
radius.ts
shadows.ts
motion.ts
index.ts # re-exports everything: import { spacing, type } from "@/theme"A brand-new app can begin with a single holding all of the objects below, then promote it to the folder form once any one class needs its own file (same promotion rule as components). Either way there is exactly one theme entry point - never two competing token files.
src/theme.tsRules that make a theme worth having:
- Every repeated visual value is a token. A literal that appears twice belongs in the theme.
- Components import tokens; screens import components. A screen file that imports for layout padding is fine; a screen file redefining a button color is drift.
spacing - Never hardcode hex colors, font sizes, or spacing multiples outside . One-off values that are genuinely local (an icon's 17px optical nudge) may stay inline - with a comment saying why.
src/theme/
对于没有现有系统的应用,所有设计令牌都存放在目录下。对于没有目录的项目(默认create-expo-app模板在根目录下有、和),请使用对应的顶层位置——通常是或已有的——并保持相同的文件布局。从最小规模开始,当某类令牌需要单独文件时再拆分:
src/theme/src/app/components/constants/theme/constants/src/theme/
colors.ts # 请参考expo-native-ui的「Colors」部分了解调色板模式
spacing.ts
typography.ts
radius.ts
shadows.ts
motion.ts
index.ts # 导出所有内容:import { spacing, type } from "@/theme"全新应用可以从单个包含所有对象的开始,当任何一类令牌需要单独文件时再升级为文件夹形式(与组件的升级规则相同)。无论哪种方式,都只有一个主题入口——绝不要有两个相互竞争的令牌文件。
src/theme.ts让主题具备价值的规则:
- 每一个重复的视觉值都是令牌。出现两次的字面量应放入主题中。
- 组件导入令牌;界面导入组件。界面文件导入用于布局内边距是可行的;但界面文件重新定义按钮颜色则属于偏差。
spacing - 绝不要硬编码十六进制颜色、字体大小或间距倍数到之外。真正属于本地的一次性值(如图标17px的视觉微调)可以保留在代码中——但需添加注释说明原因。
src/theme/
Colors
颜色
Build the palette from platform semantic colors: from wrapped in , centralized in . Semantic colors resolve on-device and adapt to light/dark automatically - prefer them for backgrounds, labels, and separators. ( "Colors" covers the full palette and rationale; the minimal version is:)
Colorexpo-routerPlatform.selecttheme/colors.tsexpo-native-uitsx
// theme/colors.ts
import { Platform } from "react-native";
import { Color } from "expo-router";
export const colors = {
label: Platform.select({
ios: Color.ios.label,
android: Color.android.dynamic.onSurface,
default: "#000000",
})!,
secondaryLabel: Platform.select({
ios: Color.ios.secondaryLabel,
android: Color.android.dynamic.onSurfaceVariant,
default: "#3c3c43",
})!,
separator: Platform.select({
ios: Color.ios.separator,
android: Color.android.dynamic.outlineVariant,
default: "#c6c6c8",
})!,
systemBackground: Platform.select({
ios: Color.ios.systemBackground,
android: Color.android.dynamic.surface,
default: "#ffffff",
})!,
systemBlue: Platform.select({
ios: Color.ios.systemBlue,
android: Color.android.dynamic.primary,
default: "#007aff",
})!,
// Deliberately fixed: text on a tinted (accent) surface stays white in both modes.
onTint: "#ffffff",
};Add brand colors as explicit light/dark pairs only when the brand requires values the platform doesn't provide:
tsx
// theme/colors.ts (brand additions)
import { useColorScheme } from "react-native";
const brandPalette = {
light: { accent: "#5B21B6", accentContrast: "#FFFFFF" },
dark: { accent: "#A78BFA", accentContrast: "#1E1B4B" },
} as const;
export function useBrandColors() {
const scheme = useColorScheme();
return brandPalette[scheme === "dark" ? "dark" : "light"];
}Keep the brand set tiny (accent, accentContrast, maybe a tint per feature). Everything else stays semantic.
Static-safe vs hook-only. The two patterns above have different reach - keep the boundary explicit:
- Semantic/platform colors (above) are static-safe: they resolve on-device, so plain token files like
colorscan import them at module scope.theme/typography.ts - Brand light/dark pairs are hook-only: reads the color scheme at render time, so brand colors can only be applied inside components. A static token file cannot call the hook.
useBrandColors() - Never mix the two in one file. If a static style (a ramp step, a
typeobject) needs the brand accent, either apply the brand color in the component at render time, or wrap the pair in a static dynamic color (variantson iOS) so it becomes static-safe.DynamicColorIOS
基于平台语义化颜色构建调色板:使用中的并包裹在中,集中存放在中。语义化颜色会在设备上解析,并自动适配明暗模式——优先将其用于背景、标签和分隔线。(的「Colors」部分涵盖完整调色板及原理;最简版本如下:)
expo-routerColorPlatform.selecttheme/colors.tsexpo-native-uitsx
// theme/colors.ts
import { Platform } from "react-native";
import { Color } from "expo-router";
export const colors = {
label: Platform.select({
ios: Color.ios.label,
android: Color.android.dynamic.onSurface,
default: "#000000",
})!,
secondaryLabel: Platform.select({
ios: Color.ios.secondaryLabel,
android: Color.android.dynamic.onSurfaceVariant,
default: "#3c3c43",
})!,
separator: Platform.select({
ios: Color.ios.separator,
android: Color.android.dynamic.outlineVariant,
default: "#c6c6c8",
})!,
systemBackground: Platform.select({
ios: Color.ios.systemBackground,
android: Color.android.dynamic.surface,
default: "#ffffff",
})!,
systemBlue: Platform.select({
ios: Color.ios.systemBlue,
android: Color.android.dynamic.primary,
default: "#007aff",
})!,
// 固定值:在着色(强调)表面上的文本在两种模式下均保持白色。
onTint: "#ffffff",
};仅当品牌需要平台未提供的值时,才添加显式的明暗模式品牌颜色对:
tsx
// theme/colors.ts(品牌扩展)
import { useColorScheme } from "react-native";
const brandPalette = {
light: { accent: "#5B21B6", accentContrast: "#FFFFFF" },
dark: { accent: "#A78BFA", accentContrast: "#1E1B4B" },
} as const;
export function useBrandColors() {
const scheme = useColorScheme();
return brandPalette[scheme === "dark" ? "dark" : "light"];
}保持品牌颜色集精简(强调色、强调色对比色,可为每个功能添加一个色调)。其他所有颜色均使用语义化颜色。
静态安全 vs 仅钩子。上述两种模式的适用范围不同——请明确区分边界:
- 语义化/平台颜色(上述)是静态安全的:它们在设备上解析,因此像
colors这样的纯令牌文件可以在模块作用域导入它们。theme/typography.ts - 品牌明暗颜色对是仅钩子可用的:在渲染时读取颜色模式,因此品牌颜色只能在组件内部应用。静态令牌文件无法调用该钩子。
useBrandColors() - 绝不要在一个文件中混合两种模式。如果静态样式(如刻度步骤、
type对象)需要品牌强调色,请在组件渲染时应用品牌颜色,或者将颜色对包装为静态动态颜色(iOS上的variants)使其变为静态安全。DynamicColorIOS
Spacing
间距
One scale, based on a 4-point grid. Name steps by size, not by use:
tsx
// theme/spacing.ts
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
} as const;- Use with spacing tokens for layout rhythm (
gapprefers gap over margin).expo-native-ui - Screen edge padding is unless the design says otherwise - pick one and keep it.
spacing.md - If a layout needs a value between steps, use the nearest step. The grid is the point.
- If the same in-between multiple of 4 keeps recurring (12 and 20 are common), add it to the scale as a named step instead of scattering literals. The audit whitelist must then include it too.
单一刻度,基于4点网格。按大小而非用途命名步骤:
tsx
// theme/spacing.ts
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
} as const;- 使用搭配间距令牌来保证布局节奏(
gap优先使用gap而非margin)。expo-native-ui - 界面边缘内边距默认使用,除非设计有特殊要求——选定一个值并保持统一。
spacing.md - 如果布局需要步骤之间的值,请使用最接近的步骤。网格是核心原则。
- 如果某个4的中间倍数(12和20很常见)反复出现,请将其作为命名步骤添加到刻度中,而非分散使用字面量。审计白名单也必须包含该值。
Typography
排版
Define named text styles, not raw font sizes. Mirror the platform ramp (Apple text styles) so sizes feel native:
tsx
// theme/typography.ts
import { TextStyle } from "react-native";
import { colors } from "./colors";
export const type = {
largeTitle: { fontSize: 34, fontWeight: "700", color: colors.label },
title: { fontSize: 22, fontWeight: "600", color: colors.label },
headline: { fontSize: 17, fontWeight: "600", color: colors.label },
body: { fontSize: 17, fontWeight: "400", color: colors.label },
subhead: { fontSize: 15, fontWeight: "400", color: colors.secondaryLabel },
caption: { fontSize: 12, fontWeight: "400", color: colors.secondaryLabel },
} as const satisfies Record<string, TextStyle>;If the project bundles static font files (one file per weight, loaded with or the config plugin), set weight via names instead and omit - otherwise iOS synthesizes the weight or falls back to the system font:
expo-fontfontFamilyfontWeighttsx
headline: { fontSize: 17, fontFamily: "SFProRounded-Semibold", color: colors.label },Expose them through one component so screens never touch :
fontSizetsx
// components/themed-text.tsx
import { Text, TextProps } from "react-native";
import { type } from "@/theme";
export function ThemedText({
variant = "body",
style,
...props
}: TextProps & { variant?: keyof typeof type }) {
return <Text style={[type[variant], style]} {...props} />;
}Screen titles still come from the navigation stack header ( rule), so is mostly for non-stack contexts.
expo-native-uilargeTitle定义命名文本样式,而非原始字体大小。镜像平台刻度(Apple文本样式)使尺寸更贴近原生:
tsx
// theme/typography.ts
import { TextStyle } from "react-native";
import { colors } from "./colors";
export const type = {
largeTitle: { fontSize: 34, fontWeight: "700", color: colors.label },
title: { fontSize: 22, fontWeight: "600", color: colors.label },
headline: { fontSize: 17, fontWeight: "600", color: colors.label },
body: { fontSize: 17, fontWeight: "400", color: colors.label },
subhead: { fontSize: 15, fontWeight: "400", color: colors.secondaryLabel },
caption: { fontSize: 12, fontWeight: "400", color: colors.secondaryLabel },
} as const satisfies Record<string, TextStyle>;如果项目打包了静态字体文件(每个字重一个文件,通过或配置插件加载),请通过名称设置字重,而非——否则iOS会合成字重或回退到系统字体:
expo-fontfontFamilyfontWeighttsx
headline: { fontSize: 17, fontFamily: "SFProRounded-Semibold", color: colors.label },通过一个组件暴露这些样式,使界面永远无需直接操作:
fontSizetsx
// components/themed-text.tsx
import { Text, TextProps } from "react-native";
import { type } from "@/theme";
export function ThemedText({
variant = "body",
style,
...props
}: TextProps & { variant?: keyof typeof type }) {
return <Text style={[type[variant], style]} {...props} />;
}界面标题仍来自导航栈头部(规则),因此主要用于非栈上下文。
expo-native-uilargeTitleRadius
圆角
tsx
// theme/radius.ts
export const radius = {
sm: 8,
md: 12,
lg: 16,
full: 9999, // capsules
} as const;Pair every non-capsule radius with (per ).
borderCurve: "continuous"expo-native-uitsx
// theme/radius.ts
export const radius = {
sm: 8,
md: 12,
lg: 16,
full: 9999, // 胶囊状
} as const;每个非胶囊状圆角都要搭配(遵循规则)。
borderCurve: "continuous"expo-native-uiShadows
阴影
Shadows are strings (never legacy shadow/elevation props - see ). Two or three elevation levels are enough:
boxShadowexpo-native-uitsx
// theme/shadows.ts
export const shadows = {
card: "0 1px 2px rgba(0, 0, 0, 0.05)",
raised: "0 4px 12px rgba(0, 0, 0, 0.10)",
overlay: "0 8px 24px rgba(0, 0, 0, 0.18)",
} as const;阴影使用字符串(绝不要使用旧版shadow/elevation属性——见)。2到3个层级足够:
boxShadowexpo-native-uitsx
// theme/shadows.ts
export const shadows = {
card: "0 1px 2px rgba(0, 0, 0, 0.05)",
raised: "0 4px 12px rgba(0, 0, 0, 0.10)",
overlay: "0 8px 24px rgba(0, 0, 0, 0.18)",
} as const;Motion
动效
Durations and shared spring/easing configs, so animations across the app feel related:
tsx
// theme/motion.ts
export const motion = {
fast: 150, // state feedback: press, toggle
base: 250, // element transitions: enter/exit
slow: 400, // large surfaces: sheets, screens
} as const;Reanimated caveat: don't pass / token values into Reanimated styles - use static colors there (see ).
ColorPlatformColorexpo-native-ui时长与共享的弹簧/缓动配置,使应用中的动画保持风格一致:
tsx
// theme/motion.ts
export const motion = {
fast: 150, // 状态反馈:按压、切换
base: 250, // 元素过渡:进入/退出
slow: 400, // 大型表面:弹窗、界面
} as const;Reanimated注意事项:不要将/令牌值传入Reanimated样式——请在其中使用静态颜色(见)。
ColorPlatformColorexpo-native-uiReusable Components
可复用组件
The theme controls values; components control structure. Shared primitives live in (see ).
src/components/expo-project-structure主题控制取值;组件控制结构。共享原语存放在目录下(见)。
src/components/expo-project-structureThe component contract
组件契约
Every design-system primitive defines, explicitly:
- Variants - visual intent: ,
primary,secondary,ghost. Add a variant only when a real screen needs it.destructive - Sizes - ,
sm,md. Defaultlg. Sizes map to spacing/typography tokens, never to fresh numbers.md - States - default, pressed (not hover - this is touch), disabled, loading. Handle pressed with a style function; never leave a tappable element without pressed feedback.
Pressable - Style override - accept a prop and merge it last, so callers can adjust layout (margins, flex) without forking the component. Callers may override layout, not identity - a caller changing a button's colors is a signal the variant set is missing something.
style
tsx
// components/button.tsx
import { Pressable, ActivityIndicator, ViewStyle, StyleProp } from "react-native";
import { colors, spacing, radius } from "@/theme";
import { ThemedText } from "./themed-text";
const variants = {
primary: { backgroundColor: colors.systemBlue, color: colors.onTint },
secondary: { backgroundColor: colors.separator, color: colors.label },
} as const;
const sizes = {
sm: { paddingVertical: spacing.xs, paddingHorizontal: spacing.sm },
md: { paddingVertical: spacing.sm, paddingHorizontal: spacing.md },
} as const;
export function Button({
variant = "primary",
size = "md",
title,
loading,
disabled,
style,
onPress,
}: {
variant?: keyof typeof variants;
size?: keyof typeof sizes;
title: string;
loading?: boolean;
disabled?: boolean;
style?: StyleProp<ViewStyle>;
onPress?: () => void;
}) {
return (
<Pressable
accessibilityRole="button"
disabled={disabled || loading}
onPress={onPress}
style={({ pressed }) => [
{
backgroundColor: variants[variant].backgroundColor,
borderRadius: radius.md,
borderCurve: "continuous",
alignItems: "center",
opacity: disabled ? 0.4 : pressed ? 0.7 : 1,
...sizes[size],
},
style, // caller overrides merge last
]}
>
{loading ? (
<ActivityIndicator color={variants[variant].color as string} />
) : (
<ThemedText variant="headline" style={{ color: variants[variant].color }}>
{title}
</ThemedText>
)}
</Pressable>
);
}每个设计系统原语都明确定义:
- 变体 - 视觉意图:、
primary、secondary、ghost。仅当真实界面需要时才添加变体。destructive - 尺寸 - 、
sm、md。默认lg。尺寸映射到间距/排版令牌,而非新的数值。md - 状态 - 默认、按压(不是悬停——这是触摸操作)、禁用、加载。使用样式函数处理按压状态;绝不要让可点击元素没有按压反馈。
Pressable - 样式覆盖 - 接受属性并最后合并,使调用者可以调整布局(外边距、flex)而无需分叉组件。调用者可覆盖布局,但不能修改组件本质——如果调用者修改按钮颜色,说明变体集缺失了某些内容。
style
tsx
// components/button.tsx
import { Pressable, ActivityIndicator, ViewStyle, StyleProp } from "react-native";
import { colors, spacing, radius } from "@/theme";
import { ThemedText } from "./themed-text";
const variants = {
primary: { backgroundColor: colors.systemBlue, color: colors.onTint },
secondary: { backgroundColor: colors.separator, color: colors.label },
} as const;
const sizes = {
sm: { paddingVertical: spacing.xs, paddingHorizontal: spacing.sm },
md: { paddingVertical: spacing.sm, paddingHorizontal: spacing.md },
} as const;
export function Button({
variant = "primary",
size = "md",
title,
loading,
disabled,
style,
onPress,
}: {
variant?: keyof typeof variants;
size?: keyof typeof sizes;
title: string;
loading?: boolean;
disabled?: boolean;
style?: StyleProp<ViewStyle>;
onPress?: () => void;
}) {
return (
<Pressable
accessibilityRole="button"
disabled={disabled || loading}
onPress={onPress}
style={({ pressed }) => [
{
backgroundColor: variants[variant].backgroundColor,
borderRadius: radius.md,
borderCurve: "continuous",
alignItems: "center",
opacity: disabled ? 0.4 : pressed ? 0.7 : 1,
...sizes[size],
},
style, // 调用者的覆盖样式最后合并
]}
>
{loading ? (
<ActivityIndicator color={variants[variant].color as string} />
) : (
<ThemedText variant="headline" style={{ color: variants[variant].color }}>
{title}
</ThemedText>
)}
</Pressable>
);
}Composition over configuration
组合优先于配置
When a component's props start describing content (, , , ), stop adding props and accept instead. A that renders with token padding outlives any with twelve content props. Reserve props for the contract above: variant, size, state, style.
leftIconsubtitlefooterTextbadgeCountchildrenCardchildrenCard当组件的属性开始描述内容(、、、)时,停止添加属性并改为接受。一个使用令牌内边距渲染的组件,比带有12个内容属性的组件更耐用。仅为上述契约保留属性:变体、尺寸、状态、样式。
leftIconsubtitlefooterTextbadgeCountchildrenchildrenCardCardWhen to extract - and when not to
何时提取——何时不提取
Promote a view into when all of these hold:
src/components/- It appears (or is about to appear) in two or more screens. Until then it stays colocated in (see
screens/<name>/).expo-project-structure - It has a nameable role ("Card", "EmptyState", "Badge") - not "the thing on the profile screen".
- Its API is smaller than its implementation. If the props would just re-expose every internal style, it isn't a reusable component yet - it's a screen fragment.
Promotion path: inline JSX → component in → . Move one step at a time, when the trigger fires - never speculatively. Wrong abstractions cost more than duplication; a second copy of a view is cheaper than a primitive with a bad API.
screens/<name>/src/components/Do not wrap platform components that already carry the design language (, , stack headers, views) just to route them through the system. Native styling is the design system for those.
SwitchDateTimePicker@expo/ui当所有以下条件满足时,将视图升级到目录:
src/components/- 它已经(或即将)出现在两个或更多界面中。在此之前,它应存放在目录下(见
screens/<name>/)。expo-project-structure - 它有一个可命名的角色(如"Card"、"EmptyState"、"Badge")——而非"个人资料界面上的那个东西"。
- 它的API比实现更简洁。如果属性只是重新暴露所有内部样式,那它还不是可复用组件——只是界面片段。
升级路径:内联JSX → 目录下的组件 → 目录。每次只走一步,触发条件满足时再升级——不要提前预判。错误的抽象比重复代码成本更高;视图的第二个副本比API糟糕的原语更便宜。
screens/<name>/src/components/不要包装已经带有设计语言的平台组件(、、栈头部、视图),只是为了将它们纳入系统。原生样式本身就是这些组件的设计系统。
SwitchDateTimePicker@expo/uiWhere Decisions Live
决策归属
| Decision | Lives in | Example |
|---|---|---|
| A visual value used anywhere twice | | brand accent, spacing step |
| Structure + variants of a reused element | | Button, Card, EmptyState |
| One screen's private composition | | profile header layout |
| One-off local adjustment | inline, with a comment | optical nudge on an icon |
| Screen titles, top-level chrome | navigation stack options | header title, large title |
| 决策 | 归属位置 | 示例 |
|---|---|---|
| 任何地方使用两次的视觉值 | | 品牌强调色、间距步骤 |
| 可复用元素的结构与变体 | | Button、Card、EmptyState |
| 单个界面的私有组合 | | 个人资料头部布局 |
| 一次性本地调整 | 内联代码,带注释 | 图标的视觉微调 |
| 界面标题、顶层导航栏 | 导航栈选项 | 头部标题、大标题 |
Self-Critique Pass
自我审查步骤
After building or changing a screen, screenshot it and check it against these principles (from Expo's design-principles guide). Each one maps to a system fix, not a local tweak:
- Hierarchy / contrast - is the most important element obviously first? Fix with ramp steps, not ad-hoc font sizes.
type - Proximity / white space - do related items sit closer than unrelated ones? Fix with + spacing tokens.
gap - Repetition / unity - do all corners, shadows, and accents match? If not, a value escaped the theme - move it in.
- Alignment - do edges share axes? Fix with consistent screen edge padding.
The pass is complete only when all four checks pass, or every failing value has moved into the theme or a component. If a screen fails the same check twice, the fix belongs in the theme or a component - not in the screen.
构建或修改界面后,截图并对照以下原则检查(来自Expo设计原则指南)。每个原则都对应系统层面的修复,而非本地调整:
- 层级/对比度 - 最重要的元素是否明显突出?使用刻度步骤修复,而非临时调整字体大小。
type - 邻近/留白 - 相关元素是否比无关元素更靠近?使用+间距令牌修复。
gap - 重复/统一性 - 所有圆角、阴影和强调色是否一致?如果不一致,说明某个值脱离了主题——将其移入主题。
- 对齐 - 边缘是否共享轴线?使用统一的界面边缘内边距修复。
只有当所有四项检查通过,或每个不符合的值都已移入主题或组件时,审查才算完成。如果界面两次未通过同一项检查,修复应在主题或组件中进行——而非在界面中。
Auditing an Existing App
检查现有应用
To measure drift in an app that already has screens - hardcoded hex values, arbitrary spacing, inconsistent component APIs - follow . It contains grep-based checks, a scoring rubric, an incremental adoption order for fixing a drifted app, and templates for documenting existing components and proposing new ones.
./references/audit.md要检测已有界面的应用是否存在偏差——硬编码十六进制值、任意间距、不一致的组件API——请遵循。其中包含基于grep的检查、评分标准、修复偏差应用的渐进式采用顺序,以及用于文档化现有组件和提议新组件的模板。
./references/audit.mdSubmitting Feedback
提交反馈
If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:
bash
npx --yes submit-expo-feedback@latest --category skills --subject "expo-design-system" "<actionable feedback>"Only submit when you have something specific and actionable to report. Include as much relevant context as possible.
If an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above.
如果您在使用本技能时遇到错误、误导性或过时信息,请报告给Expo以帮助改进:
bash
npx --yes submit-expo-feedback@latest --category skills --subject "expo-design-system" "<可操作的反馈>"仅当您有具体且可操作的内容时才提交反馈。请尽可能包含相关上下文。
如果AI代理反复失败或用户不得不接管Expo任务,请加载expo-skill-feedback技能并遵循其评估流程,而非重复使用上述命令。