Lead scoring
ProA lead scoring card where inbound B2B leads are ranked by an AI fit score, each with a score ring and a one-line reason. Open a lead to see its signals and score breakdown, then push it to HubSpot or Salesforce.
- A sales inbox or SDR queue that ranks new inbound leads by fit.
- Showing why a model scored a lead, with signals and a weighted breakdown.
- Handing a qualified lead to HubSpot or Salesforce from the same view.
- Large tables of hundreds of leads with filters and bulk edits; use a data table.
- Enriching columns of company data; use enrich-column.
- Tracking the health of existing customers; use account-health.
Installation
Pro source and install commands unlock with a Pro plan.
Usage
Pass your leads newest first, each with a weighted `breakdown`, so the ring and the bars always agree (or set `score` directly). Mark a lead `scoring` while your model works on it. Wire `onPush(lead, crm, signal)` to your backend: resolve for the done state, reject for the error and retry state. `crms` picks the destinations, and `sort`/`onSortChange` control the order. `onRescore` shows a rescore button. With no props it plays a short intro once it scrolls into view: scores land, the list re-sorts by score, the top lead opens and is pushed to HubSpot. Every push in the demo is simulated.
import { LeadScoring } from "@/components/arc/blocks/lead-scoring/lead-scoring"; export function InboundLeads({ leads }: { leads: Lead[] }) { return ( <LeadScoring leads={leads} onPush={async (lead, crm, signal) => { const res = await fetch(`/api/crm/${crm}/leads`, { method: "POST", body: JSON.stringify({ id: lead.id }), signal }); if (!res.ok) throw new Error("Push failed"); }} /> );}API reference
2 parts. The first is the root.
LeadScoring
A card of inbound leads ranked by an AI fit score. Each row shows the company mark, the person, a score ring and the model's one-line reason; opening a row shows signal chips, the weighted score breakdown and a push to HubSpot or Salesforce. With no props it plays a short intro on first view with sample leads and simulated pushes.
leadsLead[]leadScoringSamplesLeads to show, newest first. Each lead has id, name, title, company, mark, received, reason, and optional signals, breakdown, score, scoring and pushedTo.titlestring"Inbound leads"Card title.descriptionReactNode–Line under the title. Defaults to the scoring model and the lead count.sort"score" | "newest"–Controlled sort. Newest keeps the array order.defaultSort"score" | "newest""newest" while auto playing, else "score"Starting sort when uncontrolled.onSortChange(sort: LeadSort) => void–Called when the sort control changes.crmsLeadCrmId[]["hubspot", "salesforce"]CRMs a lead can be pushed to. The first is the default destination; one entry hides the picker.onPush(lead: Lead, crm: LeadCrmId, signal: AbortSignal) => Promise<void>–Creates the contact and deal. Resolve for the done state, reject for the error state with a retry. The signal aborts on unmount. Without it pushes are simulated and announced as such.onRescore() => void–Shows a rescore button. Mark leads scoring while you rescore them. The sample demo rescores on its own.autoPlaybooleantrue for sample dataPlays the intro once when the card scrolls into view: scores land, the list sorts by score, the top lead opens and is pushed to the first CRM.defaultExpandedIdstring | nullnullLead open on first render.classNamestring–Class on the card.Lead
One inbound lead.
idRequiredstring–Stable id, used as the React key and for push state.nameRequiredstring–Person's name.titleRequiredstring–Job title. Hidden in the row on narrow cards.companyRequiredstring–Company name.markLeadCompanyMarkId | ReactNode–"vercel", "ramp", "linear", "notion", "retool", "figma", or your own 20px logo node. Falls back to a monogram.receivedRequiredstring–Arrival time, already formatted ("6m ago").reasonRequiredstring–The model's one-line reason for the score.signals{ kind: LeadSignalKind; label: string }[]–Chips in the open row. Kinds: pricing, team, hiring, email, demo, stack, docs, repo, security.breakdown{ id: string; label: string; value: number; weight: number }[]–Score factors, 0 to 100, with weights that add up to 1. The score is their weighted sum unless score is set.scorenumber–0 to 100. Overrides the breakdown sum.scoringboolean–Still being scored: the ring spins and the reason shimmers.pushedToLeadCrmId[]–CRMs the lead already lives in. Shows Synced in the row.- Tab
- Moves through the sort control, rescore, each lead row and the open row's controls.
- EnterorSpace
- Opens or closes the focused lead.
- ArrowDownorArrowUp
- Moves focus between lead rows.
- HomeorEnd
- Jumps to the first or last lead row.
- ArrowLeftorArrowRight
- Switches the CRM destination inside the open row.
- Each row is a button with aria-expanded and aria-controls; its label reads name, title, company, score, fit band and the reason.
- The score ring is decorative; the number and the fit band are in the row label and the breakdown list label.
- Status is never color alone: done and error states pair an icon with a short label.
- The CRM picker is a radio group with roving focus and named options.
- A polite live region announces pushes, errors and rescoring, and says when a push was simulated.
- Re-sorting glides each row to its new place with a bounce-free spring (position only, text is never scaled).
- The ring arc and number share one motion value, so they count up together in about 640ms with tabular numerals.
- Opening a row springs the measured height with no bounce; breakdown bars grow with a 30ms stagger.
- Time, synced, reason and status swaps are single crossfades in a fixed slot, so nothing changes size.
- The scoring spinner pauses off screen. Reduced motion keeps every end state and drops travel, counting and the spinner.
- Fits 360px to 640px. Below 460px the job title leaves the row, details start at the card edge and the push button fills the row.
- Names, roles and reasons truncate with an ellipsis; scores and times keep fixed slots.
- Designed for a short queue of 5 to 20 leads; every row is rendered.
- Ring counting runs on motion values without React renders.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for ranking inbound leads, trial signups or accounts by an AI fit score with a reason and a CRM handoff.
- Pass leads newest first; let the score come from a weighted breakdown so the bars and the ring always agree.
- Keep the reason to one short line of evidence (funding stage, team size, intent), not a paragraph.
- Wire onPush to your backend; never call a CRM from the client with secret keys.
The full library index for assistants is at /llms.txt.