Home / Articles

An Animated Thumbs-Up Feedback Button Without Any Animation Library

Build a thumbs-up and thumbs-down feedback widget with a keyframe pop animation, active states and a fetch call, using only HTML, CSS and a few lines of JavaScript.

Publisher: REACTAPP.TOP7 min read1559 words

This article is published in English.

Sources: Creating an Interactive Thumbs Up Feedback Button with HTML, CSS and JavaScript

Thumbs-up and thumbs-down buttons are one of the cheapest ways to ask users whether a page, answer or feature helped them. The usual implementation just changes a background colour on click, which is easy to miss when nothing else on the page changes. This walkthrough builds a slightly richer version: clicking thumbs-up briefly swaps the icon for a sparkling smiley that pops in with a scale-and-rotate animation, then settles into a highlighted thumbs-up. You will see the markup, the CSS, the JavaScript that sequences the states, and the gaps to close before shipping it.

What the widget does

The interface has two circular buttons, one for positive and one for negative feedback. A click on the positive one runs a short sequence:

  1. The thumbs-up icon is replaced by a smiley flanked by sparkle characters.
  2. A keyframe animation pops it in with a slight grow-and-rotate.
  3. After a short pause, the smiley gives way to a plain thumbs-up again.
  4. The button switches to an active, highlighted state.

Micro-interactions like this make an interface feel responsive without an animation framework.

Markup for the two buttons

Each button gets an id for scripting, a shared class for styling and a modifier class for its own active colour. The icons are emoji characters inside the buttons:

<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>

The aria-label matters here. Without it, a screen reader would announce the emoji's own name (or nothing useful), so the label gives each button a clear accessible name. If you want assistive technology to also know which option is selected, add aria-pressed and update it alongside the active class.

Round buttons with a hover response

The base style fixes the size, removes the default look, centres the icon with flexbox and makes the button a circle:

.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% on a square element makes the circle, and transition smooths later changes; listing specific properties instead of all avoids animating things you did not intend to.

A hover rule lightens the background and enlarges the button a little, so the pointer gets immediate feedback:

.feedback-btn:hover {
    background: #f5f5f5;
    transform: scale(1.08);
}

The keyframes behind the pop

The interesting moment is the switch from thumbs-up to the sparkling smiley. That is handled by a @keyframes rule named 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);
    }
}

As a timeline: at 0% the content is shrunk to 40%, tilted back by 20 degrees and invisible. By 35% it has overshot to 140% scale with a slight tilt the other way and full opacity. At 65% it is back to normal size with a small counter-rotation, and at 100% it rests at its natural size and angle. The overshoot followed by a settle is what reads as a "pop". Note that this rule only defines the motion; a separate .sparkle class that applies it with animation: sparkle 0.8s ease forwards appears in the complete example at the end.

Sequencing the states in JavaScript

The click handler for thumbs-up coordinates the content swap, the animation class and the final state:

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);
});

It opens with a guard: if the button already has the active class, the handler returns, so the animation cannot be replayed by clicking repeatedly. Then it replaces the content with the smiley:

thumbsUp.innerHTML = "✨😊✨";

and adds the class that triggers the keyframes:

thumbsUp.classList.add("sparkle");

A setTimeout of 900 milliseconds, slightly longer than the 0.8 second animation, then restores the thumbs-up character, removes sparkle and adds active:

thumbsUp.innerHTML = "👍";

The content is never user input, so innerHTML is safe here, though textContent expresses the intent better.

Highlighting the selected option

The active class carries the visual confirmation. Positive feedback gets a pale green background and a green border:

.thumbs-up.active {
    background: #e8f5e9;
    border-color: #4caf50;
}

Negative feedback uses the same pattern in red:

.thumbs-down.active {
    background: #ffebee;
    border-color: #f44336;
}

Distinct colours show at a glance which choice was recorded, but do not rely on colour alone; aria-pressed helps users who cannot tell them apart.

Keeping the choices mutually exclusive

Thumbs-down uses a simpler interaction with no animation. It toggles its own active class, clears the positive state and, if it is now selected, resets the thumbs-up icon to its original form:

thumbsDown.addEventListener("click", function () {
    thumbsDown.classList.toggle("active");
    thumbsUp.classList.remove("active");
    if (thumbsDown.classList.contains("active")) {
        thumbsUp.innerHTML = "👍🏻";
    }
});

Clearing the other button prevents both options from looking selected at once. One edge case remains: if a user clicks thumbs-up and then thumbs-down within the 900 ms window, the pending timeout still fires and re-activates thumbs-up. Store the timer id and call clearTimeout in the thumbs-down handler to close that gap. Note too that an active thumbs-up cannot be undone by clicking it, while thumbs-down toggles; make the two consistent.

Why a small animation is worth the effort

A static colour change says something happened; a short animation (click, motion, confirmation) makes the interface feel like it responded. That matters most for inline feedback, where nothing else on the page signals success. It suits course feedback forms, FAQ and knowledge-base ratings, support tools, product reviews, surveys, documentation sites, AI chatbot answers and internal tools.

Respect prefers-reduced-motion by disabling the keyframes for users who ask for less animation.

Sending the rating to a backend

As written, the widget is purely visual. In a real product the choice needs to be stored, and the flow grows to include a request:

User clicks 👍
       ↓
Animation plays
       ↓
Feedback recorded
       ↓
Database updated
       ↓
User sees confirmation

Any backend works, whether PHP, Node.js, Python or something else. From the click handler you would post the rating to an endpoint:

fetch("/api/feedback", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        feedback: "positive"
    })
});

The server can store the page, user, response, timestamp and feedback type. Check response.ok, because fetch only rejects on network errors, and decide whether the animation plays optimistically (faster, but needs a rollback if saving fails) or only after success.

The complete page

Here is everything in a single file, including the .sparkle class, a pressed-state :active scale, and the thumbs-up handler clearing the thumbs-down selection:

<!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>

Wrapping up

A polished feedback control needs only HTML, CSS and a few lines of JavaScript. The keyframes supply the pop, a timed class swap sequences the states, and the final highlighted icon confirms the choice.

  • Keep animation logic in CSS and let JavaScript only toggle classes and content.
  • Track the timeout so rapid clicks on the other button cannot leave both options active.
  • Add aria-pressed and a reduced-motion fallback so the widget stays accessible.
  • Treat the backend call as part of the interaction: handle failures and decide between optimistic and confirmed feedback.

The same approach extends to SVG icons, Lottie animations, sound cues and richer API-backed workflows.