Noções Básicas de Design Systems
10 exemplos para você começar com Design Systems - 7 básicos e 3 intermediários.
Busque em todas as páginas da documentação
10 exemplos para você começar com Design Systems - 7 básicos e 3 intermediários.
Todos os exemplos abaixo usam primitivas nativas do React Native e um pequeno módulo de tokens - nenhuma biblioteca de design de terceiros é necessária. Crie um aplicativo Expo SDK 57 TypeScript padrão e substitua App.tsx para executar cada trecho.
npx create-expo-app@latest MyDesignApp --template blank-typescript
cd MyDesignApp
npx expo startFerramentas: Estes exemplos visam Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3. Tokens combinam naturalmente com Noções Básicas de Estilização e Modo Escuro e Esquemas de Cores.
Substitua valores hex espalhados por um único objeto de paleta. Componentes referenciam funções, não literais.
// theme/colors.ts
export const colors = {
brand: {
primary: "#2563eb",
primaryMuted: "#93c5fd",
onPrimary: "#ffffff",
},
neutral: {
50: "#f8fafc",
100: "#f1f5f9",
700: "#334155",
900: "#0f172a",
},
feedback: {
success: "#16a34a",
danger: "#dc2626",
},
} as const;import { Pressable, StyleSheet, Text, View } from "react-native";
import { colors } from "./theme/colors";
export default function App() {
return (
<View style={styles.screen}>
<Pressable style={styles.cta}>
<Text style={styles.ctaLabel}>Continue</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: "center", padding: 24, backgroundColor: colors.neutral[50] },
cta: { backgroundColor: colors.brand.primary, padding: 16, borderRadius: 10, alignItems: "center" },
ctaLabel: { color: colors.brand.onPrimary, fontWeight: "700", fontSize: 16 },
});as const restringe tipos para que erros de digitação falhem em tempo de compilaçãoonPrimary) e não por valor (white)#2563eb diretamentetailwind.config ou temaRelacionado: Noções Básicas de Estilização - StyleSheet vs inline e promoção de tokens
Uma escala de espaçamento remove números mágicos. A maioria dos design systems mobile usa múltiplos de 4.
// theme/spacing.ts
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
} as const;import { StyleSheet, Text, View } from "react-native";
import { spacing } from "./theme/spacing";
export default function App() {
return (
<View style={styles.card}>
<Text style={styles.title}>Order summary</Text>
<Text style={styles.body}>3 items · arrives tomorrow</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
margin: spacing.md,
padding: spacing.lg,
gap: spacing.sm,
borderRadius: 12,
backgroundColor: "#fff",
},
title: { fontSize: 18, fontWeight: "600" },
body: { fontSize: 15, lineHeight: 22 },
});gap em contêineres flex usa a mesma escala que padding e marginmd = 1613, 17) a menos que esteja corrigindo o alinhamento óptico em um componente específicoRelacionado: Mergulho Profundo em Flexbox -
gapvs margin no primeiro/último filho
Mapeie funções (title, body, label) para pares de fontSize + lineHeight. Telas referenciam funções, não números brutos.
// theme/typography.ts
export const typography = {
display: { fontSize: 32, lineHeight: 40, fontWeight: "700" as const },
title: { fontSize: 20, lineHeight: 28, fontWeight: "600" as const },
body: { fontSize: 16, lineHeight: 24, fontWeight: "400" as const },
label: { fontSize: 13, lineHeight: 18, fontWeight: "500" as const },
} as const;import { StyleSheet, Text, View } from "react-native";
import { typography } from "./theme/typography";
export default function App() {
return (
<View style={styles.screen}>
<Text style={styles.display}>Welcome back</Text>
<Text style={styles.body}>Your saved items are ready to checkout.</Text>
<Text style={styles.label}>LAST UPDATED · 2 MIN AGO</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 24, gap: 8 },
display: typography.display,
body: typography.body,
label: { ...typography.label, letterSpacing: 0.6, color: "#64748b" },
});fontSize com um lineHeight explícito - alturas de linha padrão cortam descendentes no AndroidmaxFontSizeMultiplier - veja Tipo Dinâmico e Escala de FontesRelacionado: Escala de Tipografia e Carregamento de Fontes - expo-font e texto acessível
Tokens semânticos desacoplam componentes de valores de paleta. surface significa "fundo do cartão" em esquemas claro e escuro.
// theme/semantic.ts
export const light = {
background: "#f8fafc",
surface: "#ffffff",
textPrimary: "#0f172a",
textSecondary: "#64748b",
border: "#e2e8f0",
accent: "#2563eb",
} as const;
export const dark = {
background: "#020617",
surface: "#0f172a",
textPrimary: "#f8fafc",
textSecondary: "#94a3b8",
border: "#334155",
accent: "#60a5fa",
} as const;import { StyleSheet, Text, useColorScheme, View } from "react-native";
import { dark, light } from "./theme/semantic";
export default function App() {
const scheme = useColorScheme() ?? "light";
const theme = scheme === "dark" ? dark : light;
return (
<View style={[styles.screen, { backgroundColor: theme.background }]}>
<View style={[styles.card, { backgroundColor: theme.surface, borderColor: theme.border }]}>
<Text style={{ color: theme.textPrimary, fontSize: 17, fontWeight: "600" }}>Inbox</Text>
<Text style={{ color: theme.textSecondary, fontSize: 15 }}>12 unread messages</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16 },
card: { padding: 16, borderRadius: 12, borderWidth: 1, gap: 4 },
});theme.textPrimary, não dark.textPrimary - a seleção ocorre uma vez na tela ou no provedorStyleSheet.create; cores se aplicam na renderização quando seguem o esquemaRelacionado: Modo Escuro e Esquemas de Cores -
useColorSchemeeAppearance.setColorScheme
Raio de canto e sombra/elevação pertencem à camada de tokens - cartões, chips e modais compartilham os mesmos valores.
// theme/shape.ts
import { Platform } from "react-native";
export const radius = {
sm: 6,
md: 12,
lg: 20,
full: 999,
} as const;
export const elevation = {
card: Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
},
android: { elevation: 3 },
default: {},
}),
modal: Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.15,
shadowRadius: 24,
},
android: { elevation: 8 },
default: {},
}),
} as const;import { StyleSheet, Text, View } from "react-native";
import { elevation, radius } from "./theme/shape";
export default function App() {
return (
<View style={styles.screen}>
<View style={styles.card}>
<Text style={styles.title}>Delivery ETA</Text>
<Text style={styles.body}>Arrives by 6:30 PM</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: "center", padding: 24, backgroundColor: "#f1f5f9" },
card: {
backgroundColor: "#fff",
borderRadius: radius.md,
padding: 20,
gap: 6,
...elevation.card,
},
title: { fontSize: 17, fontWeight: "600" },
body: { fontSize: 15, color: "#475569" },
});elevation - Platform.select em tokens mantém os componentes limposbackgroundColor opaco - cartões transparentes produzem profundidade invisívelRelacionado: Sombras, Elevação e Bordas - profundidade multiplataforma
Centralize a seleção de tema em um contexto React para que componentes aninhados evitem prop drilling.
// theme/ThemeProvider.tsx
import { createContext, useContext, useMemo } from "react";
import { useColorScheme } from "react-native";
import { dark, light } from "./semantic";
type Theme = typeof light;
const ThemeContext = createContext<Theme>(light);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const scheme = useColorScheme() ?? "light";
const theme = useMemo(() => (scheme === "dark" ? dark : light), [scheme]);
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
return useContext(ThemeContext);
}// App.tsx
import { StyleSheet, Text, View } from "react-native";
import { ThemeProvider, useTheme } from "./theme/ThemeProvider";
function ProfileCard() {
const theme = useTheme();
return (
<View style={[styles.card, { backgroundColor: theme.surface, borderColor: theme.border }]}>
<Text style={{ color: theme.textPrimary, fontWeight: "600" }}>Alex Kim</Text>
<Text style={{ color: theme.textSecondary }}>Pro member</Text>
</View>
);
}
export default function App() {
return (
<ThemeProvider>
<View style={styles.screen}>
<ProfileCard />
</View>
</ThemeProvider>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16 },
card: { padding: 16, borderRadius: 12, borderWidth: 1, gap: 4 },
});SafeAreaProvider) - um provedor para toda a árvoreuseMemo no objeto de paleta evita que consumidores re-renderizem quando a referência é estável entre esquemasRelacionado: Tematização e Sabores de Marca - multi-marca sem forks
Componentes de Design System expõem uma API pequena e tipada - variant, size, disabled - em vez de substituições de estilo por tela.
// components/Button.tsx
import { Pressable, StyleSheet, Text, type PressableProps } from "react-native";
import { spacing } from "../theme/spacing";
type Variant = "primary" | "secondary" | "ghost";
type Size = "sm" | "md" | "lg";
type Props = PressableProps & {
label: string;
variant?: Variant;
size?: Size;
};
const variantStyles: Record<Variant, { container: object; label: object }> = {
primary: { container: { backgroundColor: "#2563eb" }, label: { color: "#fff" } },
secondary: { container: { backgroundColor: "#e2e8f0" }, label: { color: "#0f172a" } },
ghost: { container: { backgroundColor: "transparent" }, label: { color: "#2563eb" } },
};
const sizeStyles: Record<Size, { container: object; label: object }> = {
sm: { container: { paddingVertical: spacing.xs, paddingHorizontal: spacing.sm }, label: { fontSize: 14 } },
md: { container: { paddingVertical: spacing.sm, paddingHorizontal: spacing.md }, label: { fontSize: 16 } },
lg: { container: { paddingVertical: spacing.md, paddingHorizontal: spacing.lg }, label: { fontSize: 17 } },
};
export function Button({ label, variant = "primary", size = "md", disabled, style, ...rest }: Props) {
return (
<Pressable
accessibilityRole="button"
disabled={disabled}
style={[
styles.base,
variantStyles[variant].container,
sizeStyles[size].container,
disabled && styles.disabled,
style,
]}
{...rest}
>
<Text style={[styles.label, variantStyles[variant].label, sizeStyles[size].label]}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: { borderRadius: 10, alignItems: "center" },
label: { fontWeight: "600" },
disabled: { opacity: 0.45 },
});import { View } from "react-native";
import { Button } from "./components/Button";
export default function App() {
return (
<View style={{ flex: 1, justifyContent: "center", gap: 12, padding: 24 }}>
<Button label="Continue" variant="primary" size="lg" />
<Button label="Back" variant="ghost" size="md" />
<Button label="Delete" variant="secondary" size="sm" disabled />
</View>
);
}accessibilityRole="button" é obrigatório em pressables personalizados - veja accessibilityLabel & accessibilityRolestyle por último no array para que os chamadores possam substituir - documente se as substituições são suportadasRelacionado: accessibilityLabel & accessibilityRole - semântica em controles personalizados
Uma primitiva Box (ou Stack) de baixo nível mapeia props de token para arrays de estilo - componentes superiores a compõem em vez de View bruto.
// components/Box.tsx
import { View, type ViewProps } from "react-native";
import { spacing, type spacing as SpacingScale } from "../theme/spacing";
type SpacingKey = keyof typeof SpacingScale;
type Props = ViewProps & {
p?: SpacingKey;
px?: SpacingKey;
gap?: SpacingKey;
bg?: string;
};
export function Box({ p, px, gap, bg, style, ...rest }: Props) {
return (
<View
style={[
p != null && { padding: spacing[p] },
px != null && { paddingHorizontal: spacing[px] },
gap != null && { gap: spacing[gap] },
bg != null && { backgroundColor: bg },
style,
]}
{...rest}
/>
);
}import { Text } from "react-native";
import { Box } from "./components/Box";
export default function App() {
return (
<Box p="lg" gap="sm" bg="#f8fafc" style={{ flex: 1 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>Notifications</Text>
<Box px="md" p="md" bg="#fff" style={{ borderRadius: 12 }}>
<Text>Your order shipped.</Text>
</Box>
</Box>
);
}p, px, gap) espelham atalhos de ferramentas de design que os designers já usamRelacionado: Tamagui - props de token otimizadas em escala
Centralize a estilização de texto para que as telas passem tone="muted" em vez de duplicar fontSize e color.
// components/Text.tsx
import { Text as RNText, type TextProps } from "react-native";
import { typography } from "../theme/typography";
import { useTheme } from "../theme/ThemeProvider";
type Role = "display" | "title" | "body" | "label";
type Tone = "default" | "muted" | "accent" | "danger";
type Props = TextProps & { role?: Role; tone?: Tone };
export function Text({ role = "body", tone = "default", style, ...rest }: Props) {
const theme = useTheme();
const toneColor = {
default: theme.textPrimary,
muted: theme.textSecondary,
accent: theme.accent,
danger: "#dc2626",
}[tone];
return <RNText style={[typography[role], { color: toneColor }, style]} {...rest} />;
}import { ThemeProvider } from "./theme/ThemeProvider";
import { Text } from "./components/Text";
import { View } from "react-native";
export default function App() {
return (
<ThemeProvider>
<View style={{ flex: 1, padding: 24, gap: 8 }}>
<Text role="title">Payment method</Text>
<Text tone="muted">Visa ending in 4242</Text>
<Text role="label" tone="accent">
CHANGE
</Text>
</View>
</ThemeProvider>
);
}...rest para que accessibilityLabel e maxFontSizeMultiplier passem para o Text nativotone="danger" deve eventualmente mapear para theme.feedback.danger - mantenha as cores de feedback em tokensRelacionado: Tipo Dinâmico e Escala de Fontes -
maxFontSizeMultiplierem rótulos
Quando o design adiciona uma nova cor de marca, estenda o objeto de token e mantenha as chaves antigas - semver para tokens espelha semver para pacotes npm.
// theme/colors.v2.ts - additive change
import { colors as v1 } from "./colors";
export const colors = {
...v1,
brand: {
...v1.brand,
secondary: "#7c3aed", // new token - old screens unaffected
},
} as const;
/** @deprecated Use colors.brand.primary - removed in v2.0 */
export const legacyPrimary = v1.brand.primary;// components/Badge.tsx - optional new variant, default unchanged
type Variant = "neutral" | "brand" | "brandSecondary";
const bg: Record<Variant, string> = {
neutral: "#e2e8f0",
brand: "#2563eb",
brandSecondary: "#7c3aed",
};
export function Badge({ label, variant = "neutral" }: { label: string; variant?: Variant }) {
return (
<View style={{ backgroundColor: bg[variant], paddingHorizontal: 10, paddingVertical: 4, borderRadius: 999 }}>
<Text style={{ color: variant === "neutral" ? "#0f172a" : "#fff", fontSize: 12, fontWeight: "600" }}>
{label}
</Text>
</View>
);
}Relacionado: Construindo uma Biblioteca de Componentes Interna - semver e Storybook para pacotes de UI
Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026