Modo Oscuro y Esquemas de Color
useColorScheme, paletas dinámicas y sincronización del sistema.
Busca en todas las páginas de la documentación
useColorScheme, paletas dinámicas y sincronización del sistema.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import { useColorScheme, StyleSheet, Text, View } from "react-native";
const colors = {
light: { background: "#ffffff", text: "#0f172a", muted: "#64748b", border: "#e2e8f0" },
dark: { background: "#0f172a", text: "#f8fafc", muted: "#94a3b8", border: "#334155" },
} as const;
export function ThemedCard({ title, body }: { title: string; body: string }) {
const scheme = useColorScheme() ?? "light";
const theme = colors[scheme];
return (
<View style={[styles.card, { backgroundColor: theme.background, borderColor: theme.border }]}>
<Text style={[styles.title, { color: theme.text }]}>{title}</Text>
<Text style={[styles.body, { color: theme.muted }]}>{body}</Text>
</View>
);
}
const styles = StyleSheet.create({
card: { borderRadius: 12, borderWidth: 1, padding: 16, gap: 6 },
title: { fontSize: 17, fontWeight: "600" },
body: { fontSize: 15, lineHeight: 22 },
});Cuándo usarlo: Cualquier pantalla que deba respetar la configuración de luz/oscuridad del sistema, u ofrecer una anulación en la aplicación, sin mantener dos árboles de componentes separados.
import { useCallback, useMemo, useState } from "react";
import {
Appearance,
Pressable,
StyleSheet,
Text,
useColorScheme,
View,
} from "react-native";
import { StatusBar } from "expo-status-bar";
import { SafeAreaView } from "react-native-safe-area-context";
type Scheme = "light" | "dark";
type Preference = "system" | Scheme;
const palette = {
light: {
background: "#f8fafc",
surface: "#ffffff",
text: "#0f172a",
subtext: "#475569",
accent: "#2563eb",
border: "#cbd5e1",
},
dark: {
background: "#020617",
surface: "#0f172a",
text: "#f8fafc",
subtext: "#94a3b8",
accent: "#60a5fa",
border: "#334155",
},
} as const;
function useAppColorScheme(): Scheme {
const system = useColorScheme();
return system === "dark" ? "dark" : "light";
}
export default function SettingsAppearanceScreen() {
const scheme = useAppColorScheme();
const theme = palette[scheme];
const [preference, setPreference] = useState<Preference>("system");
const applyPreference = useCallback((next: Preference) => {
setPreference(next);
if (next === "system") {
Appearance.setColorScheme(null); // seguir el SO nuevamente
} else {
Appearance.setColorScheme(next);
}
}, []);
const styles = useMemo(() => makeStyles(theme), [theme]);
return (
<SafeAreaView style={styles.safe} edges={["top", "left", "right"]}>
<StatusBar style={scheme === "dark" ? "light" : "dark"} />
<Text style={styles.heading}>Apariencia</Text>
<Text style={styles.subheading}>
Sistema es {scheme}. Preferencia: {preference}.
</Text>
<View style={styles.card}>
{(["system", "light", "dark"] as const).map((option) => (
<Pressable
key={option}
onPress={() => applyPreference(option)}
style={[styles.row, preference === option && styles.rowActive]}
>
<Text style={styles.rowLabel}>{option}</Text>
{preference === option && <Text style={styles.check}>✓</Text>}
</Pressable>
))}
</View>
<View style={[styles.preview, { backgroundColor: theme.surface }]}>
<Text style={styles.previewTitle}>Tarjeta de vista previa</Text>
<Text style={styles.previewBody}>Los tokens semánticos se intercambian con el esquema activo.</Text>
</View>
</SafeAreaView>
);
}
function makeStyles(theme: (typeof palette)[Scheme]) {
return StyleSheet.create({
safe: { flex: 1, backgroundColor: theme.background, padding: 16, gap: 12 },
heading: { fontSize: 28, fontWeight: "700", color: theme.text },
subheading: { fontSize: 15, color: theme.subtext },
card: {
backgroundColor: theme.surface,
borderRadius: 12,
borderWidth: StyleSheet.hairlineWidth,
borderColor: theme.border,
overflow: "hidden",
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: theme.border,
},
rowActive: { backgroundColor: theme.background },
rowLabel: { fontSize: 16, color: theme.text, textTransform: "capitalize" },
check: { color: theme.accent, fontWeight: "700" },
preview: {
borderRadius: 12,
padding: 16,
gap: 6,
borderWidth: StyleSheet.hairlineWidth,
borderColor: theme.border,
},
previewTitle: { fontSize: 17, fontWeight: "600", color: theme.text },
previewBody: { fontSize: 15, lineHeight: 22, color: theme.subtext },
});
}Lo que esto demuestra:
useColorScheme() leyendo el esquema del sistema activo ("light" | "dark" | null).background, surface, text) en lugar de hex codificado en componentes.Appearance.setColorScheme() para un conmutador de preferencia claro/oscuro/sistema en la aplicación.StatusBar desde expo-status-bar cambiando style con el esquema activo.useMemo para reconstruir StyleSheet cuando cambia el tema.useColorScheme es un hook React de react-native que se suscribe a la configuración de apariencia del SO. Devuelve "light", "dark" o null (desconocido / no disponible) y se re-renderiza cuando el usuario alterna el modo oscuro del sistema.Appearance API - Appearance.getColorScheme() para lecturas únicas; Appearance.addChangeListener para suscripciones manuales; Appearance.setColorScheme("dark" | "light" | null) para anular la aplicación independientemente del sistema (null restaura la sincronización del sistema).textPrimary, borderSubtle) no por valor (gray500). Los componentes hacen referencia a funciones; la paleta asigna funciones a hex por esquema.userInterfaceStyle - En app.json / app.config.js, "userInterfaceStyle": "automatic" sigue el SO (predeterminado). "light" o "dark" bloquea la aplicación independientemente de la configuración del sistema.| API | Tipo | Usar para |
|---|---|---|
useColorScheme() | Hook - desencadena re-renderizado | Estilos de componentes, JSX temático |
Appearance.getColorScheme() | Instantánea | Comprobaciones únicas a nivel de módulo (raro) |
Appearance.addChangeListener | Suscripción de eventos | Rutas de código no React (prefiere hook en componentes) |
Appearance.setColorScheme() | Anular | Selector de tema en la aplicación (sistema / light / dark) |
import { Appearance, useColorScheme } from "react-native";
// En un componente - reactivo
function Banner() {
const scheme = useColorScheme() ?? "light";
return <View style={{ backgroundColor: scheme === "dark" ? "#1e293b" : "#fff" }} />;
}
// Anular para alternancia en la aplicación
Appearance.setColorScheme("dark"); // forzar oscuro
Appearance.setColorScheme(null); // seguir el sistema nuevamentePaleta plana (aplicaciones pequeñas)
const colors = {
light: { bg: "#fff", text: "#111", accent: "#2563eb" },
dark: { bg: "#111", text: "#f8fafc", accent: "#60a5fa" },
};Funciones semánticas (recomendado)
const tokens = {
light: {
backgroundPrimary: "#ffffff",
textPrimary: "#0f172a",
borderDefault: "#e2e8f0",
},
dark: {
backgroundPrimary: "#0f172a",
textPrimary: "#f8fafc",
borderDefault: "#334155",
},
};Proveedor de contexto (acceso compartido)
import { createContext, useContext, useMemo } from "react";
import { useColorScheme } from "react-native";
type Theme = (typeof palette)["light"];
const ThemeContext = createContext<Theme>(palette.light);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const scheme = useColorScheme() ?? "light";
const theme = useMemo(() => palette[scheme], [scheme]);
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
return useContext(ThemeContext);
}{
"expo": {
"userInterfaceStyle": "automatic",
"ios": { "userInterfaceStyle": "automatic" },
"android": { "userInterfaceStyle": "automatic" }
}
}"automatic" - seguir el SO (predeterminado recomendado)."light" / "dark" - bloquear la aplicación; useColorScheme aún reporta el valor forzado.import { ColorSchemeName, useColorScheme } from "react-native";
type Scheme = "light" | "dark";
function resolveScheme(name: ColorSchemeName): Scheme {
return name === "dark" ? "dark" : "light";
}
function useResolvedScheme(): Scheme {
return resolveScheme(useColorScheme());
}
// Paleta tipada - exhaustividad verificada
const palette: Record<Scheme, { background: string; text: string }> = {
light: { background: "#fff", text: "#111" },
dark: { background: "#111", text: "#fff" },
};ColorSchemeName es "light" | "dark" | null | undefined.null - por defecto a "light" o la alternativa de tu aplicación.Colores codificados en StyleSheet.create a nivel de módulo - Los estilos se congelan en el tiempo de carga; no reaccionan a cambios de esquema. Corrección: Construye estilos dentro del componente con useMemo, o aplica colores dinámicos en línea: style={[styles.card, { backgroundColor: theme.surface }]}.
Ignorar null desde useColorScheme - En algunos caminos el hook devuelve null. Corrección: const scheme = useColorScheme() ?? "light".
Barra de estado clara en un fondo claro - Los iconos de la barra de estado blanco sobre blanco desaparecen. Corrección: StatusBar style="dark" en modo claro, style="light" en modo oscuro.
Olvidar imágenes e iconos - Los logotipos e ilustraciones diseñados para fondos blancos se ven mal en superficies oscuras. Corrección: Proporciona variantes oscuras @2x, usa tintColor, o envuelve en un contenedor temático que se mantenga claro en ambos esquemas.
Appearance.setColorScheme sin persistir preferencia - La opción del usuario se reinicia al reiniciar. Corrección: Almacena la preferencia en AsyncStorage / MMKV y aplica al iniciar la aplicación antes de la primera renderización.
userInterfaceStyle: "light" bloqueado en app.json mientras se construye interfaz oscura - La configuración contradice tu trabajo de tema. Corrección: Usa "automatic" a menos que intencionalmente envíes una aplicación de esquema único.
Superposiciones semi-transparentes - rgba(0,0,0,0.5) funciona en luz; en fondos oscuros la misma superposición puede verse turbia. Corrección: Define tokens de superposición separados por esquema.
| Alternativa | Usar Cuándo | No Usar Cuándo |
|---|---|---|
useColorScheme + objeto de token | Predeterminado - cero dependencias, control total | Necesitas temas complejos con puntos de ruptura y consultas de medios |
React Context ThemeProvider | Muchos componentes anidados comparten tokens | Pantallas únicas (la rama en línea está bien) |
Appearance.setColorScheme | Conmutador de tema claro/oscuro/sistema en la aplicación | Reemplazando definiciones de token - solo cambia el esquema activo |
@react-navigation/native theme | Chrome de navegación coincide con colores de aplicación | Interfaz no de navegación (aún necesitas tus propios tokens) |
react-native-unistyles / Tamagui / NativeWind | Escala de sistema de diseño, tokens receptivos + oscuros | Aplicaciones simples con una docena de colores |
expo-system-ui | Establecer color de fondo raíz nativamente al cambiar esquema | Colores de texto a nivel de componente |
"light", "dark" o null. Trata null como desconocido y regresa a "light" (o el predeterminado de tu aplicación). El hook se re-renderiza el componente cuando cambia la apariencia del SO.
import { Appearance } from "react-native";
Appearance.setColorScheme("dark"); // forzar oscuro
Appearance.setColorScheme("light"); // forzar luz
Appearance.setColorScheme(null); // seguir sistemaPersiste la opción del usuario y vuelve a aplicar en inicio en frío. useColorScheme refleja el esquema efectivo después de anular.
Las claves estáticas (fontSize, fontWeight, borderRadius) pueden vivir en un StyleSheet estático. Los colores que cambian por esquema deben aplicarse dinámicamente - ya sea reconstruir estilos con useMemo cuando cambia el esquema, o usar style={[styles.base, { color: theme.text }]}.
Configuración de Expo que establece la política de apariencia a nivel de aplicación: "automatic" (seguir SO), "light" o "dark". Afecta el comportamiento de la pantalla de splash y los valores predeterminados del shell nativo. Prefiere "automatic" para la mayoría de las aplicaciones.
import { StatusBar } from "expo-status-bar";
<StatusBar style={scheme === "dark" ? "light" : "dark"} />Contenido claro (iconos blancos) en fondos oscuros; contenido oscuro en fondos claros.
Probablemente codificaste colores en un StyleSheet.create a nivel de módulo. Mueve valores de color a una paleta codificada por esquema y aplícalos en render, o reconstruye estilos cuando cambia useColorScheme.
No - rama estilos, no árboles. Una estructura de componente con tokens de tema es más fácil de mantener que JSX duplicado para cada esquema.
Los componentes hacen referencia a theme.text en lugar de #0f172a. Cuando ajustas la paleta oscura, cada pantalla se actualiza. Renombrar gray-700 a una función como textSecondary sobrevive a revisiones de paleta.
Pasa un theme y darkTheme a NavigationContainer usando tu paleta de token. El contenido de la pantalla aún necesita su propia rama useColorScheme - la tematización de navegación solo cubre encabezados, pestañas y tarjetas construidas en el navegador.
Establece backgroundColor splash a tu token background oscuro en app.json. Usa expo-system-ui para establecer el fondo de la vista raíz. Aplica tema antes de renderizar la primera pantalla.
Appearance.getColorScheme() devuelve el valor actual de forma sincrónica. Para interfaz reactiva, prefiere useColorScheme en componentes o Appearance.addChangeListener en módulos no UI.
Las sombras son menos visibles en fondos oscuros - confía más en tokens de borde (borderColor: theme.border) y elevación de superficie sutil. Ver Sombras, Elevación y Bordes.
Usa scheme === "dark" para intercambiar source, aplica un tintColor claro, o coloca la imagen en un contenedor surface fijo que se mantenga claro en ambos esquemas (por ejemplo, bloqueo de logotipo de marca).
Sí - es un hook React Native estándar. Úsalo en cualquier pantalla o layout cliente. Se actualiza cuando cambia la apariencia del sistema mientras la aplicación se ejecuta.
Los insets de área segura son independientes del esquema de color, pero el contraste de la barra de estado depende de ambos. Coordina el estilo StatusBar con el fondo de tu encabezado. Ver Áreas Seguras y Muescas.
Text y anidamientoVersiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 19 jul 2026