UI Components Medium
Poll / Voting Widget
A clean voting interface with real-time percentage visualizations and satisfying result animations.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--poll-bg: rgba(255, 255, 255, 0.04);
--poll-border: rgba(255, 255, 255, 0.08);
--poll-primary: #8b5cf6;
--poll-primary-glow: rgba(139, 92, 246, 0.25);
--poll-text: #f8fafc;
--poll-muted: #94a3b8;
--poll-surface: rgba(255, 255, 255, 0.03);
--poll-opt-border: rgba(255, 255, 255, 0.1);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: #0b1221;
padding: 1rem;
}
.poll-widget {
background: var(--poll-bg);
border: 1px solid var(--poll-border);
border-radius: 20px;
padding: 1.75rem;
max-width: 440px;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
backdrop-filter: blur(16px);
}
.poll-header {
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
#poll-question {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--poll-text);
line-height: 1.4;
flex: 1;
}
.poll-tag {
background: rgba(139, 92, 246, 0.15);
color: #a78bfa;
border: 1px solid rgba(139, 92, 246, 0.3);
font-size: 0.688rem;
font-weight: 700;
padding: 0.25rem 0.625rem;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.08em;
white-space: nowrap;
}
.poll-options {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.poll-opt {
display: flex;
align-items: center;
padding: 0.875rem 1rem;
border: 1px solid var(--poll-opt-border);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
background: var(--poll-surface);
color: var(--poll-muted);
}
.poll-opt:hover {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.35);
color: var(--poll-text);
transform: translateX(2px);
}
.poll-opt.selected {
border-color: var(--poll-primary);
background: rgba(139, 92, 246, 0.12);
color: var(--poll-text);
box-shadow: 0 0 16px var(--poll-primary-glow);
}
.poll-opt input {
margin-right: 0.875rem;
accent-color: var(--poll-primary);
width: 16px;
height: 16px;
}
.poll-opt label {
font-size: 0.938rem;
font-weight: 500;
cursor: pointer;
}
.poll-results {
display: flex;
flex-direction: column;
gap: 1.125rem;
}
.result-row {
position: relative;
}
.result-label {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--poll-muted);
}
.result-label span:last-child {
color: #a78bfa;
font-weight: 700;
}
.result-bar-bg {
height: 6px;
background: rgba(255, 255, 255, 0.06);
border-radius: 6px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.04);
}
.result-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--poll-primary), #a78bfa);
width: 0;
transition: width 1.2s cubic-bezier(0.16, 1, 0.3, 1);
border-radius: 6px;
box-shadow: 0 0 10px var(--poll-primary-glow);
}
.result-row.winner .result-bar-fill {
background: linear-gradient(90deg, #8b5cf6, #ec4899);
box-shadow: 0 0 16px rgba(236, 72, 153, 0.3);
}
.result-row.winner .result-label span:first-child {
color: var(--poll-text);
}
.poll-footer {
margin-top: 1.75rem;
display: flex;
justify-content: space-between;
align-items: center;
}
#total-votes {
font-size: 0.813rem;
color: var(--poll-muted);
}
.vote-btn {
background: linear-gradient(135deg, var(--poll-primary), #a78bfa);
color: white;
border: none;
padding: 0.6rem 1.375rem;
border-radius: 10px;
font-weight: 700;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 0 20px var(--poll-primary-glow);
}
.vote-btn:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 4px 24px var(--poll-primary-glow);
}
.vote-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
box-shadow: none;
}const pollData = {
question: "What's your favorite frontend tool?",
options: [
{ label: "React", votes: 850 },
{ label: "Vue", votes: 420 },
{ label: "Svelte", votes: 310 },
{ label: "Angular", votes: 240 },
],
};
const optionsContainer = document.getElementById("poll-options");
const resultsContainer = document.getElementById("poll-results");
const voteBtn = document.getElementById("vote-btn");
const totalVotesEl = document.getElementById("total-votes");
let selectedOption = null;
function initPoll() {
optionsContainer.innerHTML = "";
pollData.options.forEach((opt, index) => {
const div = document.createElement("div");
div.className = "poll-opt";
div.innerHTML = `
<input type="radio" name="poll" id="opt-${index}" value="${index}">
<label for="opt-${index}">${opt.label}</label>
`;
div.addEventListener("click", () => {
selectedOption = index;
document.querySelectorAll(".poll-opt").forEach((p) => p.classList.remove("selected"));
div.classList.add("selected");
div.querySelector("input").checked = true;
voteBtn.disabled = false;
});
optionsContainer.appendChild(div);
});
updateTotalVotes();
}
function updateTotalVotes() {
const total = pollData.options.reduce((sum, opt) => sum + opt.votes, 0);
totalVotesEl.textContent = `Total votes: ${total.toLocaleString()}`;
}
function castVote() {
if (selectedOption === null) return;
pollData.options[selectedOption].votes++;
showResults();
}
function showResults() {
optionsContainer.style.display = "none";
resultsContainer.style.display = "flex";
voteBtn.style.display = "none";
const total = pollData.options.reduce((sum, opt) => sum + opt.votes, 0);
resultsContainer.innerHTML = "";
pollData.options.forEach((opt) => {
const percent = Math.round((opt.votes / total) * 100);
const row = document.createElement("div");
row.className = "result-row";
row.innerHTML = `
<div class="result-label">
<span>${opt.label}</span>
<span>${percent}%</span>
</div>
<div class="result-bar-bg">
<div class="result-bar-fill" style="width: 0%"></div>
</div>
`;
resultsContainer.appendChild(row);
// Trigger animation
setTimeout(() => {
row.querySelector(".result-bar-fill").style.width = `${percent}%`;
}, 100);
});
updateTotalVotes();
}
voteBtn.addEventListener("click", castVote);
initPoll();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Poll Vote</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="poll-widget" id="poll-widget">
<div class="poll-header">
<h3 id="poll-question">What's your favorite frontend tool?</h3>
<span class="poll-tag">Active Poll</span>
</div>
<div id="poll-options" class="poll-options">
<!-- Options added via JS -->
</div>
<div id="poll-results" class="poll-results" style="display: none;">
<!-- Results added via JS -->
</div>
<div class="poll-footer">
<span id="total-votes">Total votes: 1,420</span>
<button id="vote-btn" class="vote-btn" disabled>Cast Vote</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
const POLL = {
question: "What's your favorite frontend tool?",
options: [
{ label: "React", emoji: "⚛️", votes: 850 },
{ label: "Vue", emoji: "💚", votes: 420 },
{ label: "Svelte", emoji: "🔥", votes: 310 },
{ label: "Angular", emoji: "🔴", votes: 240 },
],
};
export default function PollVoteRC() {
const [options, setOptions] = useState(POLL.options);
const [selected, setSelected] = useState<number | null>(null);
const [voted, setVoted] = useState(false);
const total = options.reduce((s, o) => s + o.votes, 0);
function vote() {
if (selected === null) return;
setOptions((prev) => prev.map((o, i) => (i === selected ? { ...o, votes: o.votes + 1 } : o)));
setVoted(true);
}
const sortedForResults = [...options]
.map((o, i) => ({ ...o, idx: i }))
.sort((a, b) => b.votes - a.votes);
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="bg-[#161b22] border border-[#30363d] rounded-2xl p-6 w-full max-w-sm">
<h2 className="text-[#e6edf3] font-bold text-base mb-1">{POLL.question}</h2>
<p className="text-[#484f58] text-xs mb-5">
{(total + (voted ? 1 : 0)).toLocaleString()} total votes
</p>
{!voted ? (
<>
<div className="space-y-2 mb-5">
{options.map((o, i) => (
<button
key={o.label}
onClick={() => setSelected(i)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl border text-sm text-left transition-all duration-200 ${
selected === i
? "bg-[#58a6ff]/10 border-[#58a6ff]/50 text-[#e6edf3]"
: "bg-[#21262d] border-[#30363d] text-[#8b949e] hover:border-[#8b949e]/40"
}`}
>
<span className="text-lg">{o.emoji}</span>
<span className="font-medium">{o.label}</span>
{selected === i && (
<span className="ml-auto w-4 h-4 rounded-full bg-[#58a6ff] flex items-center justify-center">
<svg
width="8"
height="8"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="3"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</span>
)}
</button>
))}
</div>
<button
onClick={vote}
disabled={selected === null}
className="w-full py-2.5 bg-[#238636] border border-[#2ea043] text-white rounded-xl font-semibold text-sm disabled:opacity-40 hover:bg-[#2ea043] transition-colors"
>
Vote
</button>
</>
) : (
<div className="space-y-3">
{sortedForResults.map((o, rank) => {
const pct = Math.round((o.votes / (total + 1)) * 100);
const isWinner = rank === 0;
return (
<div key={o.label}>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span>{o.emoji}</span>
<span
className={`text-sm font-medium ${isWinner ? "text-[#f1e05a]" : "text-[#8b949e]"}`}
>
{o.label}
</span>
{isWinner && <span className="text-xs">👑</span>}
</div>
<span className="text-xs font-bold text-[#e6edf3] tabular-nums">{pct}%</span>
</div>
<div className="h-2 bg-[#21262d] rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-700"
style={{ width: `${pct}%`, background: isWinner ? "#f1e05a" : "#30363d" }}
/>
</div>
<p className="text-[10px] text-[#484f58] mt-0.5">
{o.votes.toLocaleString()} votes
</p>
</div>
);
})}
<button
onClick={() => {
setVoted(false);
setSelected(null);
}}
className="w-full mt-2 py-2 text-sm text-[#484f58] hover:text-[#8b949e] transition-colors"
>
Vote again
</button>
</div>
)}
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const POLL = {
question: "What's your favorite frontend tool?",
options: [
{ label: "React", emoji: "⚛️", votes: 850 },
{ label: "Vue", emoji: "💚", votes: 420 },
{ label: "Svelte", emoji: "🔥", votes: 310 },
{ label: "Angular", emoji: "🔴", votes: 240 },
],
};
const options = ref(POLL.options.map((o) => ({ ...o })));
const selected = ref(null);
const voted = ref(false);
const total = computed(() => options.value.reduce((s, o) => s + o.votes, 0));
const sortedForResults = computed(() =>
[...options.value].map((o, i) => ({ ...o, idx: i })).sort((a, b) => b.votes - a.votes)
);
function vote() {
if (selected.value === null) return;
options.value[selected.value].votes += 1;
voted.value = true;
}
function resetVote() {
voted.value = false;
selected.value = null;
}
function getPct(votes) {
return Math.round((votes / (total.value + 1)) * 100);
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl p-6 w-full max-w-sm">
<h2 class="text-[#e6edf3] font-bold text-base mb-1">{{ POLL.question }}</h2>
<p class="text-[#484f58] text-xs mb-5">{{ (total + (voted ? 1 : 0)).toLocaleString() }} total votes</p>
<template v-if="!voted">
<div class="space-y-2 mb-5">
<button
v-for="(o, i) in options"
:key="o.label"
@click="selected = i"
:class="[
'w-full flex items-center gap-3 px-4 py-3 rounded-xl border text-sm text-left transition-all duration-200',
selected === i
? 'bg-[#58a6ff]/10 border-[#58a6ff]/50 text-[#e6edf3]'
: 'bg-[#21262d] border-[#30363d] text-[#8b949e] hover:border-[#8b949e]/40',
]"
>
<span class="text-lg">{{ o.emoji }}</span>
<span class="font-medium">{{ o.label }}</span>
<span v-if="selected === i" class="ml-auto w-4 h-4 rounded-full bg-[#58a6ff] flex items-center justify-center">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><polyline points="20 6 9 17 4 12" /></svg>
</span>
</button>
</div>
<button
@click="vote"
:disabled="selected === null"
class="w-full py-2.5 bg-[#238636] border border-[#2ea043] text-white rounded-xl font-semibold text-sm disabled:opacity-40 hover:bg-[#2ea043] transition-colors"
>
Vote
</button>
</template>
<template v-else>
<div class="space-y-3">
<div v-for="(o, rank) in sortedForResults" :key="o.label">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2">
<span>{{ o.emoji }}</span>
<span :class="['text-sm font-medium', rank === 0 ? 'text-[#f1e05a]' : 'text-[#8b949e]']">{{ o.label }}</span>
<span v-if="rank === 0" class="text-xs">👑</span>
</div>
<span class="text-xs font-bold text-[#e6edf3] tabular-nums">{{ getPct(o.votes) }}%</span>
</div>
<div class="h-2 bg-[#21262d] rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all duration-700"
:style="{ width: getPct(o.votes) + '%', background: rank === 0 ? '#f1e05a' : '#30363d' }"
/>
</div>
<p class="text-[10px] text-[#484f58] mt-0.5">{{ o.votes.toLocaleString() }} votes</p>
</div>
<button
@click="resetVote"
class="w-full mt-2 py-2 text-sm text-[#484f58] hover:text-[#8b949e] transition-colors"
>
Vote again
</button>
</div>
</template>
</div>
</div>
</template>
<style scoped>
</style><script>
const POLL = {
question: "What's your favorite frontend tool?",
options: [
{ label: "React", emoji: "⚛️", votes: 850 },
{ label: "Vue", emoji: "💚", votes: 420 },
{ label: "Svelte", emoji: "🔥", votes: 310 },
{ label: "Angular", emoji: "🔴", votes: 240 },
],
};
let options = [...POLL.options.map((o) => ({ ...o }))];
let selected = null;
let voted = false;
$: total = options.reduce((s, o) => s + o.votes, 0);
$: sortedForResults = [...options]
.map((o, i) => ({ ...o, idx: i }))
.sort((a, b) => b.votes - a.votes);
function vote() {
if (selected === null) return;
options[selected].votes += 1;
options = options;
voted = true;
}
function resetVote() {
voted = false;
selected = null;
}
</script>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl p-6 w-full max-w-sm">
<h2 class="text-[#e6edf3] font-bold text-base mb-1">{POLL.question}</h2>
<p class="text-[#484f58] text-xs mb-5">{(total + (voted ? 1 : 0)).toLocaleString()} total votes</p>
{#if !voted}
<div class="space-y-2 mb-5">
{#each options as o, i}
<button
on:click={() => (selected = i)}
class="w-full flex items-center gap-3 px-4 py-3 rounded-xl border text-sm text-left transition-all duration-200 {selected === i
? 'bg-[#58a6ff]/10 border-[#58a6ff]/50 text-[#e6edf3]'
: 'bg-[#21262d] border-[#30363d] text-[#8b949e] hover:border-[#8b949e]/40'}"
>
<span class="text-lg">{o.emoji}</span>
<span class="font-medium">{o.label}</span>
{#if selected === i}
<span class="ml-auto w-4 h-4 rounded-full bg-[#58a6ff] flex items-center justify-center">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><polyline points="20 6 9 17 4 12" /></svg>
</span>
{/if}
</button>
{/each}
</div>
<button
on:click={vote}
disabled={selected === null}
class="w-full py-2.5 bg-[#238636] border border-[#2ea043] text-white rounded-xl font-semibold text-sm disabled:opacity-40 hover:bg-[#2ea043] transition-colors"
>
Vote
</button>
{:else}
<div class="space-y-3">
{#each sortedForResults as o, rank}
{@const pctVal = Math.round((o.votes / (total + 1)) * 100)}
{@const isWinner = rank === 0}
<div>
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2">
<span>{o.emoji}</span>
<span class="text-sm font-medium {isWinner ? 'text-[#f1e05a]' : 'text-[#8b949e]'}">{o.label}</span>
{#if isWinner}<span class="text-xs">👑</span>{/if}
</div>
<span class="text-xs font-bold text-[#e6edf3] tabular-nums">{pctVal}%</span>
</div>
<div class="h-2 bg-[#21262d] rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all duration-700"
style="width: {pctVal}%; background: {isWinner ? '#f1e05a' : '#30363d'};"
/>
</div>
<p class="text-[10px] text-[#484f58] mt-0.5">{o.votes.toLocaleString()} votes</p>
</div>
{/each}
<button
on:click={resetVote}
class="w-full mt-2 py-2 text-sm text-[#484f58] hover:text-[#8b949e] transition-colors"
>
Vote again
</button>
</div>
{/if}
</div>
</div>Poll / Voting Widget
Gather user opinions with this interactive poll. It transitions from a selectable list to a percentage-based results view upon voting, using smooth CSS transitions for the progress bars.
Features
- Radio-style choice selection
- Integrated percentage calculations
- State-aware UI (Vote vs Results)
- Animated results bars
- Total vote count display
- “Poll closed” state support