Referral card

Pro

A give-and-get referral card where people copy or share their invite link, invite friends by email as chips, and watch invites move from invited to signed up to earned, with the credit total counting up and a milestone reward unlocking.

Live · keyboard ready

Give $20, get $20

Friends get $20 off their first month. You get $20 in credit for each friend who subscribes.

northwind.app/r/emma20

Press Enter after each address

Invited
6
Signed up
4
Earned
$60
2 more paid referrals to unlock 3 months free3 of 5
  • Ryan Sullivanryan@ledgerly.com· Oct 26
    Invited
  • Chloe Nguyenchloe@parcel.so· Oct 23
    Signed up
  • Priya Shahpriya@hexwork.io· Oct 21
    Invited
  • Daniel Kimdaniel@fieldnote.studio· Oct 17
    Earned $20
  • Jasmine Brooksjasmine@lumen.dev· Oct 12
    Earned $20
  • Marcus Johnsonmarcus@tidewater.co· Oct 6
    Earned $20
  • A referral or invite program in account settings, billing or an onboarding checklist.
  • Showing a person what their invites have earned and how close they are to a bonus reward.
  • Team member invites with roles and permissions; use a members table.
  • Public leaderboards or affiliate payouts with many columns; use a table.

Installation

Pro source and install commands unlock with a Pro plan.

example.tsx
import { ReferralCard } from "@/components/arc/referral-card/referral-card"; export function Referrals({ me, referrals }) {  return (    <ReferralCard      link={`https://northwind.app/r/${me.code}`}      reward={20}      referrals={referrals}      milestone={{ goal: 5, reward: "3 months free", detail: "Applied to your next invoice" }}      onShare={(channel, url) => window.open(url, "_blank", "noopener,width=600,height=640")}      onInvite={(emails, { signal }) =>        fetch("/api/referrals/invite", { method: "POST", body: JSON.stringify({ emails }), signal }).then(response => {          if (!response.ok) throw new Error("Could not send the invites. Nothing was sent.");        })}      onMilestone={() => track("referral_milestone_unlocked")}    />  );}

API reference

2 parts. The first is the root.

ReferralCard

One referral program card. The ask on top (Give $20, get $20), the invite link in a quiet field with a Copy button, X, LinkedIn and email share buttons, and an invite by email field that turns each address into a chip. Below, the funnel as three rolling numbers (invited, signed up, earned), a segmented milestone bar with the reward it unlocks, and the list of people invited with their avatar and stage (Invited, Signed up, Earned $20) as an icon plus a word. Stage changes crossfade in place, the earned total counts up, and reaching the goal turns the bar and its line into the unlocked state.

PropTypeDefaultDescription
linkstring"https://northwind.app/r/emma20"The person's invite link. The domain shows muted and the code in full color; Copy copies the full URL.
rewardnumber20Credit each side gets, in whole currency units. Drives the heading, the Earned total and the Earned label on rows.
currencystring"USD"ISO currency code used to format the reward and the total.
titlestring"Give $20, get $20"Heading. Built from reward and currency when unset.
descriptionstring–One or two lines under the heading. Defaults to what both sides get.
referralsReferral[]sample programEveryone invited, newest first: { id, name?, email, avatar?, stage: "invited" | "signed-up" | "earned", invitedAt (ISO) }. Counts, the milestone and the list are derived from it. When the array changes the card follows it. Leave unset for the simulated demo.
milestoneReferralMilestone{ goal: 5, reward: "3 months free", detail: "Applied to your next invoice" }Paid referrals needed (goal, up to 12 segments are drawn), the reward in a few words, and an optional line shown once unlocked.
shareMessagestring–Text that goes with the link in the X post, LinkedIn share and email body.
maxInvitesnumber10Most addresses one send can hold. Extra addresses are refused with a note.
simulatebooleantrue without referralsPlays the sample program once on screen (a friend subscribes, another signs up and subscribes, the milestone unlocks) and moves the first address of each simulated send through the stages.
onCopy(link: string) => void–Called after the link is copied.
onShare(channel: "x" | "linkedin" | "email", url: string) => void–Called with the channel and a ready share URL (X post intent, LinkedIn share-offsite, or mailto). Open it with window.open. Without it, sharing is simulated and the live region says so.
onInvite(emails: string[], options: { signal: AbortSignal }) => void | Promise<unknown>–Sends the invites. Return a promise to keep Send loading; a rejection keeps the chips and shows the Error's message. On success the addresses join the top of the list as Invited. Without it, sending is simulated.
onMilestone(milestone: ReferralMilestone) => void–Called once when paid referrals reach the goal.
classNamestring–Class on the root element.

referralShareUrl

Helper export that builds the share URL for a channel, the same one onShare receives.

PropTypeDefaultDescription
channel"x" | "linkedin" | "email"–Share target.
linkstring–Invite link.
messagestring–Text for the post or email body.
Tab
Moves through Copy, the three share buttons, the email field and Send.
Enter, comma, spaceorTab
In the email field, turns the typed address into a chip.
Enter
In an empty email field, sends the chips.
Backspace
In an empty email field, removes the last chip. Each chip also has its own remove button.
Cmd/Ctrl+V
Pasting addresses separated by commas, spaces or new lines adds them all.
  • Share buttons are icon buttons with aria-labels (Post on X, Share on LinkedIn, Share by email) and tooltips; a polite live region reports what happened, including Simulated, nothing was posted.
  • The hint under the email field is a polite status tied to the input with aria-describedby; errors set aria-invalid and show an alert icon plus text, never color alone.
  • Stages are an icon plus a word. The milestone is a progressbar with aria-valuetext such as 4 of 5 paid referrals.
  • The funnel is a definition list, the people a labelled list.
  • While sending, the field turns read only instead of disabled, so keyboard focus stays put.
  • Numbers roll to their new value in 600ms with tabular numerals, so columns never shift.
  • Stage labels, the milestone line, share icons and the hint crossfade once: old out in 100ms, new in over 160ms with a 2px rise, stacked in one grid cell.
  • Milestone segments fill from the left in 240ms and turn to the success color when unlocked.
  • New invites open at the top of the list with a bounce-free height spring (0.24s) inside a fixed-height list, so the card never grows.
  • Reduced motion keeps every end state and drops the roll, rise and height travel; the sample program plays twice as fast.
  • The card is fluid up to 560px. Below a 470px card width the share buttons take their own full row and the milestone line keeps room for two lines.
  • Long emails truncate with an ellipsis while the date stays visible; chips scroll sideways inside a fixed-height field.
  • Rolling numbers are written by motion values, not React state, so a roll causes no re-renders.
  • The sample program starts when the card is 40% on screen and runs once.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Derive everything from referrals: pass the list from your backend and update it when webhooks report sign-ups and paid conversions.
  • Keep reward, currency and copy consistent with your actual program terms; the heading is built from reward.
  • Use referralShareUrl when you need the same share links elsewhere.
  • Only the first address of each simulated send moves through the stages, so the demo stays believable.

The full library index for assistants is at /llms.txt.