Alterando a cor do tema no React Native

O React Native color scheme é o recurso que informa ao aplicativo se o usuário prefere a aparência clara (light) ou escura (dark) no dispositivo. Com ele, o app pode acompanhar automaticamente o tema escolhido no sistema operacional.

Neste artigo, vamos alterar as cores de um aplicativo React Native com base nas preferências do usuário. Para isso, usaremos o hook useColorScheme, o TypeScript e o styled-components.

Por que oferecer temas light e dark

Em muitos aplicativos mobile ou desktop, é comum encontrar a opção de alterar o tema da aplicação. Além disso, vários deles permitem sincronizar o tema com a escolha feita no dispositivo.

O Discord, por exemplo, oferece a escolha manual entre os temas disponíveis nas configurações do próprio app.

Tela de configurações de aparência do Discord com as opções de tema claro e escuro
O Discord permite alterar o tema da aplicação diretamente nas configurações.

O Slack, por sua vez, consegue seguir a escolha feita no sistema operacional. Assim, quando o usuário muda a aparência do dispositivo, o app acompanha a mudança.

Configurações de tema do Slack com a opção de sincronizar com a aparência do sistema operacional
O Slack pode alterar o tema com base na escolha do sistema operacional.

Esse comportamento melhora a experiência, porque respeita uma decisão que o usuário já tomou. Por isso, ele virou praticamente um padrão em aplicativos modernos.

O que é o React Native color scheme

No React Native, o color scheme vem do módulo Appearance. Segundo a documentação oficial do Appearance, ele expõe as preferências de aparência do usuário, como o esquema de cores preferido.

O módulo se inspira no rascunho de Media Queries do W3C. Na prática, ele reproduz no mobile o que a media feature prefers-color-scheme faz no CSS da web.

Se você já trabalha com temas na web, o artigo sobre formatos de cores no CSS ajuda a escolher bons valores para cada paleta.

Os valores do hook useColorScheme

A forma mais simples de ler o React Native color scheme em componentes funcionais é o hook useColorScheme. Ele lê o valor atual e também se inscreve nas atualizações do módulo Appearance.

O hook pode retornar três valores:

  • light: o usuário prefere o tema claro.
  • dark: o usuário prefere o tema escuro.
  • null: nenhuma preferência foi indicada ou o módulo nativo não está disponível.

Como o valor pode ser nulo, o código sempre precisa de um tema padrão. Veremos isso no exemplo mais adiante.

Suporte ao React Native color scheme nas plataformas

No Android, o React Native color scheme corresponde ao tema claro ou escuro do sistema a partir do Android 10 (API 29). Já no iOS, ela corresponde ao Light ou Dark Mode a partir do iOS 13.

Em versões anteriores dos sistemas, portanto, não existe uma preferência global de aparência. Nesse caso, o app deve cair no tema padrão definido por você.

Criando o projeto de exemplo

O projeto foi construído na versão 0.64.0 do React Native, a mais recente na data da publicação original. Mesmo assim, o mesmo código também funciona em outras versões que já tenham o hook.

Opcionalmente, você pode fixar a versão desejada e escolher o TypeScript na criação. Aqui, vamos usar o TypeScript, e o projeto se chamará Colourse.

A configuração completa do ambiente está no guia Setting up the development environment. Com o ambiente pronto, crie o projeto pelo template de TypeScript:

npx react-native init Colourse --template react-native-template-typescript

Se você prefere outra combinação de React com TypeScript no mobile, veja também o artigo sobre React, Ionic e TypeScript.

Ferramentas utilizadas

Para montar o exemplo, usaremos o styled-components, mas você pode adotar a solução de estilos que preferir. A instalação e a configuração estão no guia oficial do styled-components.

No React Native, os componentes estilizados vêm do caminho styled-components/native. Além disso, o ThemeProvider disponibiliza o tema para todos os componentes da árvore por meio do contexto do React.

Definindo os temas do React Native color scheme

Vamos criar dois temas para coincidir com as opções que o usuário pode escolher no dispositivo. Cada tema define, por enquanto, apenas a cor de fundo e a cor do texto.

