caliper uidocs

Profile page

Block

A person's page: who they are and where, their local time, pinned work, a year of activity and what they did lately. Following updates at once and rolls back if it fails.

Installs components/ui/profile-page.tsxDepends on @radix-ui/react-slot, @radix-ui/react-tabs, d3-shape, lucide-reactMotion shared motion tokens
pnpm dlx shadcn@latest add https://caliperui.site/r/profile-page.json

Amara Okafor

@amara · Design engineer at Halden

I build the close review screens and the component library behind them. Previously at a freight start-up. Write to me about tables, keyboard shortcuts or Lisbon coffee.

Followers
1,284
Following
211
Projects
6

Pinned

Contributions in the last year
Mon
Wed
Fri
Sun
1,129 contributions · 233 active days
LessMore

Contributions in the last year
DateValue
2025-10-135
2025-10-147
2025-10-1510
2025-10-167
2025-10-179
2025-10-180
2025-10-190
2025-10-2011
2025-10-218
2025-10-2210
2025-10-237
2025-10-249
2025-10-250
2025-10-260
2025-10-2710
2025-10-287
2025-10-299
2025-10-3011
2025-10-318
2025-11-010
2025-11-020
2025-11-038
2025-11-0410
2025-11-056
2025-11-068
2025-11-074
2025-11-080
2025-11-090
2025-11-104
2025-11-116
2025-11-122
2025-11-134
2025-11-145
2025-11-152
2025-11-160
2025-11-170
2025-11-182
2025-11-193
2025-11-200
2025-11-212
2025-11-220
2025-11-230
2025-11-242
2025-11-250
2025-11-261
2025-11-270
2025-11-280
2025-11-290
2025-11-300
2025-12-011
2025-12-020
2025-12-030
2025-12-043
2025-12-050
2025-12-060
2025-12-072
2025-12-082
2025-12-094
2025-12-101
2025-12-113
2025-12-121
2025-12-130
2025-12-140
2025-12-152
2025-12-164
2025-12-172
2025-12-184
2025-12-196
2025-12-200
2025-12-210
2025-12-222
2025-12-234
2025-12-246
2025-12-253
2025-12-265
2025-12-270
2025-12-280
2025-12-295
2025-12-302
2025-12-314
2026-01-011
2026-01-022
2026-01-030
2026-01-040
2026-01-053
2026-01-060
2026-01-071
2026-01-083
2026-01-090
2026-01-100
2026-01-110
2026-01-120
2026-01-132
2026-01-140
2026-01-151
2026-01-160
2026-01-170
2026-01-180
2026-01-190
2026-01-201
2026-01-210
2026-01-220
2026-01-232
2026-01-240
2026-01-250
2026-01-260
2026-01-272
2026-01-284
2026-01-291
2026-01-304
2026-01-312
2026-02-010
2026-02-026
2026-02-034
2026-02-046
2026-02-053
2026-02-066
2026-02-070
2026-02-080
2026-02-098
2026-02-106
2026-02-118
2026-02-1210
2026-02-137
2026-02-140
2026-02-150
2026-02-169
2026-02-1711
2026-02-188
2026-02-1910
2026-02-207
2026-02-210
2026-02-222
2026-02-238
2026-02-2410
2026-02-257
2026-02-268
2026-02-2710
2026-02-280
2026-03-010
2026-03-025
2026-03-037
2026-03-049
2026-03-055
2026-03-067
2026-03-070
2026-03-080
2026-03-097
2026-03-104
2026-03-116
2026-03-122
2026-03-134
2026-03-140
2026-03-150
2026-03-165
2026-03-172
2026-03-184
2026-03-196
2026-03-203
2026-03-210
2026-03-220
2026-03-234
2026-03-246
2026-03-253
2026-03-265
2026-03-272
2026-03-280
2026-03-290
2026-03-304
2026-03-316
2026-04-013
2026-04-026
2026-04-038
2026-04-040
2026-04-050
2026-04-064
2026-04-077
2026-04-089
2026-04-096
2026-04-108
2026-04-110
2026-04-120
2026-04-139
2026-04-146
2026-04-158
2026-04-164
2026-04-176
2026-04-182
2026-04-190
2026-04-206
2026-04-213
2026-04-225
2026-04-236
2026-04-243
2026-04-250
2026-04-260
2026-04-273
2026-04-284
2026-04-291
2026-04-302
2026-05-010
2026-05-020
2026-05-030
2026-05-040
2026-05-051
2026-05-060
2026-05-070
2026-05-081
2026-05-090
2026-05-102
2026-05-110
2026-05-120
2026-05-130
2026-05-140
2026-05-150
2026-05-160
2026-05-170
2026-05-180
2026-05-190
2026-05-200
2026-05-210
2026-05-220
2026-05-230
2026-05-240
2026-05-251
2026-05-260
2026-05-271
2026-05-284
2026-05-291
2026-05-300
2026-05-310
2026-06-013
2026-06-025
2026-06-033
2026-06-045
2026-06-052
2026-06-060
2026-06-070
2026-06-084
2026-06-096
2026-06-103
2026-06-116
2026-06-128
2026-06-130
2026-06-140
2026-06-154
2026-06-166
2026-06-177
2026-06-184
2026-06-196
2026-06-200
2026-06-210
2026-06-227
2026-06-234
2026-06-245
2026-06-252
2026-06-264
2026-06-270
2026-06-280
2026-06-294
2026-06-301
2026-07-013
2026-07-025
2026-07-032
2026-07-042
2026-07-050
2026-07-063
2026-07-075
2026-07-082
2026-07-094
2026-07-101
2026-07-110
2026-07-120
2026-07-133
2026-07-145
2026-07-152
2026-07-164
2026-07-177
2026-07-180
2026-07-190
2026-07-204
2026-07-216
2026-07-229
2026-07-236
2026-07-248
2026-07-250
2026-07-262
2026-07-2710
2026-07-288
2026-07-2910
2026-07-307
2026-07-319
2026-08-010
2026-08-020
2026-08-0311
2026-08-048
2026-08-0510
2026-08-0612
2026-08-079
2026-08-080
2026-08-090
2026-08-109
2026-08-1111
2026-08-128
2026-08-139
2026-08-146
2026-08-150
2026-08-160
2026-08-176
2026-08-188
2026-08-194
2026-08-206
2026-08-217
2026-08-220
2026-08-230
2026-08-242
2026-08-254
2026-08-265
2026-08-272
2026-08-283
2026-08-290
2026-08-300
2026-08-314
2026-09-010
2026-09-022
2026-09-030
2026-09-041
2026-09-050
2026-09-060
2026-09-072
2026-09-080
2026-09-091
2026-09-103
2026-09-110
2026-09-120
2026-09-130
2026-09-142
2026-09-154
2026-09-161
2026-09-173
2026-09-180
2026-09-192
2026-09-200
2026-09-212
2026-09-224
2026-09-231
2026-09-244
2026-09-256
2026-09-260
2026-09-270
2026-09-282
2026-09-294
2026-09-306
2026-10-013
2026-10-025
2026-10-030
2026-10-040
2026-10-056
2026-10-063
2026-10-074
2026-10-081
2026-10-093
2026-10-100
2026-10-112

