Petition Signature Counter — Live Count-Up Signature Widget HTML CSS JS
Petition Signature Counter · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="psc-card">
<p class="psc-cause">Protect the Cedar Creek Wetlands</p>
<div class="psc-count" id="pscCount">0</div>
<p class="psc-label">signatures — goal: <b id="pscGoalLabel">25,000</b></p>
<div class="psc-bar-track">
<div class="psc-bar-fill" id="pscBarFill"></div>
</div>
<form class="psc-form" id="pscForm">
<input type="text" id="pscName" placeholder="Full name" required autocomplete="name">
<input type="email" id="pscEmail" placeholder="Email address" required autocomplete="email">
<button type="submit" class="psc-submit" id="pscSubmit">Sign this petition</button>
</form>
<p class="psc-confirm" id="pscConfirm" aria-live="polite"></p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#052e26;color:#ecfdf5;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:32px 20px}
.psc-card{background:#0b3d33;border:1px solid #14543f;border-radius:18px;padding:28px;width:100%;max-width:400px;text-align:center}
.psc-cause{font-size:14px;font-weight:700;color:#6ee7b7;margin-bottom:14px}
.psc-count{font-size:clamp(44px,10vw,60px);font-weight:900;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums;color:#fff}
.psc-label{font-size:13px;color:#9cd9c4;margin:6px 0 16px}
.psc-label b{color:#ecfdf5}
.psc-bar-track{height:8px;background:#0f4b3d;border-radius:4px;overflow:hidden;margin-bottom:22px}
.psc-bar-fill{height:100%;width:0%;background:linear-gradient(90deg,#34d399,#10b981);border-radius:4px;transition:width .8s cubic-bezier(.22,.9,.3,1)}
.psc-form{display:flex;flex-direction:column;gap:9px;text-align:left}
.psc-form input{border:1.5px solid #14543f;background:#083026;border-radius:10px;padding:12px 13px;font-size:13.5px;color:#ecfdf5;font-family:inherit;outline:none;transition:border-color .15s}
.psc-form input::placeholder{color:#5f9484}
.psc-form input:focus{border-color:#34d399}
.psc-submit{background:#10b981;color:#022c22;border:none;border-radius:10px;padding:13px;font-size:14.5px;font-weight:800;cursor:pointer;margin-top:2px;transition:background .15s}
.psc-submit:hover{background:#34d399}
.psc-submit:disabled{opacity:.6;cursor:default}
.psc-confirm{font-size:12.5px;color:#6ee7b7;font-weight:600;margin-top:12px;min-height:16px}var GOAL = 25000;
var count = 18742; // starting count, as if the petition is already in progress
var countEl = document.getElementById('pscCount');
var barFill = document.getElementById('pscBarFill');
var form = document.getElementById('pscForm');
var confirmEl = document.getElementById('pscConfirm');
var submitBtn = document.getElementById('pscSubmit');
function renderStatic(n) {
countEl.textContent = n.toLocaleString('en-US');
barFill.style.width = Math.min(100, (n / GOAL) * 100) + '%';
}
// Animates the counter from "from" to "to" over duration ms with an ease-out curve.
function countUp(from, to, duration) {
var start = null;
function step(ts) {
if (start === null) start = ts;
var progress = Math.min(1, (ts - start) / duration);
var eased = 1 - Math.pow(1 - progress, 3);
var current = Math.round(from + (to - from) * eased);
renderStatic(current);
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
renderStatic(count);
document.getElementById('pscGoalLabel').textContent = GOAL.toLocaleString('en-US');
form.addEventListener('submit', function (e) {
e.preventDefault();
var name = document.getElementById('pscName').value.trim();
var email = document.getElementById('pscEmail').value.trim();
if (!name || !email) return;
var newCount = count + 1;
countUp(count, newCount, 900);
count = newCount;
submitBtn.disabled = true;
submitBtn.textContent = 'Signed \u2713';
confirmEl.textContent = 'Thanks, ' + name.split(' ')[0] + ' — your signature has been added.';
form.querySelectorAll('input').forEach(function (i) { i.disabled = true; });
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Petition Signature Counter — A Count-Up Signature Widget With a Sign Form

A petition page lives or dies on momentum — a big, satisfying counter that visibly climbs when someone signs does more to encourage the next signature than any amount of persuasive copy. This snippet builds that: a large tabular-number counter, a name-and-email sign form, and a progress bar toward a stated goal, all in plain HTML, CSS, and vanilla JavaScript.
A real count-up, not an instant jump
The core interaction detail is that signing doesn't just set textContent to the new number — it runs countUp(), a requestAnimationFrame loop that eases from the old count to the new one over 900ms using a cubic ease-out. Even though a single signature only moves the number by one, the animated climb (paired with font-variant-numeric: tabular-nums so digits don't jitter sideways) is what makes the moment feel rewarding rather than a flat form-submit confirmation. For a reusable version of this pattern by itself, see count-up.
Goal-relative progress bar
Beneath the counter, a thin bar fills to count / GOAL, giving a second, complementary signal to the raw number — useful once a petition is past the point where the count alone is legible at a glance (25,000 versus 25,001 doesn't visually register, but a bar creeping toward full does).
A form that commits before it counts
Signing requires a name and email, validated with the browser's native required attributes plus a JS guard, before the counter increments — so the animation only fires on a genuine submission, and the confirmation message personalizes with the signer's first name. After signing, the form disables and the button locks to "Signed ✓" so a signer can't accidentally double-submit and inflate the count.
Accessible confirmation
The confirmation line carries aria-live="polite", so screen reader users hear the thank-you message as soon as it appears, without needing to navigate back to find it — important since the visual counter animation itself isn't announced.
Starting from a realistic in-progress count
The demo seeds count at 18,742 rather than zero, because that's how petitions actually look when someone lands on them — mid-campaign, with visible momentum already built. Swap it for zero if you're launching fresh.
Customizing it
Swap GOAL and the starting count for your real numbers, replace the demo submit handler with a call to your petition backend, and adjust the count-up duration and easing to taste. Pair it with a progress bar or live visitor counter for a companion widget.
Step by step
How to Use
- 1Paste HTML, CSS, and JSThe counter loads at 18,742 with a filled progress bar toward the 25,000 goal.
- 2Fill in name and emailBoth fields are required before the form can submit.
- 3Click "Sign this petition"The counter animates up by one with an eased count-up, and the bar advances.
- 4Read the confirmationA personalized thank-you message appears and the form locks to prevent double-signing.
- 5Set GOAL and the starting countMatch them to your real petition's numbers.
- 6Wire the submit handlerReplace the demo logic with a real API call, then trigger countUp() on success.
Real-world uses
Common Use Cases
requestAnimationFrame loop and its easing, then compare it with the simpler number ticker when no form is involved.Got questions?
Frequently Asked Questions
countUp() runs a requestAnimationFrame loop that tracks elapsed time against a fixed duration (900ms), computes a 0-to-1 progress value, applies a cubic ease-out curve to it, and renders the interpolated number on every frame until progress reaches 1. This produces a smooth climb from the old total to the new one rather than an instant text swap, which is what makes signing feel like an event.
Most fonts render digits at slightly different widths (a "1" is narrower than an "8"), which makes a rapidly changing number visually jitter left and right as digits swap. tabular-nums forces every digit to the same fixed width, so the counter's overall width stays stable and the count-up animation reads as a smooth climb rather than a wobbling one.
After a successful submission, every input in the form is disabled and the submit button is disabled and relabeled "Signed ✓", so the same browser session cannot submit again. This is a client-side UX safeguard, not a security measure — a real petition backend should also deduplicate by email or account server-side.
Replace the body of the submit handler: instead of just incrementing the local count variable, POST the name and email to your API, and on a successful response call countUp(count, newCount, 900) with the authoritative count your backend returns (which also protects against the count drifting if multiple people sign concurrently).
Keep count in component state and trigger the requestAnimationFrame loop inside an effect/lifecycle hook whenever a new target count is set (rather than re-rendering instantly), updating a separate "displayed count" state variable each frame. The form validation and confirmation logic map directly to controlled inputs and conditional rendering.