Voting Buttons — Free Upvote Downvote HTML CSS JS Snippet

Voting Buttons · Buttons · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Three-state voting
Up, neutral, and down with click-to-undo on the active arrow.
Immutable base score
Score is base + vote, so rapid toggling never drifts.
Color feedback
Green for positive, red for negative, neutral at zero.
Bump animation
The score lifts and scales on every change via reflow restart.
Accessible buttons
Real buttons with aria-pressed and aria-label for screen readers.
Single vote choke point
All logic flows through cast() — one place to add API calls.
Tactile press
Arrows scale down on :active for a physical feel.
No dependency
Pure HTML/CSS/JS — no framework, no icon library.

About this UI Snippet

Voting Buttons — Upvote / Downvote Score Widget

Screenshot of the Voting Buttons snippet rendered live

Voting buttons are the up/down arrows beside a post, comment, or feature request that let users rate it and surface the best content — the pattern Reddit, Stack Overflow, and Product Hunt are built on. This snippet implements the full interaction in plain HTML, CSS, and vanilla JavaScript: an upvote and downvote arrow, a live score between them, three-state toggle logic, and color feedback, with no framework or dependency.

Three-state toggle logic

Each row tracks one of three states — upvoted (+1), neutral (0), or downvoted (-1) — in a votes map keyed by item id. The key behaviour is that clicking the *already-active* arrow clears the vote rather than re-applying it: votes[id] = (votes[id] === dir) ? 0 : dir. That single expression handles every transition (up→neutral, up→down, neutral→down, and back) the way users expect, so there is no separate "remove vote" button.

Score derived from a base, never mutated

The displayed number is computed as base + vote, where base is the server-side tally and vote is only the current user's contribution. Keeping the base immutable and deriving the score on every render means the count is always correct after any sequence of clicks — you can't drift out of sync by toggling rapidly, because nothing is incremented in place.

Color and motion feedback

The score turns green when positive, red when negative, and neutral at zero, and the active arrow fills with a matching tint via the .on class. Every change triggers a short vbBump keyframe — the number lifts and scales briefly — restarted reliably with the void el.offsetWidth reflow trick so the animation replays even on consecutive clicks. The arrows also scale down on :active for a tactile press.

Accessible by default

Arrows are real <button> elements with aria-label and aria-pressed reflecting the current vote, so screen readers announce "Upvote, pressed" correctly and keyboard users can Tab and activate them with Enter or Space — no extra wiring needed.

Wiring to real data

Replace the DATA array with your posts and send a request in cast() (the single choke point for every vote) to POST the new direction; optimistic UI already updates instantly, so you only need to reconcile on error. The scoreFor() helper is pure, making it trivial to also sort the list by score for a "hot" or "top" ranking.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA list of items renders, each with an upvote arrow, score, and downvote arrow.
  2. 2
    Click upvoteThe arrow fills green and the score increases by one with a bump animation.
  3. 3
    Click the active arrow againYour vote clears and the score returns to its base value.
  4. 4
    Switch directionClicking downvote after an upvote moves the score by two in one step.
  5. 5
    Watch the color statesPositive scores turn green, negative turn red, zero stays neutral.
  6. 6
    Wire to your APISwap the DATA array and POST the new direction inside cast().

Real-world uses

Common Use Cases

Feature request boards
Let users vote ideas up the list, paired with a poll widget.
Comment threads
Score replies the way Reddit does inside a comment thread.
Q&A and forums
Surface the best answers Stack Overflow-style above a rating breakdown.
Product feedback
Collect signal next to an NPS survey or helpful feedback widget.
Changelog reactions
Gauge interest on shipped items in a changelog feed.
Learning the pattern
A clean reference for three-state toggle voting logic.

Got questions?

Frequently Asked Questions

Voting is three-state. The cast() function uses votes[id] = (votes[id] === dir) ? 0 : dir, so clicking the arrow you already selected sets the vote back to neutral and the score returns to its base. Clicking the opposite arrow switches direction in a single step, moving the displayed score by two.

The displayed number is base + vote, where base is the server tally and vote is just this user's -1, 0, or +1. Deriving it on every render means no sequence of rapid clicks can corrupt the count, which is a common bug when you mutate a counter directly on each press.

Every vote flows through cast(), so it is the only place you add a request. POST the item id and new direction, update the UI optimistically (it already does), and roll back only if the request fails. Because the base is separate from the user's vote, reconciling the server's true total is straightforward.

Yes. The arrows are native button elements with aria-label and aria-pressed set to the current state, so they are focusable, operable with Enter or Space, and announced as pressed or not pressed. No custom key handling or roles are required.

Hold the votes map in state (useState in React, a ref or reactive object in Vue, a component property in Angular) and render the score as base + vote. Move the cast() toggle into an event handler that also calls your API. In Tailwind, apply the green and red tints with conditional utility classes based on the vote value instead of the .on class.