Xzero UI
Pro library

Feature votingPro

A public feature request board where votes roll digit by digit and the button fills with the accent. Top, New and Trending reorder the rows with one smooth move. A built-in composer suggests similar requests as you type, so people vote instead of posting duplicates.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/feature-voting
Live · keyboard ready

Pass your requests (votes, comments, status, createdAt, trend, tag with an optional Linear, Okta, Slack, Notion, GitHub or Figma mark, and recent voters) and wire onVote(id, voted, signal) and onSubmit(draft, signal) to your API. Both are optimistic and roll back if the promise rejects. Swap in findSimilar to check duplicates against your search index. Set onOpenRequest to make the titles open a detail page. Use disabled for signed-out visitors and loading for placeholder rows. With no props it renders a sample board with simulated live votes.

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/feature-voting

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { FeatureVoting } from "@/components/xzero/blocks/feature-voting/feature-voting";

export function RoadmapBoard({ requests, user }) {
  return (
    <FeatureVoting
      requests={requests}
      currentUser={{ name: user.name, src: user.avatarUrl }}
      onVote={async (id, voted, signal) => {
        const response = await fetch(`/api/requests/${id}/vote`, { method: voted ? "POST" : "DELETE", signal });
        if (!response.ok) throw new Error("Vote not saved");
      }}
      onSubmit={async (draft, signal) => {
        const response = await fetch("/api/requests", { method: "POST", body: JSON.stringify(draft), signal });
        if (!response.ok) throw new Error("Request not posted");
        return response.json();
      }}
      onOpenRequest={id => router.push(`/roadmap/${id}`)}
    />
  );
}

API reference

FeatureVoting

A public feature request board in one card. Each row has a vote button whose count rolls digit by digit and fills with the accent when you vote, the request title, a status chip (Under review, Planned, In progress, Shipped) drawn as an icon plus label, the area or integration with its real mark (Linear, Okta, Slack, Notion, GitHub, Figma) and a comment count. Hovering a vote button shows a small card with the latest voters' faces and names. Top, New and Trending reorder the rows with one bounce free move; rows never reorder on their own, so nothing jumps under the pointer. Suggest a feature opens an inline composer that lists similar requests as you type, each with a Vote button so people vote instead of posting a duplicate. A footer line confirms every action, and the sample board drops in live votes from other customers while it is on screen.

PropTypeDefaultDescription
requestsFeatureRequest[]sample boardRequests on the board: { id, title, status?: "review" | "planned" | "progress" | "shipped", votes, comments, createdAt (ISO date, for New), trend? (votes in the last 7 days, for Trending), tag?: { label, mark? }, voters?: { name, src? }[] (latest first), voted? }. A new array replaces the board and re-applies the current sort.
titlestring"Feature requests"Heading of the card.
descriptionstring"Vote on what we build next. We read every request."One muted line under the heading.
defaultSort"top" | "new" | "trending""top"Sort applied on mount.
onSortChange(sort: FeatureSort) => void–Called when someone picks Top, New or Trending. Use it to persist the choice or refetch.
onVote(id: string, voted: boolean, signal: AbortSignal) => void | Promise<void>–Called after the optimistic vote or unvote. Reject to roll it back with an error line. The signal aborts when the same request is toggled again. Without it, votes are simulated and the footer says so.
onSubmit(draft: { title, details }, signal: AbortSignal) => Promise<FeatureRequest | void>–Posts a new request. Resolve with the saved request, or nothing to add the draft as Under review with your vote. Reject to keep the composer open with an error. Without it, posting is simulated (700ms).
findSimilar(query: string, requests: FeatureRequest[]) => FeatureRequest[]findSimilarRequestsDuplicate finder used while typing a title. The default ranks by shared words, prefixes and a few synonyms (sso, login, okta). Swap in a search API or embeddings lookup; the first three results show.
onOpenRequest(id: string) => void–Turns each title into a button, for opening the request's detail page or comment thread.
currentUser{ name: string; src?: string }Nathan ColeThe signed in person. Shown first, as You, in the voters card after they vote.
livebooleantrue for the sample boardDrops in a vote from another customer every few seconds while the card is visible. Off with reduced motion.
loadingbooleanfalseShows four placeholder rows at the same row height.
disabledbooleanfalseTurns voting and suggesting off, for signed out visitors. Counts and statuses stay readable.
classNamestring–Class on the root element.

findSimilarRequests

The built in duplicate finder, exported for server side checks: findSimilarRequests(query, requests, limit = 3).

No props.

sortRequests

Returns request ids in Top, New or Trending order: sortRequests(requests, sort).

No props.

Keyboard interactions

KeysAction
TabMoves through Suggest a feature, the sort control, then each vote button (and title, when onOpenRequest is set).
Enter / SpaceVotes or removes your vote on the focused request. Focusing a vote button with the keyboard shows its voters.
Arrow keysMove between Top, New and Trending inside the sort control.
EnterIn the title field, posts the request. Cmd or Ctrl + Enter posts from the details field.
EscapeCloses the composer and returns focus to Suggest a feature.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Guidance for AI tools

Blocks are complete, self-contained screens with sample data. Replace the sample data and connect the callbacks described above. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components