Poll Card

The PollCard is a premium engagement element designed for ForkUI. It uses Framer Motion to animate result distributions, providing instant visual feedback. Built with a minimalist monochrome aesthetic, it supports multi-state voting and percentage tracking, fitting perfectly into modern community dashboards or social feeds without corporate clutter.

framer-motionlucide-react

Community Poll

Which framework are you using for your next project?

82 TOTAL VOTES

Installation

Install the required dependencies:

npm install framer-motion lucide-react
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
components/ui/poll-card.tsx
"use client";
  import React, { useState } from "react";
  import { motion } from "framer-motion";
  import { Check } from "lucide-react";
  
  interface Option {
    id: string;
    label: string;
    votes: number;
  }
  
  export function PollCard() {
    const [voted, setVoted] = useState<string | null>(null);
    const [options, setOptions] = useState<Option[]>([
      { id: "1", label: "Next.js 15 App Router", votes: 42 },
      { id: "2", label: "Pages Router (Legacy)", votes: 12 },
      { id: "3", label: "Remix / Vite", votes: 28 },
    ]);
  
    const totalVotes = options.reduce((acc, opt) => acc + opt.votes, 0);
  
    const handleVote = (id: string) => {
      if (voted) return;
      setVoted(id);
      setOptions(options.map(opt => 
        opt.id === id ? { ...opt, votes: opt.votes + 1 } : opt
      ));
    };
  
    return (
      <div className="p-6 rounded-[2rem] border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-900/30 backdrop-blur-sm">
        <h4 className="text-sm font-black uppercase tracking-widest text-zinc-400 mb-6">
          Community Poll
        </h4>
        <p className="font-bold text-lg mb-6 dark:text-white">
          Which framework are you using for your next ForkUI project?
        </p>
  
        <div className="space-y-3">
          {options.map((option) => {
            const percentage = Math.round((option.votes / (totalVotes || 1)) * 100);
            const isSelected = voted === option.id;
  
            return (
              <button
                key={option.id}
                onClick={() => handleVote(option.id)}
                disabled={!!voted}
                className="relative w-full group overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800 p-4 transition-all active:scale-[0.98]"
              >
                {/* Progress Bar Background */}
                {voted && (
                  <motion.div
                    initial={{ width: 0 }}
                    animate={{ width: `${percentage}%` }}
                    className="absolute inset-0 bg-zinc-900/5 dark:bg-white/5 z-0"
                    transition={{ duration: 1, ease: "circOut" }}
                  />
                )}
  
                <div className="relative z-10 flex justify-between items-center">
                  <span className="font-bold text-sm dark:text-zinc-200 flex items-center gap-2">
                    {isSelected && <Check size={14} className="text-emerald-500" />}
                    {option.label}
                  </span>
                  {voted && (
                    <span className="text-xs font-black text-zinc-500">
                      {percentage}%
                    </span>
                  )}
                </div>
              </button>
            );
          })}
        </div>
  
        <div className="mt-6 pt-4 border-t border-zinc-100 dark:border-zinc-800 flex justify-between items-center">
          <span className="text-[10px] font-bold text-zinc-400">
            {totalVotes} TOTAL VOTES
          </span>
          {voted && <span className="text-[10px] font-bold text-emerald-500 uppercase tracking-tighter">Vote Registered</span>}
        </div>
      </div>
    );
  }