CSS Variables – czemu warto je znać?

Spis treści

  1. Wprowadzenie
  2. Czym są CSS Variables?
  3. Bardzo praktyczne użycie CSS Variables
    1. Implementacja edytora wizualnego do CSS Variables
    2. Klikalny przykład na Github Pages
    3. Pełen kod przykładu na Github
  4. Podsumowanie

Wprowadzenie

Przed pojawieniem się CSS Custom Properties (znane też jako CSS Variables) implementacja takich funkcjonalności jak różne motywy, tryb ciemny czy danie możliwości konfigurowania kolorystyki dynamicznie była dość uciążliwa. Preproesory takie jak SCSS wspomagały ten proces poprzez własną implementację zmiennych i możliwość pisania funkcji generujących np różne klasy dla różnych motywów – ale efektem takich działań była spora ilość generowanego kodu, który musiał powstać by zrekompensować brak dynamiki.

CSS Variables pozwalają zmieniać wartości w stylach w pełni dynamicznie – a ten post pokaże, do czego tą właściwość można wykorzystać.

Czym są CSS Variables?

Pokrótce – CSS Variables to zmienne zawierające wartości, których możemy używać w różnych miejscach stylów CSS oraz które możemy dynamicznie zmieniać, z poziomu CSS lub JS.
Dodatkowo możliwość nazywania zmiennych pozwala nadawać im kontekst.

Przykładowo, zdeklarujmy zmienne definiujące główny oraz poboczny kolor tekstu w aplikacji wraz z kolorem tła:

:root {
  --text-color-main: black;
  --text-color-secondary: darkgray;
  --background-color-main: white;
}

Dzięki temu kod ma szansę stać się dużo bardziej samoopisujący się, przykładowo:

* {
  background: var(--background-color-main);
}

h1, h2, h3 {
  color: var(--text-color-primary);
}

small, h4, h5, h6 {
  color: var(--text-color-secondary);
}

Powyższy kod od razu ujawnia intencje autora.

Jeśli chcemy w tym momencie np zaimplementować tryb ciemny, wystarczy że zmienimy kolory bazowe:

.dark-mode {
  --text-color-main: white;
  --text-color-secondary: lightgray;
  --background-color-main: black;
}

Zmian możemy dokonywać również z poziomu JS:

const root = document.documentElement;

root.style.setProperty('--text-color-main', 'white');
root.style.setProperty('--text-color-secondary', 'lightgray');
root.style.setProperty('--background-color-main', 'black');

Bardzo praktyczne użycie CSS Variables

Do czego w praktyce można użyć CSS Variables? Do generowania wielu opcji kolorystycznych UI naszego projektu.

Przykładem może być tryb ciemny:

Aczkolwiek prostota ich wykorzystania powoduje że w łatwy sposób można stworzyć nawet edytor wizualny – co będzie tematem tego rozdziału.

Implementacja edytora wizualnego do CSS Variables

Zacznijmy od zdefiniowania zmiennych na elemencie html:

html {  
  --text-color-main: #000000;
  --text-color-secondary: #808080;
  /* inne zmienne*/  
}

Dlaczego na elemencie html a nie :root jak przeważnie jest w przykładach? Ponieważ deklaracja na elemencie html spowoduje, że wartości CSS Variables dzięki temu będzie prościej wczytać w JS.

A po co będziemy potrzebować wczytać te wartości w JS? Ponieważ będziemy chcieli te wartości ustawić jako domyślne w formularzu, który pozwoli je na żywo edytować.

Kod formularza wyglądać będzie następująco:

<form>
  <label for="--text-color-main">Text color main</label><input id="--text-color-main" type="color"><br>
  <label for="--text-color-secondary">Text color secondary</label><input id="--text-color-secondary" type="color"><br>
  <!--  Inne pola-->
  <button type="submit">
    Download changes
  </button>
</form>

ID inputa odpowiada nazwie zmiennej CSS, dzięki czemu w prosty sposób po załadowaniu się strony możemy wpisać w formularz domyślne wartości:

const htmlStyles = getComputedStyle(document.documentElement);
const variables = [...document.querySelectorAll('input, select')]
  .filter(el => el.id.startsWith('--'))
  .reduce((acc, input) => {
    acc[input.id] = htmlStyles.getPropertyValue(input.id).trim();
      return acc;
    }, {});
applyVariables(variables);

Z głównego elementu document.documentElement, czyli tagu html, pobieramy wartości styli. Następnie pobieramy wszystkie pola formularza, filtrujemy je tak by mieć tylko te z ID odpowiadającym nazwie zmiennej CSS. Po czym budujemy obiekt gdzie nazwa zmiennej będzie kluczem, a wartość pod kluczem odpowiadać będzie wartości tej zmiennej.

Samo zaaplikowanie zmiennych wyglądać będzie tak:

const applyVariables = variables => {
  for (const propertyName in variables) {
    if (variables.hasOwnProperty(propertyName)) {
      const pureValue = variables[propertyName];

      const value = isNaN(parseInt(pureValue)) ? pureValue : parseInt(pureValue);
      document.querySelector(`#${propertyName}`).value = value;

      const valueForHtml = isNaN(parseInt(pureValue)) ? pureValue : `${parseInt(pureValue)}px`;
      document.documentElement.style.setProperty(propertyName, valueForHtml);
    }
  }
};

Powyższy kod bierze wartość zmiennej i, zakładając pewne uproszczenia, działa następująco:

  1. Jeśli zmienna jest liczbą, oznacza wartość w pikselach, np wielkość czcionki i przed ustawieniem jej bezpośrednio na elemencie html należy dodać jej 'px' by została poprawnie zaaplikowana.
  2. Jeśli nie jest – np jest to kolor w hexach – możemy aplikować ją bezpośrednio.

Dlaczego w ogóle aplikujemy zmienne na elemencie html, skoro zostały one już ustawione w CSS?
Dlatego, że użyjemy tej samej funkcji do aplikowania na żywo zmian wczytanych z pliku.

Ale najpierw aplikacja zmian na żywo, by mieć podgląd w jaki sposób zmiana w CSS Variables wpływa na UI:

form.addEventListener('change', e => {
  const input = e.target;
  const pureValue = input.value.trim();
  const value = isNaN(parseInt(pureValue))? pureValue : `${parseInt(pureValue)}px`;
  variables[input.id] = value;
  document.documentElement.style.setProperty(input.id, value);
});

Każda zmiana w formularzu będzie widoczna na UI.

Jednak chcemy również zapisać nasze zmiany do pliku w momencie zatwierdzenia formularza:

form.addEventListener('submit', e => {
  e.preventDefault();

  const result = {};
  for (const input of e.currentTarget) {
    if (input.id.startsWith('--')) {
      const pureValue = input.value;
      result[input.id] = isNaN(parseInt(pureValue)) ? pureValue : `${pureValue}px`;
    }
  }
  const useNewSave = document.querySelector('#new-way-to-save').checked;
  const blob = new Blob([JSON.stringify(result, null, 2)], {type: 'text/application/json'});
  if (useNewSave) {
    saveNew(blob);
  }
  else {
    saveOld(blob);
  }
});

W momencie submitu formularza pobieramy wszystkie inputy które zawierają wartości dla zmiennych CSS, tworzymy z nich bloba który zostanie zapisany jako plik JSON.

W formularzu istnieje pole odpowiedzialne za wybór sposobu zapisu. Dostępne są 2 sposoby – „nowy” i „stary”.

„Nowy” polega na użyciu File System Access Api:

const pickerOptions = {
  types: [
    {
      description: 'Files with variables',
      accept: {
        'application/json': ['.json']
      }
    },
  ],
  excludeAcceptAllOption: true,
};

const saveNew = async blob => {
  const handler = await window.showSaveFilePicker({
    ...pickerOptions,
    suggestedName: 'variables.json',
    startIn: 'documents',
  });
  const writable = await handler.createWritable();
  await writable.write(blob);
  await writable.close();
}

Przypomina on sposób zapisywania dokumentów znany m.in. z natywnych aplikacji Windows, pojawia się okno wyboru pod jaką nazwą i gdzie zapisać plik.

Możesz poczytać więcej o tym API tutaj:

„Stary” sposób polega na programowym stworzeniu linka i kliknięciu go z kodu, by plik ze zmiennymi się pobrał:

const saveOld = blob => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'variables.json';
  a.click();
  setTimeout(() => {
    URL.revokeObjectURL(url);
  }, 100);
};

Przykład umożliwia również wczytanie uprzednio zapisanych plików ze zmiennymi.

Tutaj również istnieje „nowy” i „stary” sposób na odczyt pliku z dysku.

„Nowy” używa wspomnianego wcześniej File System Access Api:

document.querySelector('#load-new').addEventListener('click', async e => {
  const [handler] = await window.showOpenFilePicker(pickerOptions);
  const file = await handler.getFile();
  const fileStr = await file.text();
  const variables = JSON.parse(fileStr);
  applyVariables(variables);
});

„Stary” polega na użyciu pola typu file:

document.querySelector('#load-old').addEventListener('change', async e => {
  const fileStr = await e.target.files[0].text();
  const variables = JSON.parse(fileStr);
  applyVariables(variables);
});

Klikalny przykład na Github Pages

Klikalny przykład dostępny jest pod adresem:

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

Pełen kod przykładu na Github

Pełen kod przykładu dostępny jest pod adresem:

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

Podsumowanie

CSS Variables mogą bardzo istotnie wspomóc proces tworzenia różnych tematów w projektach komercyjnych, dając możliwość konfiguracji nawet klientom końcowym. W połączeniu z szerokim już dziś wsparciem wydają się być podstawowym narzędziem pracy przy tworzeniu nowoczesnego, elastycznego designu.
Dlatego warto je znać i stosować w rzeczywistych projektach.