O tema light fica em src/theme/light/index.ts:

import { DefaultTheme } from 'styled-components/native';

// Tema claro: fundo quase branco e texto quase preto
const light: DefaultTheme = {
  colors: {
    background: '#F6F6F6',
    text: '#121212',
  },
};

export default light;

O tema dark fica em src/theme/dark/index.ts e inverte as duas cores:

import { DefaultTheme } from 'styled-components/native';

// Tema escuro: as cores do tema claro invertidas
const dark: DefaultTheme = {
  colors: {
    background: '#121212',
    text: '#F6F6F6',
  },
};

export default dark;

Repare também que o tema escuro usa #121212 em vez do preto puro. Esse é o tom de superfície escura usado como superfície base nos temas escuros do Material Design, o que evita contraste excessivo com o texto claro.

Da mesma forma, o texto do tema claro usa #121212 em vez de preto absoluto. Com isso, as duas paletas ficam simétricas e mais confortáveis para leituras longas.

Em projetos reais, os temas costumam ganhar mais cores, como primária, secundária, bordas e estados de erro. Ainda assim, a regra continua a mesma: cada chave precisa existir nos dois temas.

Note que a constante do tema escuro se chama dark. Esse detalhe evita confusão na hora de importar os dois arquivos no mesmo módulo.

Tipando o tema com TypeScript

Para o TypeScript reconhecer as propriedades do tema, você amplia a interface DefaultTheme do styled-components. Uma forma comum é criar um arquivo de declaração, como src/styled.d.ts:

import 'styled-components';

// Amplia a interface padrão com as cores usadas nos temas
declare module 'styled-components' {
  export interface DefaultTheme {
    colors: {
      background: string;
      text: string;
    };
  }
}

Com isso, o editor sugere theme.colors.background e acusa erro se algum tema esquecer uma cor. Dessa forma, os dois temas sempre seguem o mesmo contrato.

Agrupando os temas

Em seguida, vale reunir os dois temas em um único objeto, indexado pelo nome do esquema. Uma forma simples de fazer isso é um arquivo src/theme/index.ts:

import light from './light';
import dark from './dark';

// As chaves coincidem com os valores retornados pelo useColorScheme
const themes = { light, dark };

export default themes;

Assim, o valor devolvido pelo hook serve diretamente como chave para buscar o tema correto.

Estrutura do projeto

A estrutura padrão do React Native foi mantida conforme a criação do projeto. Afinal, o React Native color scheme não exige nenhuma pasta especial.

Apenas uma pasta src foi adicionada com as referências dos temas.

App.tsx
├── src
    ├── theme
        ├── dark
        ├── light

Os arquivos de cada tema estão no repositório do projeto no GitHub.

Estrutura de pastas do projeto com os temas light e dark do React Native color scheme dentro de src/theme
Estrutura do tema apresentada no projeto Colourse.

Estrutura dos componentes

Para dar visibilidade às mudanças de tema, criamos dois componentes: um wrapper e uma label. O primeiro representa a view do conteúdo da tela, enquanto o segundo exibe o texto com a cor do tema.

import styled from 'styled-components/native';

// Container da tela: usa a cor de fundo do tema ativo
const Wrapper = styled.View`
  background-color: ${({ theme }) => theme.colors.background};
  flex: 1;
  justify-content: center;
  align-items: center;
`;

// Texto central: usa a cor de texto do tema ativo
const Label = styled.Text`
  color: ${({ theme }) => theme.colors.text};
  font-size: 24px;
`;

Repare que nenhum dos componentes conhece as cores diretamente, o que os deixa prontos para qualquer valor do React Native color scheme. Em vez disso, eles leem tudo do theme, que o ThemeProvider injeta.

Depois disso, a estrutura do App.tsx passa a envolver os componentes com o provedor de tema:

return (
  <ThemeProvider theme={theme}>
    <Wrapper>
      <Label>{deviceColorScheme}</Label>
    </Wrapper>
  </ThemeProvider>
);

Alterando o tema com o React Native color scheme