Follow, then unfollow: the demo’s unfollow fails, and the count is put back.

When to use #

Use it for

  • Community, marketplace and team directory profiles.
  • The signed-in person's own profile, with Edit profile.

Skip it when

  • A list of many people; use the team directory.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/profile-page.json

Adds its 10 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { useState } from "react"
import { ProfilePage } from "@/components/ui/profile-page"
import { SegmentedControl } from "@/components/ui/segmented-control"
import { type ActivityDay } from "@/components/ui/activity-heatmap"

const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

function contributions(): ActivityDay[] {
  const start = Date.UTC(2025, 9, 13);
  return Array.from({ length: 364 }, (_, i) => {
    const date = new Date(start + i * 86_400_000).toISOString().slice(0, 10);
    const weekday = new Date(start + i * 86_400_000).getUTCDay();
    const wave = Math.sin(i / 9) + Math.cos(i / 23);
    const value = weekday === 0 || weekday === 6 ? (i % 11 === 0 ? 2 : 0) : Math.max(0, Math.round(3 + wave * 3 + ((i * 7) % 5) - 1));
    return { date, value };
  });
}

export function ProfilePageDemo() {
  const [self, setSelf] = useState(false);
  const [data] = useState(contributions);
  return (
    <div className="grid w-full max-w-4xl gap-4">
      <SegmentedControl className="w-fit" label="View as" value={self ? "self" : "visitor"} onValueChange={(value) => setSelf(value === "self")} options={[{ value: "visitor", label: "Visitor" }, { value: "self", label: "Owner" }]} />
      <ProfilePage
        key={String(self)}
        name="Amara Okafor"
        handle="amara"
        role="Design engineer at Halden"
        bio="I build the close review screens and the component library behind them. Previously at a freight start-up. Write to me about tables, keyboard shortcuts or Lisbon coffee."
        location="Lisbon, Portugal"
        timeZone="Europe/Lisbon"
        links={[{ label: "amara.design", href: "#site" }, { label: "Mastodon", href: "#mastodon" }]}
        followers={1284}
        stats={[{ label: "Following", value: 211 }, { label: "Projects", value: 6 }]}
        self={self}
        onFollowChange={async (next) => {
          await wait(500);
          if (!next) throw new Error("demo: unfollow fails once");
        }}
        onMessage={() => {}}
        onEdit={() => {}}
        projects={[
          { id: "1", name: "close-review", description: "The review queue for the first morning of the close: keys, undo and bulk approve.", tag: "TypeScript", stars: 342, updated: "2 days ago", href: "#p1" },
          { id: "2", name: "ledger-grid", description: "A virtualised ledger with frozen columns and inline matching.", tag: "React", stars: 1290, updated: "last week", href: "#p2" },
          { id: "3", name: "fx-rounding", description: "How Halden decides when a difference is just rounding.", tag: "Notes", stars: 58, href: "#p3" },
          { id: "4", name: "shortcut-sheet", description: "Every keyboard shortcut in the app, printable on one page.", tag: "Docs", stars: 77, href: "#p4" },
          { id: "5", name: "bank-icons", description: "Monochrome marks for 300 European banks.", tag: "SVG", stars: 401, href: "#p5" },
        ]}
        activity={[
          { id: "a1", day: "Today", time: "09:42", text: "Merged “Keep the review cursor after undo” in close-review" },
          { id: "a2", day: "Today", time: "08:15", text: "Commented on “Frozen columns jump on resize”" },
          { id: "a3", day: "Yesterday", time: "17:03", text: "Released ledger-grid 2.4.0" },
          { id: "a4", day: "Yesterday", time: "11:30", text: "Opened “Announce row counts after filtering”" },
          { id: "a5", day: "Monday", time: "14:22", text: "Starred bank-icons" },
        ]}
        contributions={data}
      />
      <p className="text-[13px] text-muted-foreground">Follow, then unfollow: the demo’s unfollow fails, and the count is put back.</p>
    </div>
  );
}

API reference #

ProfilePage

component
PropTypeDefaultDescription
name *string—
handle *string—Without the
avatarSrcstring—
rolestring—
biostring—
locationstring—
timeZonestring—IANA time zone, e.g. "Europe/Lisbon"; shows their local time.
linksreadonly { label: string; href: string; }[][]
statsreadonly { label: string; value: number; }[][]Extra figures beside followers, e.g. [{ label: "Projects", value: 14 }].
followers *number—
followingbooleanfalse
selfbooleanfalseYour own profile: Edit profile instead of Follow and Message.
onFollowChange((following: boolean) => void | Promise<void>)—Called with the new state. May return a promise; a thrown error puts the state back.
onMessage(() => void)—
onEdit(() => void)—
projectsreadonly ProfileProject[][]
activityreadonly ProfileActivity[][]Newest first; `day` groups the list, e.g. "Today".
contributionsreadonly ActivityDay[]—Daily counts for the contribution heatmap.
classNamestring—

Accessibility #

It exposes status semantics, with aria-hidden, aria-pressed, aria-label and aria-labelledby kept in step with state. Changes that matter are announced through a live region.

Keyboard and focus handling comes from Radix tabs: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.

Motion #

Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.