Sprawdź jak zaimplementować tryb ciemny w React

Implementacja trybu ciemnego w React

Spis treści

  1. Wprowadzenie
  2. Zanim zaczniemy
    1. React Context
    2. CSS Custom Properties (aka CSS Variables)
      1. Czym są CSS custom properties?
      2. Przykład użycia CSS Variables
      3. Kod przykładu na Github
      4. Interaktywny przykład na Github Pages
    3. Prefers-color-scheme
      1. Czym jest prefers-color-scheme?
      2. Przykład użycia prefers-color-scheme
      3. Kod przykładu na Github
      4. Interaktywny przykład na Github Pages
  3. Tryb ciemny w React
    1. Przykład
    2. Kod przykładu na Github
    3. Interaktywny przykład na Github Pages
  4. Podsumowanie

Wprowadzenie

Tryb ciemny jest standardowym rozwiązaniem w nowoczesnych aplikacjach jak Github. Zmianę trybu na bardziej sprzyjający używaniu komputera po ciemku wspierają już natywnie systemy operacyjne. Na coraz większej ilości stron internetowych, między innymi dokumentacji Material UI, dostępne są przyciski przełączające między trybem dziennym i nocnym.

Sam zapewne używasz trybu ciemnego w swoim IDE, jak większość programistów.

Warto więc wiedzieć, jak w prosty sposób zabrać się za implementację tych trybów w Twojej aplikacji, zwłaszcza że – spoiler alert! – istnieje możliwość sprawdzenia z poziomu CSS i JS, czy system operacyjny użytkownika działa w trybie nocnym i do tego dostosować wyświetlanie w aplikacji.

Najpierw omówimy potrzebne tematy z osobna, a potem wszystko zgrabnie połączymy w prosty przykład obsługi trybu jasnego i ciemnego.

Zanim zaczniemy

React Context

React Context przyda nam się do przetrzymywania tego, jaki tryb wybrał użytkownik. Jeśli wcześniej nie miałeś z nim do czynienia, polecam Ci zajrzeć do postu w którym Context jest pokrótce opisany – Context jako jedna z możliwych alternatyw dla Reduxa:

Wpis poruszający między innymi temat React Context

CSS Custom Properties (aka CSS Variables)

Czym są css custom properties?

CSS pozwala na definiowanie zmiennych dzięki funkcjonalności o nazwie CSS Custom Properties, która często jest nazywana CSS Variables. W skrócie chodzi o to, że jeżeli zadeklarujemy zmienną, np w zakresie globalnym:

:root {
  --primary-color: blue;
}

To potem możemy używać tej zmiennej unikając wielu powtórzeń w kodzie. Kod klasyczny wyglądałby następująco:

.button {
  background-color: blue;
}

.input {
  border-color: blue;
}

.header {
  color: blue;
}

Natomiast kod używający CSS Variables tak:

.button {
  background-color: var(--primary-color);
}

.input {
  background-color: var(--primary-color);
}

.header {
  background-color: var(--primary-color);
}

Dzięki temu że używamy wszędzie tej samej zmiennej, w prosty sposób można zaimplementować np zmiany kolorystyki szablonów. Dodatkowo, można to zrobić zarówno z poziomu CSS jak i JS, dzięki czemu te wartości mogą przyjść z backendu – na przykład administrator strony może zmieniać jej kolorystykę bez konieczności zdeployowania nowej wersji aplikacji poprzez zmianę wartości w JSON jaki zwraca backend. Zmiana w CSS mogłaby wyglądać tak:

:root {
  --primary-color: blue;
}

body.green-theme {
  --primary-color: green;
}

body.red-theme {
  --primary-color: red;
}

A sama klasa do body dodawana byłaby przez JS.

Natomiast chcąc zmienić wartość zmiennej bezpośrednio z kodu JS mogłoby to wyglądać tak:

const response = await fetch(API_URL);
const data = await response.json();
document
  .querySelector('body')
  .style
  .setProperty('--primary-color', data.primaryColor);

Przykład użycia CSS Variables

