Nowoczesny przełącznik kolorów strony internetowej używając tylko HTML i CSS
Ostatnio znalazłem chwilę, by obejrzeć niezwykle wnikliwą i dynamiczną prezentację Adama Argyle’a zatytułowaną „25 nowych i świetnych funkcji CSS”.
Obejrzyj tutaj: 25 nowych i świetnych funkcji CSS autorstwa Adama Argyle’a
Już po pięciu minutach, gdy pokazał zastosowanie @view-transition { navigation: auto; } i cała publiczność zareagowała entuzjastycznie, wciągnęło mnie to na dobre i oglądałem aż do końca.
Dowiedziałem się naprawdę wiele i wdrożyłem sporo z omawianych funkcji na swoich stronach. O części z nich słyszałem wcześniej, ale nie mam zwyczaju wdrażać wszystkiego od razu. Gdy technologie są jeszcze na wczesnym etapie adopcji w przeglądarkach, wolę poczekać. Problem w tym, że później często o nich zapominam, dlatego prezentacja Adama przypomniała mi o kilku koncepcjach, które już kiedyś spotkałem.
Jedną z nich było użycie color-scheme: light dark; oraz podejście z wykorzystaniem składni light-dark() dla kolorów jasnych i ciemnych.
Na swojej stronie wdrożyłem jakiś czas temu trójpoziomowy przełącznik kolorów. Jednak rozwiązanie to mocno opierało się na JavaScripcie oraz regułach CSS, które trzeba było powielać, aby uzyskać oczekiwany efekt.
Dzięki wprowadzeniu light-dark nadszedł moment, by odejść od tamtego podejścia i przekształcić mój przełącznik motywów kolorystycznych w rozwiązanie oparte wyłącznie na HTML i CSS.
Dodałem jedynie minimalną ilość JavaScriptu — opcjonalnie, ale przydatnie z perspektywy użytkownika.
Byłem zaskoczony, jak prosta okazała się ta konwersja i że funkcjonalność dla użytkowników pozostała bez zmian. Szczerze mówiąc, wątpię, by ktokolwiek zauważył różnicę. Usprawnienie najbardziej widoczne jest w prostocie mojego CSS po mojej stronie, natomiast dla zwykłego odwiedzającego wszystko działa jak dotychczas.
Przyjrzyjmy się mojej implementacji.
Śmiało wykorzystaj ją na swojej stronie :)
CSS – prefers-color-scheme (wsteczna kompatybilność)
Zanim wdrożyłem nowe podejście light-dark() dla trybu jasnego i ciemnego na swojej stronie, zadałem sobie dwa pytania:
- Czy to zadziała u użytkowników, którzy nie korzystają z najnowszej przeglądarki?
- Czy kolory nadal będą wyświetlane zgodnie z założeniem?
Odpowiedź brzmiała nie.
Dlatego musiałem zapewnić wsteczną kompatybilność tak długo, jak będzie to konieczne.
Umieściłem kolory w elemencie :root {} w moim CSS. Domyślnie wszystko w tym elemencie odnosi się do kolorów jasnych. Następnie określiłem opcję dla ciemnego motywu przy użyciu zapytania @media, jak robiłem to już od pewnego czasu.
W standardowej formie wygląda to tak:
/* Motyw jasny (domyślny) – rozwiązanie awaryjne dla przeglądarek bez żadnego wsparcia */
:root {
--main: #137faa;
}
/* Motyw ciemny – rozwiązanie awaryjne dla przeglądarek obsługujących zapytania medialne, ale nie light-dark() */
@media (prefers-color-scheme: dark) {
:root {
--main: #2EBAE0;
}
}
Zmienna --main jest zdefiniowanym parametrem dla mojego koloru głównego, którego później używam w CSS w następujący sposób:
.article-entry p > a {
color: var(--main);
}
To działa dokładnie tak jak wcześniej. Gdy użytkownik ustawi swoje urządzenie w trybie jasnym, otrzymuje główny kolor #137faa. Po przełączeniu na tryb ciemny wyświetlana jest ciemna wersja koloru głównego #2EBAE0.
CSS – color-scheme (przyszła kompatybilność)
Gdy rozwiązanie awaryjne dla wstecznej kompatybilności jest już przygotowane, nadchodzi czas na wprowadzenie nowej składni. Robię to w taki sposób, aby była stosowana tylko wtedy, gdy przeglądarka użytkownika obsługuje color-scheme: light dark; stąd użycie klauzuli @supports oraz umieszczenie :root {} wewnątrz niej.
Zaczynając od color-scheme: light dark;, skupiamy się następnie na predefiniowaniu parametrów używanych w całym stylach.
/* Nowa składnia dla nowoczesnych przeglądarek */
/* Ten blok zostanie odczytany przez nowoczesne przeglądarki i nadpisze rozwiązania awaryjne */
@supports (color-scheme: light dark) {
:root {
color-scheme: light dark;
--main: light-dark(#137faa, #2EBAE0);
}
}
Jak widać, składnia light-dark(#137faa, #2EBAE0); jest znacznie prostsza. W przypadku motywu jasnego używa #137faa jako koloru głównego, a po przełączeniu na tryb ciemny stosuje #2EBAE0 jako jego ciemny wariant.
To podejście jest proste, a jednocześnie bardzo skuteczne — pozwala utrzymać CSS w większym porządku i wyraźnie rozróżnić miejsca, w których kolory się zmieniają, od tych, gdzie stosowane są kolory stałe.
Teraz pora zająć się naszym przełącznikiem motywów kolorystycznych.
HTML – trójpoziomowy przełącznik motywów kolorystycznych
Mój wcześniejszy przełącznik kolorów opierał się na The Best Light/Dark Mode Theme Toggle in JavaScript autorstwa Salmy Almy-Naylor oraz It’s Tri-State Switch Time autorstwa Bryce’a Wraya.
Nie ma w nim nic złego — działa dobrze zarówno w starszych, jak i nowoczesnych przeglądarkach. Jedynym problemem, z mojej perspektywy, było to, że musiałem brać pod uwagę różne sposoby serwowania kolorów.
Na przykład, gdy użytkownik ma włączony tryb ciemny, ale chce przełączyć stronę na tryb jasny, albo gdy witryna domyślnie ustawia tryb jasny, a użytkownik woli ciemny.
To wprowadza pewien poziom złożoności przy użyciu [data-theme="dark"] i [data-theme="light"]. Muszę myśleć odwrotnie: gdy na urządzeniu użytkownika ustawiony jest motyw jasny, ale przełącznik ustawiony jest na ciemny, :root {} musi serwować ciemną wersję kolorów, ponieważ w takim przypadku zapytanie @media stoi w sprzeczności z wybraną opcją w przełączniku.
Zdecydowałem się teraz usunąć tę złożoność, z drobnym kompromisem, który omówię później.
Mój nowy przełącznik oparty wyłącznie na HTML, który nie korzysta z żadnego JavaScriptu, wygląda tak:
<div class="switchTheme">
<label class="lightLabel">
<input type="radio" name="switchTheme" value="light" id="lightMode" aria-label="{{ T "SelectLightMode" }}" title="{{ T "LightMode" }}">
<span class="toggle-switch__control"></span>
</label>
<label class="autoLabel">
<input type="radio" name="switchTheme" value="light dark" id="autoMode" aria-label="{{ T "SelectAutoMode" }}" title="{{ T "AutoMode" }}" checked>
<span class="toggle-switch__control"></span>
</label>
<label class="darkLabel">
<input type="radio" name="switchTheme" value="dark" id="darkMode" aria-label="{{ T "SelectDarkMode" }}" title="{{ T "DarkMode" }}">
<span class="toggle-switch__control"></span>
</label>
</div>
Ponieważ korzystam z Hugo do budowania swojej strony, wprowadziłem przetłumaczone elementy do
aria-labeloraztitle.
i18n\pl.toml
[LightMode]
other = "Jasny motyw"
[SelectLightMode]
other = "Wybierz jasny motyw"
[AutoMode]
other = "Tryb automatyczny"
[SelectAutoMode]
other = "Wybierz motyw jasny lub ciemny automatycznie"
[DarkMode]
other = "Ciemny motyw"
[SelectDarkMode]
other = "Wybierz ciemny motyw"
Modyfikacja mojego przełącznika do nowego podejścia nie wymaga ode mnie zmiany (prawie) żadnego z pierwotnego kodu CSS.
elementy switchTheme ze style.css
/* trójpoziomowy przełącznik trybu ciemnego wyłącznie w HTML i CSS */
.switchTheme {
display: flex; /* tylko to zostanie przeniesione dalej */
flex-direction: row;
gap: 0;
white-space: nowrap;
width: 6rem;
justify-content: space-around;
}
.lightLabel,
.autoLabel,
.darkLabel {
display: inline-block;
}
.switchTheme input[type="radio"] {
display: none;
}
.switchTheme svg {
fill: var(--black);
height: 1rem;
width: 1rem;
}
/* sun */
#lightMode + .toggle-switch__control::after {
height: 1.5rem;
width: 1.5rem;
display: block;
content: '';
-webkit-mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM200-440H40v-80h160v80Zm720 0H760v-80h160v80ZM440-760v-160h80v160h-80Zm0 720v-160h80v160h-80ZM256-650l-101-97 57-59 96 100-52 56Zm492 496-97-101 53-55 101 97-57 59Zm-98-550 97-101 59 57-100 96-56-52ZM154-212l101-97 55 53-97 101-59-57Zm326-268Z'/></svg>") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM200-440H40v-80h160v80Zm720 0H760v-80h160v80ZM440-760v-160h80v160h-80Zm0 720v-160h80v160h-80ZM256-650l-101-97 57-59 96 100-52 56Zm492 496-97-101 53-55 101 97-57 59Zm-98-550 97-101 59 57-100 96-56-52ZM154-212l101-97 55 53-97 101-59-57Zm326-268Z'/></svg>") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--nightgray);
}
/* auto */
#autoMode + .toggle-switch__control::after {
height: 1.5rem;
width: 1.5rem;
display: block;
content: '';
-webkit-mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M276-280h76l40-112h176l40 112h76L520-720h-80L276-280Zm138-176 64-182h4l64 182H414Zm66 376q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z'/></svg>") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M276-280h76l40-112h176l40 112h76L520-720h-80L276-280Zm138-176 64-182h4l64 182H414Zm66 376q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z'/></svg>") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--nightgray);
}
/* moon */
#darkMode + .toggle-switch__control::after {
height: 1.5rem;
width: 1.5rem;
display: block;
content: '';
-webkit-mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z'/></svg>") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z'/></svg>") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--nightgray);
}
/* sun:checked */
#lightMode:checked + .toggle-switch__control::after {
height: 1.5rem;
width: 1.5rem;
display: block;
content: '';
-webkit-mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM200-440H40v-80h160v80Zm720 0H760v-80h160v80ZM440-760v-160h80v160h-80Zm0 720v-160h80v160h-80ZM256-650l-101-97 57-59 96 100-52 56Zm492 496-97-101 53-55 101 97-57 59Zm-98-550 97-101 59 57-100 96-56-52ZM154-212l101-97 55 53-97 101-59-57Zm326-268Z'/></svg>") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM200-440H40v-80h160v80Zm720 0H760v-80h160v80ZM440-760v-160h80v160h-80Zm0 720v-160h80v160h-80ZM256-650l-101-97 57-59 96 100-52 56Zm492 496-97-101 53-55 101 97-57 59Zm-98-550 97-101 59 57-100 96-56-52ZM154-212l101-97 55 53-97 101-59-57Zm326-268Z'/></svg>") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--yellow);
}
/* auto:checked */
#autoMode:checked + .toggle-switch__control::after {
height: 1.5rem;
width: 1.5rem;
display: block;
content: '';
-webkit-mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M276-280h76l40-112h176l40 112h76L520-720h-80L276-280Zm138-176 64-182h4l64 182H414Zm66 376q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z'/></svg>") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M276-280h76l40-112h176l40 112h76L520-720h-80L276-280Zm138-176 64-182h4l64 182H414Zm66 376q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z'/></svg>") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--main);
}
/* moon:checked */
#darkMode:checked + .toggle-switch__control::after {
height: 1.5rem;
width: 1.5rem;
display: block;
content: '';
-webkit-mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z'/></svg>") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'><path d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z'/></svg>") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--yellow);
}
.toggle-switch__control { cursor: pointer;}
/* KONIEC */
Kilka przykładów wykorzystania podejścia
light-dark()oraz trójpoziomowego przełącznika można znaleźć w artykule CSS colour-scheme-dependent colours with light-dark() na web.dev. Dla podstawowego zrozumienia podejścialight-dark()zdecydowanie polecam wpis Trysa Mudforda Implementing light-dark().
Aby przełącznik działał, musisz zintegrować jego stan z CSS.
CSS dla trójpoziomowego przełącznika motywów kolorystycznych
Oto zasadnicza część kodu CSS, która umożliwia działanie przełącznika w zależności od jego stanu.
/* Gdy zaznaczone jest radio "Auto" (light dark) */
&:has(input[name="switchTheme"][value="light dark"]:checked) {
color-scheme: light dark;
}
/* Gdy zaznaczone jest radio "Light" */
&:has(input[name="switchTheme"][value="light"]:checked) {
color-scheme: light;
}
/* Gdy zaznaczone jest radio "Dark" */
&:has(input[name="switchTheme"][value="dark"]:checked) {
color-scheme: dark;
}
To nie wszystko.
Ponieważ brałem pod uwagę wsteczną kompatybilność, muszę pamiętać, że w przeglądarkach, które jeszcze nie obsługują składni light-dark(), mój przełącznik nie będzie działał. W takich przypadkach stan witryny (tryb jasny lub ciemny) będzie zależał wyłącznie od ustawień systemowych użytkownika.
CSS – ukrywanie przełącznika dla przestarzałych przeglądarek
W sytuacji, gdy użytkownik odwiedza moją stronę w przestarzałej przeglądarce, wyświetlanie przełącznika nie ma sensu. Dlatego zdecydowałem się ponownie wykorzystać podejście @supports, aby warunkowo pokazywać go tylko w nowoczesnych przeglądarkach.
Domyślnie ukrywam przełącznik i przywracam go dla przeglądarek obsługujących color-scheme.
.switchTheme {
/* Ukrywa przełącznik początkowo we wszystkich przeglądarkach */
display: none !important;
}
/* Jeśli przeglądarka obsługuje zarówno właściwość color-scheme */
@supports (color-scheme: light dark) {
.switchTheme {
/* Wyświetlaj przełącznik tylko w przeglądarkach, które go obsługują */
display: flex !important; /* to zostało przeniesione z głównego pliku CSS dla mojego przełącznika */
}
/* pozostały CSS z poprzedniej części */
}
Należy pamiętać, że mój przełącznik jest pozycjonowany przy użyciu elementu
flex, który został usunięty z głównej sekcji CSS i przeniesiony powyżej.
Przechodząc na implementację wyłącznie w HTML i CSS, utraciłem jedną kluczową funkcjonalność, która z perspektywy użytkownika może być nieco irytująca. Gdy zmieniają oni motyw za pomocą przełącznika, a następnie przechodzą na inną stronę w mojej witrynie, stan nie jest zapamiętywany i powraca do ustawienia domyślnego (kontrolowanego przez system).
Aby to przywrócić, muszę na końcu dodać bardzo minimalną ilość JavaScriptu, aby wprowadzić pamięć.
<script defer src="darkmode-toggle-memory.js"></script>
JS (opcjonalnie) – zapamiętywanie stanu przełącznika
Aby przechować stan przełącznika w mojej wcześniejszej implementacji opartej na JavaScripcie, używałem localStorage przeglądarki.
Wykorzystam tę część ponownie, aby dodać pamięć do mojej nowej wersji.
// Pojedynczy klucz do przechowywania wartości preferencji użytkownika (np. 'dark', 'light', 'light dark')
const THEME_STORAGE_KEY = 'theme-preference';
// Pobierz wszystkie przyciski radiowe
const themeRadios = document.querySelectorAll('input[name="switchTheme"]');
// --- 1. Wczytaj stan podczas ładowania strony ---
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) || 'light dark'; // Default to 'auto'
const savedRadio = document.querySelector(`input[name="switchTheme"][value="${savedTheme}"]`);
if (savedRadio) {
// Zaznacz przycisk radiowy, który odpowiada zapisanej wartości
savedRadio.checked = true;
}
// Twój istniejący CSS przechwyci teraz ten zaznaczony stan za pomocą selektora :has()!
// --- 2. Zapisz stan po kliknięciu ---
themeRadios.forEach(radio => {
radio.addEventListener('click', () => {
// Po kliknięciu zapisz jego wartość (`value`) w localStorage
localStorage.setItem(THEME_STORAGE_KEY, radio.value);
});
});
To rozwiązuje kwestię zapamiętywania stanu przełącznika i bez dodawania zauważalnego obciążenia działa płynnie dla wszystkich.
Użytkownicy, którzy domyślnie przeglądają z wyłączonym JavaScriptem — a ich liczba stale rośnie — również będą zadowoleni z takiego podejścia.
Pozdrawiam.




Komentarze i Reakcje