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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
:root {
--primary-color: blue;
}
:root { --primary-color: blue; }
: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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.button {
background-color: blue;
}
.input {
border-color: blue;
}
.header {
color: blue;
}
.button { background-color: blue; } .input { border-color: blue; } .header { color: blue; }
.button {
  background-color: blue;
}

.input {
  border-color: blue;
}

.header {
  color: blue;
}

Natomiast kod używający CSS Variables tak:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.button {
background-color: var(--primary-color);
}
.input {
background-color: var(--primary-color);
}
.header {
background-color: var(--primary-color);
}
.button { background-color: var(--primary-color); } .input { background-color: var(--primary-color); } .header { background-color: var(--primary-color); }
.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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
:root {
--primary-color: blue;
}
body.green-theme {
--primary-color: green;
}
body.red-theme {
--primary-color: red;
}
:root { --primary-color: blue; } body.green-theme { --primary-color: green; } body.red-theme { --primary-color: red; }
: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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const response = await fetch(API_URL);
const data = await response.json();
document
.querySelector('body')
.style
.setProperty('--primary-color', data.primaryColor);
const response = await fetch(API_URL); const data = await response.json(); document .querySelector('body') .style .setProperty('--primary-color', data.primaryColor);
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
: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));
}
: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)); }
: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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
});
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); });
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@media (prefers-color-scheme: dark) {
:root {
--background-color: black;
--text-color: white;
}
}
@media (prefers-color-scheme: light) {
:root {
--background-color: white;
--text-color: black;
}
}
@media (prefers-color-scheme: dark) { :root { --background-color: black; --text-color: white; } } @media (prefers-color-scheme: light) { :root { --background-color: white; --text-color: black; } }
@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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
: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;
}
}
: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; } }
: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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.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);
}
.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); }
.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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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>
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>
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
: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;
}
: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; }
: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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const ThemeContext = createContext({
theme: 'No theme set',
setTheme: () => {},
});
const useThemeContext = () => useContext(ThemeContext);
const ThemeContext = createContext({ theme: 'No theme set', setTheme: () => {}, }); const useThemeContext = () => useContext(ThemeContext);
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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'} />
</>
)
}
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'} /> </> ) }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import appStyles from './App.module.css';
const className = theme === 'default'? ''
: theme === 'light'? appStyles.light : appStyles.dark;
import appStyles from './App.module.css'; const className = theme === 'default'? '' : theme === 'light'? appStyles.light : appStyles.dark;
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”:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.message {
/* style wygladu*/
background-color: var(--background-color);
color: var(--font-color);
}
.message { /* style wygladu*/ background-color: var(--background-color); color: var(--font-color); }
.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.

Jeden komentarz do “Sprawdź jak zaimplementować tryb ciemny w React”

Możliwość komentowania została wyłączona.