Stwórzmy oparty na 3 sliderach przykład pokazujący jak użyć CSS Variables do zmiany koloru elementu. Każdy ze sliderów będzie odpowiadał za jeden z palety kolorów RGB. Stwórzmy więc kod CSS:

:root {
  --red-value: 0;
  --green-value: 0;
  --blue-value: 0;
}

.result {
  width: 50px;
  height: 50px;
  background-color: rgb(var(--red-value), var(--green-value), var(--blue-value));
}

Kod HTML zawierał będzie 3 sildery utworzone za pomocą elementu input typu range:

<div id="bubble-wrapper">
  <label for="red">Red</label>
  <input type="range" min="0" max="255" step="1" value="0" id="red">
  <br>
  <label for="green">Green</label>
  <input type="range" min="0" max="255" step="1" value="0" id="green">
  <br>
  <label for="blue">Blue</label>
  <input type="range" min="0" max="255" step="1" value="0" id="blue">
</div>
<br>
Result:
<div class="result"></div>

Wrapper potrzebny jest nam po to, by zamiast podpinać się pod event „input” trzykrotnie, osobno dla każdego slidera – podpiąć się pod ten event tylko na jednym elemencie. Ponieważ event „input” propaguje się w górę możemy podpiąć się pod niego w następujący sposób:

document.querySelector('#bubble-wrapper').addEventListener('input', e => {
  if (!['red', 'green', 'blue'].includes(e.target.id)) {
    return;
  }
  document
    .querySelector('body')
    .style
    .setProperty(`--${e.target.id}-value`, e.target.value);
});

Efektem działania tego kodu będzie zmiana wartości zmiennych na elemencie body:

Zaktualizowane wartości zmiennych CSS za pomocą zmiany wartości na sliderach

Kod przykładu na Github

Kod przykładu znajdziesz na:

https://github.com/radek-anuszewski/css-variables-demo

Interaktywny przykład na Github Pages

Jeżeli chcesz zobaczyć jak kod działa w praktyce, możesz odwiedzić Github Pages:

https://radek-anuszewski.github.io/css-variables-demo/index.html

Prefers-color-scheme

Czym jest prefers-color-scheme?

Prefers-color-scheme to funkcjonalność pozwalająca wykryć jaki schemat kolorów, jasny czy ciemny, ma ustawiony użytkownik w swoim systemie operacyjnym. Deklaracja wygląda w następujący sposób:

@media (prefers-color-scheme: dark) {
    :root {
        --background-color: black;
        --text-color: white;
    }
}

@media (prefers-color-scheme: light) {
    :root {
        --background-color: white;
        --text-color: black;
    }
}

Oczywiście wewnątrz deklaracji prefers-color-scheme można pisać dowolne style, nie tylko zmienne.

Przykład użycia prefers-color-scheme

Napiszmy prosty przykład użycia tej funkcjonalności – kwadrat z tekstem w środku, gdzie dla trybu ciemnego tło będzie czarne a tekst biały, a dla trybu jasnego odwrotnie. Stwórzmy style które będą zmieniać kolory w zależności od schematu kolorów:

:root {
    --background-color: red;
    --text-color: red;
}

@media (prefers-color-scheme: dark) {
    :root {
        --background-color: black;
        --text-color: white;
    }
}

@media (prefers-color-scheme: light) {
    :root {
        --background-color: white;
        --text-color: black;
    }
}

Sam element, kwadrat o boku 300px, zawierąjacy tekst, ostylowany będzie następująco:

.result {
    width: 200px;
    height: 200px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 20px;
    background-color: var(--background-color);
    color: var(--text-color);
}

Kod HTML będzie bardzo prosty:

If you set dark color scheme in your OS, you'll se black background and white text.
<br>
If not, you'll see white background and black text.
<div class="result">
  Result
</div>

Ponieważ mam włączony tryb ciemny, wyświetla się u mnie czarny kwadrat z białym tekstem:

Efekt uruchomienia przykładu dla prefers-color-scheme: dark

Kod przykładu na Github

Kod przykładu dostępny jest pod adresem:

