# Profile Page

> A React user profile screen: cover band, avatar with presence, bio and links, follow and message, counters, tabs and an edit profile sheet.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add profile-page`
- Docs: https://wingo-ui.com/components/profile-page

## Interaction

The header shows the cover band, the avatar with a presence dot (online, away, busy, or "Seen 2 hours ago" while offline), the name with a verified mark, the handle, a headline, the bio, the location, links that open in a new tab and the join date. Follow flips to Following at once with a popping check and adds one to Followers; if onFollow rejects it flips back and a screen reader hears why. Message calls onMessage. The Projects counter opens the Projects tab; Followers and Following are buttons when onStatClick is set. Three tabs (arrow keys move between them, Home and End jump): Overview with the pinned project cards, a year of contributions as a heatmap (hover or focus a day for its count) and the latest activity with View all; Projects with every project as a link card (language, stars, last update, a Private badge); Activity as a timeline grouped by day. On the owner's profile Edit profile opens a sheet from the right (a bottom sheet with a drag handle on phones): change the photo (crop, upload progress, remove), the name, the username, a bio with a 160 character counter, the location and the website. Save checks the fields first: a missing name, a username that is not 3 to 20 letters, numbers or underscores, or a website that is not an address turn red and the first one takes focus; the messages then follow your typing. While onSave runs, Save shows a spinner and the sheet stays open; a rejection keeps the draft and shows its message, a success updates the header and closes the sheet. Escape or Cancel closes without saving. On phones the header stacks, the counters become a row of three tiles, the actions fill the width, and the tab pages follow a horizontal swipe.

## Installation

```bash
npx wingo-ui@latest add profile-page
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/profile-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/profile-page.tsx`
- `components/blocks/profile-page-data.ts`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Avatar Upload](https://wingo-ui.com/components/avatar-upload) ([Markdown](https://wingo-ui.com/components/avatar-upload.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Card](https://wingo-ui.com/components/card) ([Markdown](https://wingo-ui.com/components/card.md))
- [GitHub Activity](https://wingo-ui.com/components/github-activity) ([Markdown](https://wingo-ui.com/components/github-activity.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Presence](https://wingo-ui.com/components/presence) ([Markdown](https://wingo-ui.com/components/presence.md))
- [Sheet](https://wingo-ui.com/components/sheet) ([Markdown](https://wingo-ui.com/components/sheet.md))
- [Swipeable Tabs](https://wingo-ui.com/components/swipeable-tabs) ([Markdown](https://wingo-ui.com/components/swipeable-tabs.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
// app/(app)/u/[handle]/page.tsx
"use client"

import { ProfilePage } from "@/components/blocks/profile-page"
import { PROFILE_PAGE_SAMPLE } from "@/components/blocks/profile-page-data"

export default function ProfileRoute() {
  return (
    <ProfilePage
      user={PROFILE_PAGE_SAMPLE.user}
      projects={PROFILE_PAGE_SAMPLE.projects}
      isOwner
      onFollow={async (following) => {
        await fetch("/api/follow", { method: following ? "POST" : "DELETE" })
      }}
      onSave={async (values) => {
        const res = await fetch("/api/profile", { method: "PUT", body: JSON.stringify(values) })
        if (!res.ok) throw new Error("We could not save your profile. Try again.")
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `user?` | `ProfileUser` | `"PROFILE_PAGE_SAMPLE.user"` | The person: { id, name, handle, avatar?, cover?, coverColor?, headline?, bio?, location?, website?, links?, joinedAt?, status?, lastSeen?, verified?, followers, following, projectCount?, isFollowing? }. Default: Emma Carter (PROFILE_PAGE_SAMPLE.user). |
| `projects?` | `ProfileProject[]` | `"PROFILE_PAGE_SAMPLE.projects"` | Project cards: { id, name, description?, href?, language?, languageColor?, stars?, updatedAt?, pinned?, visibility? }. Default: six sample projects. |
| `activity?` | `ProfileActivity[]` | `"PROFILE_PAGE_SAMPLE.activity"` | Timeline rows: { id, type: commit \| release \| comment \| star \| project \| follow \| review, title, description?, time, href? }. |
| `contributions?` | `ProfileContribution[]` | `"PROFILE_PAGE_SAMPLE.contributions"` | A year of { date, count, level } days for the heatmap, starting on a Sunday; [] hides it. Default: a seeded sample year. |
| `isOwner?` | `boolean` | `false` | The viewer's own profile: Edit profile replaces Follow and Message. |
| `tab?` | `"overview" \| "projects" \| "activity"` |  | The open tab, controlled; pair it with onTabChange (a route per tab). |
| `defaultTab?` | `"overview" \| "projects" \| "activity"` | `"overview"` | The tab open on mount. |
| `onTabChange?` | `(tab: ProfileTab) => void` |  | Called when another tab opens, by a press, the keyboard, a swipe or the Projects counter. |
| `following?` | `boolean` |  | Whether the viewer follows this person, controlled; pair it with onFollow. |
| `defaultFollowing?` | `boolean` | `false` | Following on mount while uncontrolled; user.isFollowing turns it on too. |
| `onFollow?` | `(following: boolean) => void \| Promise<unknown>` |  | Runs after Follow flips (optimistic); a rejection flips it back and announces labels.followFailed. |
| `onMessage?` | `() => void` |  | Adds a Message button next to Follow. |
| `onStatClick?` | `(stat: "followers" \| "following") => void` |  | Turns the Followers and Following counters into buttons (open a list, a route). The Projects counter always opens the Projects tab. |
| `editOpen?` | `boolean` |  | The edit sheet, controlled; pair it with onEditOpenChange. Every opening starts from the current profile. |
| `defaultEditOpen?` | `boolean` | `false` | Opens the edit sheet on mount. |
| `onEditOpenChange?` | `(open: boolean) => void` |  | Called when the edit sheet opens or closes. |
| `onSave?` | `(values: ProfileDraft) => void \| Promise<unknown>` |  | Saves { name, handle, bio, location, website, avatar } after the checks pass (website with https:// added). A promise keeps Save loading; a rejection keeps the sheet open with its message. |
| `onAvatarUpload?` | `(blob: Blob, context: { file; signal; onProgress }) => Promise<string \| void>` |  | Uploads the cropped photo from the edit sheet; resolve the new url. Without it the photo is a local preview until Save. |
| `pinnedCount?` | `number` | `4` | Project cards on Overview: pinned ones first, then the rest in order; 0 hides the section. |
| `recentCount?` | `number` | `3` | Activity rows on Overview, newest first; 0 hides the section. |
| `showHeatmap?` | `boolean` | `true` | The contributions heatmap on Overview. |
| `heatmapColor?` | `string` | `"#39d353"` | Any CSS color for the heatmap cells; busier days are more opaque. |
| `swipeable?` | `boolean` | `true` | Below 768px the tab pages follow a horizontal swipe. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Follow, Save and the tab indicator. |
| `radius?` | `"sm" \| "md" \| "lg"` | `"md"` | Corner size of the header, the cards and the panels (12 / 16 / 24px). |
| `loading?` | `boolean` | `false` | Skeletons in the real layout while the profile loads. |
| `animateIn?` | `boolean` | `true` | The header parts rise in one after another when the page mounts on the client; never while hydrating or under reduced motion. |
| `locale?` | `string` | `""` | Intl locale for the counters, the join date and relative times. Empty falls back to the UIProvider locale, then en-US (the demo offers en-GB, de-DE and ro-RO). |
| `now?` | `Date \| number` |  | The reference time for "Updated 5 hours ago", "Seen 2 hours ago" and the activity times; default the live clock. |
| `labels?` | `Partial<ProfilePageLabels>` |  | Every visible string, merged over the English DEFAULT_PROFILE_PAGE_LABELS; avatarUpload carries the photo picker's strings. |
| `className?` | `string` |  | Classes for the root. |
| `classNames?` | `Partial<Record<"header" \| "cover" \| "avatar" \| "identity" \| "name" \| "bio" \| "meta" \| "stats" \| "stat" \| "actions" \| "tabs" \| "overview" \| "projects" \| "activity" \| "projectCard" \| "heatmap" \| "timeline" \| "sheet", string>>` |  | Classes for the inner parts. |

## Parts

### DEFAULT_PROFILE_PAGE_LABELS (constant)

Every English string of the page. Spread it and override what you translate.

### PROFILE_PAGE_SAMPLE (constant)

The demo data the page renders without props: { user, projects, activity, contributions }.

### PROFILE_SAMPLE_USER (constant)

Emma Carter, a design engineer in Brooklyn, New York, online, with 1,284 followers.

### PROFILE_SAMPLE_PROJECTS (constant)

Six projects, four of them pinned, one private.

### PROFILE_SAMPLE_ACTIVITY (constant)

Seven activity rows: a release, commits, a review, a star, a comment, a new project and a follow.

### PROFILE_SAMPLE_CONTRIBUTIONS (constant)

A seeded year of contribution days ending on PROFILE_PAGE_NOW.

### PROFILE_PAGE_NOW (constant)

The fixed clock of the sample (Nov 5, 2026, 2:40 PM UTC); pass it as now so screenshots never drift.

### generateContributions (function)

Seeded demo contribution days, the same on every render. For demos and tests; pass your real calendar in production.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `end?` | `Date \| number` | `"PROFILE_PAGE_NOW"` | The last day. |
| `weeks?` | `number` | `53` | How many weeks back, rounded to a Sunday. |
| `seed?` | `number` | `2026` | The random seed; the same seed gives the same days. |
| `returns?` | `ProfileContribution[]` |  | The days, oldest first. |

### ProfileUser (type)

The person shown in the header.

### ProfileProject (type)

One project card.

### ProfileActivity (type)

One activity row.

### ProfileContribution (type)

One heatmap day: { date: "2026-11-05", count, level: 0 to 4 }.

### ProfileDraft (type)

What onSave receives: { name, handle, bio, location, website, avatar }.

### ProfilePageLabels (type)

The shape of labels.

## Examples

- **Your own profile**: isOwner swaps Follow for Edit profile; save to see the header update.
- **Edit sheet open**: Photo, name, username, bio, location and website, checked on Save. A bottom sheet on phones.
- **Save that fails**: onSave rejects once: the sheet stays open with the message and the draft.
- **Offline, photo cover, brand**: A cover photo, Seen 2 hours ago, 18.4K followers and the brand tone.
- **New member**: No projects, activity or heatmap yet: empty states instead of blank space.
- **Follow that fails**: Follow flips at once and flips back when onFollow rejects.
- **Projects tab, large corners**: defaultTab="projects" with radius="lg".
- **Loading**: Skeletons in the real layout.

## See also

- [Settings Page](https://wingo-ui.com/components/settings-page) ([Markdown](https://wingo-ui.com/components/settings-page.md))
- [Avatar Upload](https://wingo-ui.com/components/avatar-upload) ([Markdown](https://wingo-ui.com/components/avatar-upload.md))
- [GitHub Activity](https://wingo-ui.com/components/github-activity) ([Markdown](https://wingo-ui.com/components/github-activity.md))
- [Swipeable Tabs](https://wingo-ui.com/components/swipeable-tabs) ([Markdown](https://wingo-ui.com/components/swipeable-tabs.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))

---

Canonical: https://wingo-ui.com/components/profile-page
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
