Feature voting
ProA 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.
- A public feature request page where customers vote and suggest ideas.
- An in-app Give feedback panel that shows what is planned and shipped.
- A customer portal that needs to deflect duplicate requests before they are posted.
- A full roadmap with columns per stage: use a kanban style board.
- One off feedback on a single page or screen: use a short feedback form.
- Release notes of what already shipped: use changelog-feed.
Installation
Pro source and install commands unlock with a Pro plan.
Usage
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.
import { FeatureVoting } from "@/components/arc/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
3 parts. The first is the root.
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.
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.
- Tab
- Moves through Suggest a feature, the sort control, then each vote button (and title, when onOpenRequest is set).
- EnterorSpace
- Votes or removes your vote on the focused request. Focusing a vote button with the keyboard shows its voters.
- Arrow keys
- Move between Top, New and Trending inside the sort control.
- Enter
- In the title field, posts the request. Cmd or Ctrl + Enter posts from the details field.
- Escape
- Closes the composer and returns focus to Suggest a feature.
- Vote buttons are toggle buttons (aria-pressed) labelled with the request and its count, for example Vote for SAML single sign-on with Okta, 351 votes.
- Statuses are always an icon plus a word, never color alone.
- Every vote, duplicate vote and new post is announced through a polite status region; simulated actions say Simulated, nothing was sent.
- The similar requests list is linked to the title field with aria-describedby, and each Vote button names the request it votes for.
- The voters card is decorative (aria-hidden); the same count is in the vote button's label.
- Counts roll only the digits that change, up for a vote and down for an unvote, in tabular numerals so widths never shift.
- Sorting moves rows with one 240ms bounce free spring on transform. Rows never reorder on a live vote, so nothing moves under the pointer.
- The composer, the similar requests list and a newly posted row open by animating their measured height; everything below glides instead of jumping.
- The vote chevron nudges up 3px once when you vote. A new or chosen row gets a brief accent wash.
- Footer messages crossfade. With reduced motion, rolls, moves and folds cut to their end state and live votes stop.
- One column card from 320px up. Below 440px the button reads Suggest and padding tightens; below 360px the integration tag hides.
- Titles and the footer truncate with an ellipsis; row height never changes.
- The voters card appears on hover with a mouse and on keyboard focus; touch skips it.
- Renders every request; paginate or virtualize past about 50 rows.
- Duplicate detection runs locally on each keystroke and is linear in the number of requests.
- The live vote timer pauses when the card is off screen or the tab is hidden.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for a public or in-app feature request board, roadmap feedback widget or customer voice page.
- Wire onVote and onSubmit to your API; both are optimistic and roll back on rejection.
- Pass findSimilar backed by your search index or embeddings to catch duplicates across thousands of requests.
- Keep the card between 360 and 760px; for a full kanban roadmap with columns, use a dedicated roadmap layout instead.
- Set disabled for signed out visitors and render your own sign in prompt next to the card.
The full library index for assistants is at /llms.txt.