Com o hook, conseguimos ler a preferência do usuário. Então, com base nela, definimos o tema de estilo da aplicação.

import React from 'react';
import { useColorScheme } from 'react-native';
import { ThemeProvider } from 'styled-components/native';

import themes from './src/theme';

const App = () => {
  // Lê a preferência de aparência do dispositivo
  const deviceColorScheme = useColorScheme();

  // Usa o tema dark quando não há preferência definida
  const theme = themes[deviceColorScheme || 'dark'];

  return (
    <ThemeProvider theme={theme}>
      <Wrapper>
        <Label>{deviceColorScheme}</Label>
      </Wrapper>
    </ThemeProvider>
  );
};

export default App;

A expressão deviceColorScheme || 'dark' define o tema escuro como padrão quando o hook retorna null. Se preferir, troque por 'light', de acordo com a identidade visual do seu app.

A label exibe o próprio valor do React Native color scheme na tela. Dessa forma, fica fácil confirmar qual tema está ativo durante os testes.

Testando o React Native color scheme no simulador iOS

No simulador do iOS usado aqui, a configuração inicial era light. Portanto, na primeira execução do projeto, o app aparece com o tema claro configurado.

Aplicativo React Native com color scheme light no simulador iOS, fundo claro e texto light no centro
Apresentação do aplicativo com o tema light.

Para alterar a aparência do simulador, abra os ajustes e siga o caminho abaixo:

Settings > Developer > Appearance > Dark Appearance
Tela de ajustes do simulador iOS com a seção Developer aberta
Ajustes do simulador iOS, onde fica a opção de aparência.
Opção Dark Appearance ativada nas preferências de desenvolvedor do simulador iOS
Alterando as preferências de aparência do usuário.

Em seguida, reabra o aplicativo para visualizar as modificações. O fundo fica escuro, e a label passa a exibir o valor dark.

Aplicativo React Native com color scheme dark no simulador iOS, fundo escuro e texto dark no centro
Apresentação do aplicativo com o tema dark.

Atalhos para trocar a aparência

Além do caminho pelos ajustes, o simulador aceita o atalho Command + Shift + A para alternar entre light e dark. Também é possível usar o terminal com o xcrun simctl:

# Ativa o modo escuro no simulador em execução
xcrun simctl ui booted appearance dark

# Volta para o modo claro
xcrun simctl ui booted appearance light

Esses comandos ajudam bastante quando você testa vários temas em sequência. Afinal, eles evitam navegar pelos ajustes a cada troca.

Cuidados com o React Native color scheme no Android

No Android, a preferência de aparência só existe a partir do Android 10. Por isso, em versões anteriores, o hook não recebe uma escolha do usuário e o tema padrão entra em ação.

Em projetos React Native puros, a documentação do Expo recomenda conferir se a MainActivity declara a flag uiMode em configChanges no AndroidManifest.xml. Sem ela, a mudança de tema pode recriar a Activity em vez de apenas notificar o app.

Em versões antigas, também houve relatos de que o hook não atualizava no Android. Um exemplo é a issue 28823 do repositório do React Native, aberta na versão 0.62.2.

Portanto, se a troca não aparecer na hora, reabra o aplicativo como fizemos no iOS. Em seguida, confira a versão do React Native e a configuração do manifesto.

Ouvindo mudanças fora dos componentes

O hook resolve a maioria dos casos dentro de componentes funcionais. No entanto, às vezes você precisa ler o tema em outro ponto do código.

Nesse caso, o módulo Appearance oferece o método getColorScheme(), que retorna o React Native color scheme ativo. Além disso, addChangeListener() registra uma função chamada sempre que a preferência muda, e removeChangeListener() remove essa função.

A documentação recomenda não guardar o valor em cache. Afinal, a preferência pode mudar a qualquer momento, por ação do usuário ou por agendamento, como temas que seguem o nascer e o pôr do sol.

Por isso, prefira consultar o valor a cada renderização, que é exatamente o que o hook faz por você.

Boas práticas para o React Native color scheme

