Przycisk opinii w formie animowanego kciuka w górę bez żadnej biblioteki animacji
Stwórz widget z opiniami w postaci kciuka w górę i w dół z animacją pop na podstawie kluczowych klatek, stanami aktywnymi oraz żądaniem danych, używając wyłącznie HTML, CSS i kilku linijek JavaScript.
Źródła: Creating an Interactive Thumbs Up Feedback Button with HTML, CSS and JavaScript
Klawisze z podniesionym i opuszczonym kciukiem to jeden z najtańszych sposobów na zapytanie użytkowników, czy strona, odpowiedź lub funkcja im pomogły. Zwykła implementacja polega jedynie na zmianie koloru tła po kliknięciu, co łatwo przeoczyć, jeśli nic innego na stronie się nie zmienia. W tym przewodniku stworzymy nieco bardziej rozbudowaną wersję: po kliknięciu na klawisz z podniesionym kciukiem ikona zostaje na chwilę zastąpiona błyszczącym emotikonem, który pojawia się dzięki animacji skali i obrotu, a następnie wraca do standardowej postaci podniesionego kciuka. Zobaczysz kod strukturalny, pliki CSS oraz JavaScript odpowiedzialne za sekwencjonowanie stanów, a także elementy, które należy poprawić przed publikacją.
Czym zajmuje się ten widget
Interfejs zawiera dwa okrągłe klawisze – jeden do pozytywnej, a drugi do negatywnej opinii. Kliknięcie w klawisz pozytywny uruchamia krótką sekwencję działań:
- Ikona podniesionego kciuka zostaje zastąpiona emotikonem otoczonym błyszczącymi znakami.
Takie mikrointerakcje sprawiają, że interfejs wydaje się responsywny bez konieczności używania frameworków animacyjnych.
Tagi do dwóch przycisków
Każdy przycisk ma identyfikator do skryptowania, wspólną klasę do stylizacji oraz klasę modyfikującą jego własny kolor aktywny. Ikony to znaki emoji umieszczone wewnątrz przycisków:
<div class="feedback">
<button
id="thumbsUp"
class="feedback-btn thumbs-up"
aria-label="Thumbs up">
👍🏻
</button>
<button
id="thumbsDown"
class="feedback-btn thumbs-down"
aria-label="Thumbs down">
👎🏻
</button>
</div>
Tutaj istotna jest właściwość aria-label. Bez niej odczytarka ekranu ogłosiłaby samo nazwę emoji (lub nic użytecznego), więc ta etykieta nadaje każdemu przyciskowi jasną, dostępną nazwę. Jeśli chcesz, aby technologie wspomagające wiedziały również, która opcja jest wybrana, dodaj aria-pressed i aktualizuj ją razem z klasą active.
Klawisze okrągłe z reakcją na przesunięcie kursora
Bazowy styl ustala rozmiar, usuwa domyślny wygląd, centruje ikonę za pomocą flexbox i sprawia, że klawisz ma kształt koła:
.feedback-btn {
width: 52px;
height: 52px;
border: 1px solid #ddd;
background: #fff;
border-radius: 50%;
cursor: pointer;
font-size: 25px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s ease;
}
border-radius: 50% w elementie kwadratowym tworzy kółko, a transition zapewnia płynne zmiany; podanie konkretnych właściwości zamiast all pozwala uniknąć animacji elementów, których nie chcieliśmy.
Zasada reakcji na przesunięcie kursora rozjaśnia tło i nieco powiększa klawisz, dzięki czemu kursor otrzymuje natychmiastową informację zwrotną:
.feedback-btn:hover {
background: #f5f5f5;
transform: scale(1.08);
}
Kluczowe klatki animacji stojące za efektem „pop”
Ciekawy moment to zmiana z gestu kciuka w górę na iskrzącego się emotikonę. Jest to obsługiwane przez zasadę @keyframes o nazwie sparkle:
@keyframes sparkle {
0% {
transform: scale(0.4) rotate(-20deg);
opacity: 0;
}
35% {
transform: scale(1.4) rotate(10deg);
opacity: 1;
}
65% {
transform: scale(1) rotate(-5deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
Jako wykres czasu: przy 0% treść jest zmniejszona do 40%, przechylona o 20 stopni do tyłu i niewidoczna. Przy 35% osiąga skalę 140%, z lekkim przechyłem w przeciwnym kierunku i pełną przejrzystością. Przy 65% wraca do normalnego rozmiaru z niewielką przeciwwrotką, a przy 100% pozostaje w swoim naturalnym rozmiarze i kącie. To przekroczenie wartości, po którym następuje ustabilizowanie, daje efekt „pop”. Należy pamiętać, że ta zasada definiuje jedynie ruch; osobna klasa .sparkle, która ją implementuje za pomocą animation: sparkle 0.8s ease forwards, znajduje się w kompletnym przykładzie na końcu.
Sekwencjonowanie stanów w JavaScript
Obsługa kliknięcia odpowiada za koordynację zamiany treści, klasę animacji oraz ostateczny stan:
thumbsUp.addEventListener("click", function () {
if (thumbsUp.classList.contains("active")) {
return;
}
thumbsUp.innerHTML = "✨😊✨";
thumbsUp.classList.add("sparkle");
setTimeout(function () {
thumbsUp.innerHTML = "👍";
thumbsUp.classList.remove("sparkle");
thumbsUp.classList.add("active");
}, 900);
});
Zaczyna się od sprawdzenia: jeśli przycisk ma już klasę active, funkcja obsługi wraca, dzięki czemu animacja nie może być odtwarzana wielokrotnym klikaniem. Następnie zastępuje się treść emotikoną:
thumbsUp.innerHTML = "✨😊✨";
i dodaje klasę, która uruchamia kluczowe klatki animacji:
thumbsUp.classList.add("sparkle");
Używany jest setTimeout na 900 milisekund, co jest nieco dłużej niż czas trwania animacji wynoszący 0,8 sekundy; po tym czasie przywraca się znak kciuka w górę, usuwa się klasę sparkle i dodaje klasę active:
thumbsUp.innerHTML = "👍";
Treść nigdy nie pochodzi od użytkownika, więc użycie innerHTML jest bezpieczne, chociaż textContent lepiej oddaje intencję.
Wyświetlanie wybranej opcji
Klasa active służy do wizualnego potwierdzenia. Pozytywna informacja zwrotna ma jasnozielony tło i zieloną obwódkę:
.thumbs-up.active {
background: #e8f5e9;
border-color: #4caf50;
}
Ujemna informacja zwrotna wykorzystuje ten sam wzorzec w kolorze czerwonym:
.thumbs-down.active {
background: #ffebee;
border-color: #f44336;
}
Różne kolory pozwalają od razu zobaczyć, która opcja została wybrana, ale nie należy polegać wyłącznie na kolorze; aria-pressed pomaga użytkownikom, którzy nie potrafią ich odróżnić.
Zapewnienie wzajemnej wykluczalności opcji
Przycisk z kciukiem w dół wykorzystuje prostszą interakcję bez animacji. Zmienia swój własny klasę active, usuwa stan pozytywny, a jeśli jest teraz wybrany, przywraca ikonę z kciukiem w górę do jej pierwotnego kształtu:
thumbsDown.addEventListener("click", function () {
thumbsDown.classList.toggle("active");
thumbsUp.classList.remove("active");
if (thumbsDown.classList.contains("active")) {
thumbsUp.innerHTML = "👍🏻";
}
});
Usuwanie wyboru z drugiego przycisku zapobiega temu, by obie opcje wyglądały na wybrane jednocześnie. Pozostaje jeden przypadek graniczny: jeśli użytkownik kliknie przycisk „plus” a następnie „minus” w ciągu 900 ms, nadal zostanie uruchomiony timer i ponownie aktywowany będzie przycisk „plus”. Aby zlikwidować tę lukę, należy przechowywać identyfikator timera i wywołać clearTimeout w obsłudze przycisku „minus”. Należy również zauważyć, że aktywny przycisk „plus” nie może zostać anulowany poprzez jego ponowne kliknięcie, podczas gdy przycisk „minus” zmienia stan; należy to uporządkować, aby oba funkcjonowały spójnie.
Dlaczego mała animacja jest warta wysiłku
Zmiana koloru w trybie statycznym wskazuje, że coś się wydarzyło; krótka animacja (kliknięcie, ruch, potwierdzenie) sprawia wrażenie, że interfejs zareagował. Jest to szczególnie ważne w przypadku informacji zwrotnych inline, gdy nic innego na stronie nie sygnalizuje sukcesu. Nadaje się do form ocen kursów, ocen w FAQ i bazach wiedzy, narzędzi wsparcia, recenzji produktów, ankiet, stron dokumentacyjnych, odpowiedzi chatbotów AI oraz narzędzi wewnętrznych.
Szanuj zmienną prefers-reduced-motion, wyłączając klatki kluczowe dla użytkowników, którzy proszą o mniej animacji.
Wysyłanie oceny do serwera backend
Jak jest napisane, widget ma charakter wyłącznie wizualny. W rzeczywistym produkcie konieczne jest przechowywanie wyboru, a proces rozszerza się o żądanie:
User clicks 👍
↓
Animation plays
↓
Feedback recorded
↓
Database updated
↓
User sees confirmation
Może to być dowolny backend – PHP, Node.js, Python lub coś innego. Z obsługi kliknięcia należy wysłać ocenę do odpowiedniego punktu końcowego:
fetch("/api/feedback", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
feedback: "positive"
})
});
Serwer może przechowywać stronę, użytkownika, odpowiedź, datę i typ informacji zwrotnej. Sprawdź wartość response.ok, ponieważ funkcja fetch odrzuca żądanie tylko w przypadku błędów sieciowych, i zdecyduj, czy animacja ma być uruchamiana w trybie optymistycznym (szybciej, ale wymagającego cofnięcia w razie niepowodzenia zapisu) czy tylko po odniesieniu sukcesu.
Cała strona
Tutaj znajduje się wszystko w jednym pliku, w tym klasa .sparkle, efekt skaliowania w stanie naciśnięcia :active oraz funkcja obsługi przycisku „polubić”, która usuwa wybór opcji „nie polubić”:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feedback Buttons</title>
<style>
.feedback {
display: flex;
gap: 12px;
align-items: center;
}
.feedback-btn {
width: 52px;
height: 52px;
border: 1px solid #ddd;
background: #fff;
border-radius: 50%;
cursor: pointer;
font-size: 25px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s ease;
}
.feedback-btn:hover {
background: #f5f5f5;
transform: scale(1.08);
}
.feedback-btn:active {
transform: scale(0.92);
}
/* Sparkle animation */
.sparkle {
animation: sparkle 0.8s ease forwards;
}
@keyframes sparkle {
0% {
transform: scale(0.4) rotate(-20deg);
opacity: 0;
}
35% {
transform: scale(1.4) rotate(10deg);
opacity: 1;
}
65% {
transform: scale(1) rotate(-5deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
/* Filled thumbs up */
.thumbs-up.active {
background: #e8f5e9;
border-color: #4caf50;
}
.thumbs-down.active {
background: #ffebee;
border-color: #f44336;
}
</style>
</head>
<body>
<div class="feedback">
<button
id="thumbsUp"
class="feedback-btn thumbs-up"
aria-label="Thumbs up">
👍🏻
</button>
<button
id="thumbsDown"
class="feedback-btn thumbs-down"
aria-label="Thumbs down">
👎🏻
</button>
</div>
<script>
const thumbsUp = document.getElementById("thumbsUp");
const thumbsDown = document.getElementById("thumbsDown");
thumbsUp.addEventListener("click", function () {
// Prevent repeated animation
if (thumbsUp.classList.contains("active")) {
return;
}
// Step 1: Sparkling smiley
thumbsUp.innerHTML = "✨😊✨";
thumbsUp.classList.add("sparkle");
// Step 2: Change to filled thumbs up
setTimeout(function () {
thumbsUp.innerHTML = "👍";
thumbsUp.classList.remove("sparkle");
thumbsUp.classList.add("active");
}, 900);
// Remove thumbs down selection
thumbsDown.classList.remove("active");
});
thumbsDown.addEventListener("click", function () {
thumbsDown.classList.toggle("active");
// Remove thumbs up selection
thumbsUp.classList.remove("active");
if (thumbsDown.classList.contains("active")) {
thumbsUp.innerHTML = "👍🏻";
}
});
</script>
</body>
</html>
Podsumowanie
Doskonała kontrola informacji zwrotnej wymaga jedynie HTML, CSS oraz kilku linijek JavaScriptu. Kluczowe klatki czasowe odpowiadają za efekt wybuczenia, sekwencje zmian klas w określonych momentach sterują stanami, a ostateczny podświetlony ikona potwierdza dokonany wybór.
- Zachowaj logikę animacji w CSS i pozwól JavaScriptowi jedynie przełączać klasy oraz treść.
aria-pressed oraz rozwiązanie awaryjne typu reduced-motion, aby widget pozostał dostępny.Taki sam podejście można zastosować do ikon SVG, animacji Lottie, dźwięków oraz bardziej zaawansowanych procesów opartych na API.
Pozycje pokrewne
- Kto powinien odpowiadać za zachowanie interfejsu? Wybór między HTML, CSS a JavaScript — ramy decyzyjne dotyczące tego, kiedy natywne funkcje HTML i CSS, takie jak dialogi, popovery, :has() oraz zapytania kontenerów, powinny zastąpić JavaScript, a kiedy nie.
- Struktura, styl, behawior: Jak HTML, CSS i JavaScript dzielą pracę — Przystępna dla początkujących mapa pokazująca, co każdy z tych elementów odpowiada za stronę internetową, jak ze sobą współpracują oraz jakie powszechne błędy zamazują granice między nimi.