https://github.com/radek-anuszewski/prefers-color-scheme-demo

Interaktywny przykład na Github Pages

Jeśli chcesz sprawdzić działanie na Twoim urządzeniu odwiedź adres:

https://radek-anuszewski.github.io/prefers-color-scheme-demo/index.html

Tryb ciemny w React

Przykład

Połączmy teraz wszystkie elementy w jedną całość w przykładzie, który zawierał będzie możliwość przełączania się pomiędzy trybem jasnym, ciemnym i domyślnym ustawieniem systemu.

Do stylowania aplikacji użyjemy CSS Modules, które jest domyślnie wspierane w create-react-app i o którym wspominam w artykule Poznaj 5 Sposobów Stylowania Komponentów React w 2020:

Bazując na przykładzie prefers-color-scheme dodajmy ustawianie zmiennych CSS dla następujących przypadków:

  1. Tryb jasny
  2. Tryb ciemny
  3. Klasa light
  4. Klasa dark

Kod prezentuje się następująco:

:root {
    --background-color: white;
    --color: black;
}

@media (prefers-color-scheme: light) {
    :root {
        --background-color: white;
        --font-color: black;
    }
}

@media (prefers-color-scheme: dark) {
    :root {
        --background-color: black;
        --font-color: white;
    }
}

.light {
    --background-color: white;
    --font-color: black;
}

.dark {
    --background-color: black;
    --font-color: white;
}

Stwórzmy Context który na poziomie globalnym będzie przetrzymywał informacje o tym jaki tryb jest wybrany oraz customowy hook, który uprości odpytywanie o te wartości w komponentach:

const ThemeContext = createContext({
  theme: 'No theme set',
  setTheme: () => {},
});

const useThemeContext = () => useContext(ThemeContext);

Do zmiany wartości będą służyć przyciski, z których każdy dostanie wartość jaką będzie ustawiał. Dodatkowo, jeżeli jego wartość będzie taka sama jak ustawiona w Context, będzie on wyłączony:

const Button = ({value}) => {
  const { theme, setTheme } = useThemeContext()

  return (
    <button
      onClick={() => setTheme(value)}
      disabled={theme === value}
    >
      {value.toUpperCase()}
    </button>
  )
}

const Buttons = () => {
  return (
    <>
      <Button value={'light'} />
      <Button value={'dark'} />
      <Button value={'default'} />
    </>
  )
}

Klasa ustawiająca wartość zmiennych, bądź jej brak wyliczana będzie na podstawie obecnie wybranego trybu:

import appStyles from './App.module.css';

const className = theme === 'default'? ''
    : theme === 'light'? appStyles.light : appStyles.dark;

CSS modules dla o unikalność nazw klas, dlatego importujemy style jako obiekt, gdzie nazwom klas odpowiadają nazwy kluczy tego obiektu.

Same zmienne natomiast są używane do stylowania „wiadomości”:

.message {
  /* style wygladu*/
  background-color: var(--background-color);
  color: var(--font-color);
}

Pełny kod do obejrzenia będzie na Githubie i do przeklikania na Github Pages, jego rezultat przy domyślnym trybie ciemnym będzie następujący:

Rezultat przykładu dla domyślnego trybu ciemnego

Natomiast po przełączenia na tryb jasny:

Rezultat przykładu po przełączeniu się na tryb jasny

Kod przykładu na Github

Kod przykładu możesz znaleźć pod adresem:

https://github.com/radek-anuszewski/dark-mode-context-demo

Interaktywny przykład na Github Pages

Klikalną wersję aplikacji znajdziesz tutaj:

https://radek-anuszewski.github.io/dark-mode-context-demo/

Podsumowanie

Wpis ten przeprowadził Cię przez napisanie prostej aplikacji zawierającej tryb jasny i ciemny. Choć prawdziwa produkcyjna aplikacja miałaby zapewne parę kruczków, gdzie do obsługi tych trybów wymagany byłby jakiś dedykowany kod, znaczna część przełączania miedzy trybem jasnym i ciemnym może zadziać się automatycznie.