Algumas decisões simples deixam o React Native color scheme mais fácil de manter conforme o app cresce.

  • Centralize todas as cores nos objetos de tema e evite valores fixos nos componentes.
  • Mantenha os dois temas com as mesmas chaves, garantidas pela tipagem do DefaultTheme.
  • Defina conscientemente o tema padrão para quando o hook retornar null.
  • Teste as telas nos dois temas, inclusive imagens e ícones que podem sumir no fundo escuro.
  • Avalie oferecer também uma escolha manual, como fazem o Discord e o Slack.

Seguindo esses pontos, o app respeita a preferência do usuário sem espalhar condicionais de cor pelo código.

Ajustando a StatusBar ao React Native color scheme

Trocar o fundo da tela não basta para um tema escuro convincente. Afinal, a barra de status também precisa contrastar com o fundo, senão relógio e bateria somem.

O componente StatusBar do React Native aceita a propriedade barStyle, com os valores light-content e dark-content. Então, basta escolher o valor com base no mesmo React Native color scheme que define o tema.

import { StatusBar, useColorScheme } from 'react-native';

const AppStatusBar = () => {
  const deviceColorScheme = useColorScheme();

  // Conteúdo claro sobre fundo escuro e vice-versa
  const barStyle = deviceColorScheme === 'light' ? 'dark-content' : 'light-content';

  return <StatusBar barStyle={barStyle} />;
};

Repare que a regra segue o mesmo padrão do tema: quando não há preferência, o app assume o modo escuro. Dessa forma, a barra de status e o conteúdo da tela nunca ficam em desacordo.

Permitindo a escolha manual do tema

Como vimos no Discord, muitos usuários querem escolher o tema dentro do próprio app. Nesse caso, a preferência do sistema vira apenas uma das opções disponíveis.

Uma forma simples de implementar isso é guardar a escolha em um estado do React. Assim, a opção system segue o React Native color scheme, enquanto light e dark forçam um tema fixo.

import React, { useState } from 'react';
import { useColorScheme } from 'react-native';
import { ThemeProvider } from 'styled-components/native';

import themes from './src/theme';

type ThemePreference = 'system' | 'light' | 'dark';

const App = () => {
  const deviceColorScheme = useColorScheme();
  const [preference, setPreference] = useState<ThemePreference>('system');

  // A opção system segue o dispositivo; as demais forçam um tema
  const activeScheme = preference === 'system'
    ? deviceColorScheme || 'dark'
    : preference;

  return (
    <ThemeProvider theme={themes[activeScheme]}>
      {/* telas do aplicativo */}
    </ThemeProvider>
  );
};

Em seguida, uma tela de configurações pode chamar setPreference quando o usuário tocar em cada opção. Para manter a escolha entre execuções, persista o valor em um armazenamento local de sua preferência.

Código da aplicação

O código completo da aplicação está disponível no repositório react-native-color-scheme no GitHub. Lá você encontra o App.tsx e os arquivos dos dois temas.

Conclusão

O React Native color scheme permite que o app acompanhe a aparência escolhida no dispositivo com pouco código. Basta ler o valor com o useColorScheme e entregar o tema correspondente ao ThemeProvider.

Com temas tipados, um padrão bem definido e testes nos dois modos, a troca entre light e dark fica previsível. Assim, o usuário encontra o app exatamente do jeito que configurou o próprio dispositivo.

FAQ

Quais valores o useColorScheme pode retornar?

O hook retorna light, dark ou null. O null aparece quando não há preferência indicada ou quando o módulo nativo não está disponível.

Preciso do styled-components para usar temas no React Native?

Não, o hook funciona com qualquer solução de estilos. O styled-components apenas facilita a distribuição do tema pelo ThemeProvider.

A partir de quais versões do sistema existe o modo escuro?

No Android, a partir do Android 10 (API 29). No iOS, a partir do iOS 13.

Como trocar a aparência do simulador iOS rapidamente?

Use o atalho Command + Shift + A ou o comando xcrun simctl ui booted appearance dark no terminal.

Por que o tema não muda na hora no Android?

Verifique se a MainActivity declara uiMode em configChanges e qual versão do React Native o projeto usa. Se necessário, reabra o aplicativo.