webdevpuneet.com

Upvote / Downvote Widget — Free HTML CSS JS Voting Arrows Snippet

Upvote / Downvote Widget · Buttons · Plain HTML, CSS & JS · Live preview

CategoryButtons

Copy & paste

Source Code

<div class="vote-card">
  <div class="vote" data-count="128" data-user-vote="0">
    <button class="vote-btn up" onclick="vote(this, 1)" aria-label="Upvote" aria-pressed="false">
      <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 4l8 9h-5v7h-6v-7H4z"/></svg>
    </button>
    <span class="vote-count">128</span>
    <button class="vote-btn down" onclick="vote(this, -1)" aria-label="Downvote" aria-pressed="false">
      <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 20l-8-9h5V4h6v7h5z"/></svg>
    </button>
  </div>
  <div class="post-body">
    <h3>Why static site generators are having a moment</h3>
    <p>A quick breakdown of why teams keep coming back to SSGs for content-heavy sites, even with the rise of full-stack frameworks.</p>
    <div class="meta">Posted by u/devfox · 3 hours ago · 42 comments</div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Single vote(btn, direction) function drives both arrows and the count with no duplicated logic
Vote state stored as base count plus signed user vote — canceling a vote always returns the exact original number
Clicking the active arrow again cancels the vote; clicking the opposite arrow flips it in one click
Matching active colors on the arrow icon and the count text keep the widget visually consistent
aria-pressed kept in sync with the visual state so screen readers announce the current vote direction
Scale-down active state and focus-visible outline for tactile, keyboard-friendly interaction
No global variables — all state lives in the widget's own dataset attributes
Easy to wire to a real backend by extending the single vote function with a fetch call
Works with any number of independent vote widgets on the same page without id collisions
No framework, no state management library, no build step required

About this UI Snippet

Upvote / Downvote Widget — HTML, CSS & JavaScript Voting Component

Screenshot of the Upvote / Downvote Widget snippet rendered live

Forum and link-aggregator sites like Reddit and Hacker News use a simple two-arrow voting widget to let users rank content: an up arrow, a running score, and a down arrow, stacked vertically beside each post. Clicking an arrow changes the score and highlights whichever direction is currently active — clicking it again cancels the vote, and clicking the opposite arrow flips it.

This snippet reproduces that exact interaction model in plain HTML, CSS, and vanilla JavaScript, with a single vote(btn, direction) function driving all the logic and no external state library involved.

How vote state is tracked

Each .vote widget stores two numbers directly on its own dataset: data-count is the *base* score before the current user's vote, and data-user-vote is 1, -1, or 0 depending on what the current user has clicked. The displayed count is always baseCount + userVote — recomputing it this way (rather than mutating the base count directly) means toggling a vote off always returns to the exact original number, with no drift from repeated clicks.

How the toggle logic works

The core of vote() is one line: userVote = userVote === direction ? 0 : direction. If you click upvote while already upvoted, this sets userVote back to 0, canceling it. If you click upvote while downvoted, it jumps straight to 1 — a single click flips a downvote into an upvote, exactly like the real thing, because the displayed count moves by up to 2 in one click (from -1 to +1 relative to base).

How the active-color highlighting works

Two CSS classes, .vote-btn.up.active and .vote-btn.down.active, recolor the arrow icons — orange for upvote, indigo for downvote. The count text itself picks up a matching .up-color or .down-color class so the whole widget visually agrees on which direction is currently selected. Both classes are toggled based on the freshly computed userVote, so only one arrow (or neither) is ever highlighted at a time.

Accessibility

Both buttons carry aria-pressed, updated alongside the visual state on every click, so screen readers announce whether a given arrow is currently the active vote — a natural fit since each arrow behaves like a toggle button, not a one-shot action.

Step by step

How to Use

  1. 1
    Load the snippetClick "Upvote / Downvote Widget" in the sidebar Library tab. The preview shows a post card with a 128-point vote widget beside it.
  2. 2
    Click the arrowsClick the up arrow to see the count increase and turn orange, click it again to cancel the vote, then try the down arrow to see it flip to indigo.
  3. 3
    Set a real starting countIn the HTML panel, update data-count on the .vote div to the post's actual score from your backend.
  4. 4
    Wire it to your APIIn the JS panel, extend vote() to send the new userVote value to your server (e.g. via fetch) instead of, or alongside, updating the DOM.
  5. 5
    Recolor the statesIn the CSS panel, change the colors on .vote-btn.up.active and .vote-btn.down.active to match your brand.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for React, or "Tailwind" for React + Tailwind CSS.

Real-world uses

Common Use Cases

FORUM
Forum and comment ranking
Let users rank posts, comments, or answers in a Q&A or community forum with familiar Reddit-style voting arrows.
Learn dataset-driven toggle state
Study how storing base count and user vote separately on the DOM element itself avoids a separate JS state object entirely.
Prototype content ranking features
Drop this into a prototype for any feed, changelog, or roadmap page where users should be able to signal what matters most.
Match your brand accent colors
Swap the orange/indigo active colors for your own up/down palette and save the customized version for reuse.
Build accessible toggle-button pairs
The aria-pressed pattern here is a reusable reference for any pair of mutually exclusive toggle buttons elsewhere in your UI.
Connect to a real voting API
Extend the vote function to POST the new direction to your backend and reconcile the returned authoritative count with the optimistic UI update.

Got questions?

Frequently Asked Questions

The displayed count is always recalculated as baseCount + userVote, where baseCount never changes and userVote is only ever 1, -1, or 0. Because the count is derived fresh each time rather than incremented in place, canceling a vote always returns to the exact original number.

The ternary in vote() sets userVote directly to the new direction whenever it differs from the current one, so a single click flips from +1 to -1 (or vice versa) — the count moves by 2 relative to the base in one click, matching real vote-flipping behavior.

Add a fetch call inside vote() that sends the widget's id and the new userVote value to your API. You can update the DOM optimistically as shown, then reconcile with the server's authoritative count in the fetch response if they ever disagree.

Yes. Hide the .vote-count element with display: none and keep only the two buttons — the same toggle logic works for a simple like/dislike pair without displaying a running total.

Yes. Both arrows are real button elements with aria-pressed and a visible focus-visible outline, so they can be reached with Tab and activated with Enter or Space like any native button.

Yes. Because all state is stored on each .vote element's own dataset rather than in a global variable, any number of independent widgets can coexist on one page without interfering with each other.

Add a small formatCount helper that divides by 1000 and appends "k" above a threshold, then call it when setting countEl.textContent instead of writing the raw number directly.

Yes. Add a disabled attribute to both buttons when there is no logged-in user, and check for it at the top of vote() to bail out early, optionally redirecting to a login prompt instead.