# Portfolio Page

> A React portfolio template for designers and developers: hero, case studies in a sheet, experience, priced services, writing and contact.

- Type: React template (Templates)
- 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 portfolio-page`
- Docs: https://wingo-ui.com/templates/portfolio-page

## Interaction

Start a project and Hire me glide down to the contact band and put the cursor in the name field; See my work glides to the projects (instantly under reduced motion). Every project card is one button: it lifts on hover and opens the case study in a sheet from the right (a bottom sheet you can swipe down on phones) with the problem, the numbered process, outcome numbers and a swipeable image strip that arrow keys scroll once it has focus. The arrow buttons in the sheet move to the previous or next project, Escape closes it and focus returns to the card, and Start a similar project closes it and jumps to the form. Book this on a service picks that service in the form. Copy email copies the address and says Copied. The form checks the name, the email and the message, focuses the first problem, spins on Send message and then thanks the sender. On phones a Hire me bar slides up once the hero scrolls away and hides again at the contact band.

## Installation

```bash
npx wingo-ui@latest add portfolio-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/portfolio-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/templates/portfolio-page.tsx`
- `components/templates/portfolio-page-data.ts`

npm dependencies: `npm i lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Marketing Section](https://wingo-ui.com/components/marketing-section) ([Markdown](https://wingo-ui.com/components/marketing-section.md))
- [Marketing Header](https://wingo-ui.com/components/marketing-header) ([Markdown](https://wingo-ui.com/components/marketing-header.md))
- [Marketing Footer](https://wingo-ui.com/components/marketing-footer) ([Markdown](https://wingo-ui.com/components/marketing-footer.md))
- [Timeline Section](https://wingo-ui.com/components/timeline-section) ([Markdown](https://wingo-ui.com/components/timeline-section.md))
- [Testimonials Section](https://wingo-ui.com/components/testimonials-section) ([Markdown](https://wingo-ui.com/components/testimonials-section.md))
- [Blog Cards Section](https://wingo-ui.com/components/blog-cards-section) ([Markdown](https://wingo-ui.com/components/blog-cards-section.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.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))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Radio Group](https://wingo-ui.com/components/radio-group) ([Markdown](https://wingo-ui.com/components/radio-group.md))
- [Sheet](https://wingo-ui.com/components/sheet) ([Markdown](https://wingo-ui.com/components/sheet.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.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/page.tsx
"use client"

import Link from "next/link"
import { PortfolioPage, type PortfolioContactValues } from "@/components/templates/portfolio-page"

async function sendMessage(values: PortfolioContactValues) {
  await fetch("/api/contact", { method: "POST", body: JSON.stringify(values) })
}

export default function HomeRoute() {
  return <PortfolioPage linkComponent={Link} layout="grid" onContact={sendMessage} />
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `owner?` | `PortfolioOwner` | `"PORTFOLIO_OWNER"` | { name, role, location?, avatar?, email, socials? }: the person in the header, hero, contact band, sticky bar and footer. Without an avatar url the initials show. |
| `projects?` | `PortfolioProject[]` | `"PORTFOLIO_PROJECTS"` | { slug, title, client, summary, year, role, duration?, tags, color, cover, image?, problem, process, outcome, metrics, gallery?, href? }. cover is a drawn picture (app, mobile, dashboard, brand, flow, type) tinted with color; image replaces it with a photo. |
| `experience?` | `PortfolioExperience[]` | `"PORTFOLIO_EXPERIENCE"` | { company, role, start, end?, location?, description, highlight? } roles for the timeline; a role without end is the current one and gets the Now pill. |
| `services?` | `PortfolioService[]` | `"PORTFOLIO_SERVICES"` | { id, title, description, price, unit?, duration?, features, icon?, featured? }. The featured one is ringed with a Most booked badge; the services are also the choices in the contact form. |
| `testimonials?` | `Testimonial[]` | `"PORTFOLIO_TESTIMONIALS"` | Client quotes for the testimonials grid (the TestimonialsSection shape). |
| `posts?` | `PortfolioPost[]` | `"PORTFOLIO_POSTS"` | Posts for the writing list (the BlogCardsSection post shape); they link to /blog/<slug>. |
| `postCategories?` | `Record<string, BlogCardsCategory>` | `"PORTFOLIO_POST_CATEGORIES"` | The categories the posts point to: { label, icon?, color? }. |
| `facts?` | `PortfolioFact[]` | `"PORTFOLIO_FACTS"` | { value, label } key facts under the hero actions; an empty array hides them. |
| `labels?` | `Partial<PortfolioPageLabels>` |  | Every string on the page, including the hero headline and bio, the section headings, the form and its errors; a few are functions (period, basedIn, successDescription). |
| `available?` | `boolean` | `true` | Open for work: a pulsing green pill reading Available for new projects. false shows an amber Booked through January 2027 (labels.unavailable) in the hero, the contact band and the sticky bar. |
| `layout?` | `"grid" \| "list"` | `"grid"` | The work section: grid shows two large cards per row; list shows one wide card per row with the cover beside the text, alternating sides, and two outcome numbers. Both are one column on phones. |
| `project?` | `string` |  | The slug of the open case study (controlled); "" closes the sheet. |
| `defaultProject?` | `string` | `""` | The case study open on first render (uncontrolled), e.g. northwind-planner. |
| `onProjectChange?` | `(slug: string) => void` |  | Fires with the slug when a case study opens or changes, and with "" when it closes. |
| `onContact?` | `(values: PortfolioContactValues) => void \| Promise<unknown>` |  | Runs on Send message with { name, email, projectType, message }. A returned promise keeps the button loading; a rejection keeps the form and shows labels.failed. Without it the page pretends for 900 ms, then thanks the sender. |
| `currency?` | `string` | `"USD"` | ISO currency of the service prices. |
| `locale?` | `string` | `""` | Intl locale for prices and post dates; empty uses the UIProvider's, else en-US. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Accents: the process step numbers, the featured service ring and badge, the timeline's current role and the testimonial metrics. A non-neutral tone also tints the page glow. |
| `ctaTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Hire me, Start a project, Book this on the featured service and Send message. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"lg"` | Corners of the project and service cards: 0 / 12 / 16 / 24px; the covers inside follow. |
| `background?` | `"plain" \| "soft" \| "glow" \| "aurora" \| "grid" \| "dots"` | `"glow"` | The main background: plain is the bare page color; the others tint the page and add a glow, an aurora, a grid or dots over the first screen. |
| `backgroundColor?` | `string` | `"oklch(0.62 0.14 275)"` | The tint and glow color of the main background; left at the default it follows tone when tone is not neutral. |
| `bandBackground?` | `"muted" \| "tinted" \| "gradient" \| "dots"` | `"muted"` | The secondary background of the experience, testimonials and contact bands. Cards and fields inside are lifted one step above it. |
| `bandColor?` | `string` | `"oklch(0.62 0.14 275)"` | The tint of tinted and gradient bands; left at the default it follows backgroundColor. |
| `showExperience?` | `boolean` | `true` | The experience timeline (and its link in the header). |
| `showServices?` | `boolean` | `true` | The services with starting prices; off also removes them from the form's choices. |
| `showTestimonials?` | `boolean` | `true` | The testimonials band. |
| `showWriting?` | `boolean` | `true` | The writing list. |
| `showContact?` | `boolean` | `true` | The contact band with the email and the form. Off, Hire me opens a mailto link instead. |
| `stickyBar?` | `boolean` | `true` | Phones only: a Hire me bar pinned to the bottom (above the home indicator) once the hero scrolls away, hidden again at the contact band and while a case study is open. |
| `header?` | `boolean` | `true` | The sticky header with the name, the section links and Hire me. Off when a site layout already has one. |
| `footer?` | `boolean` | `true` | The simple footer with the section links and the copyright line. |
| `headerProps?` | `Partial<MarketingHeaderProps>` |  | Anything for MarketingHeader (layout, variant, items...); wins over the page's own values. |
| `footerProps?` | `Partial<MarketingFooterProps>` |  | Anything for MarketingFooter (variant, columns, socials...). |
| `experienceProps?` | `Partial<TimelineSectionProps>` |  | Anything for the experience TimelineSection (layout, card, marker...); wins over the page's values. |
| `testimonialsProps?` | `Partial<TestimonialsSectionProps>` |  | Anything for the TestimonialsSection (layout, columns, card...). |
| `writingProps?` | `Partial<BlogCardsSectionProps>` |  | Anything for the writing BlogCardsSection (layout, limit, viewAllHref, basePath...). |
| `animateIn?` | `boolean` | `true` | The hero rises in once when the page mounts on the client; never during hydration, under reduced motion or in thumbnails. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for the header, footer and post links (next/link in an app). |
| `className?` | `string` |  | Classes for the root. |
| `classNames?` | `Partial<Record<PortfolioPageSlot, string>>` |  | Classes for header, hero, heroMedia, work, project, cover, caseStudy, experience, services, service, testimonials, writing, contact, form, stickyBar and footer. |

## Parts

### PORTFOLIO_PAGE_SAMPLE (constant)

All the sample content in one object: owner, facts, projects, experience, services, testimonials, posts and postCategories.

### PORTFOLIO_OWNER (constant)

Emma Carter, product designer in New York, with an email and three socials.

### PORTFOLIO_FACTS (constant)

Three hero facts: years, products shipped, startups.

### PORTFOLIO_PROJECTS (constant)

Four case studies for Northwind, Umbrella Health, Globex and Wayne Studio.

### PORTFOLIO_EXPERIENCE (constant)

Four roles from 2015 to today, newest first.

### PORTFOLIO_SERVICES (constant)

A design sprint, a design system (featured) and a monthly design partner retainer.

### PORTFOLIO_TESTIMONIALS (constant)

Three client quotes with ratings and metrics.

### PORTFOLIO_POSTS (constant)

Four essays from 2026.

### PORTFOLIO_POST_CATEGORIES (constant)

Process, design systems, research, data and career, with icons and colors.

### DEFAULT_PORTFOLIO_PAGE_LABELS (constant)

The English strings.

### PortfolioPageProps (type)

Props of PortfolioPage, including the attributes of the root element.

### PortfolioPageLabels (type)

Every string of the page.

### PortfolioPageLayout (type)

"grid" | "list".

### PortfolioPageRadius (type)

"none" | "sm" | "md" | "lg".

### PortfolioPageSlot (type)

The keys of classNames.

### PortfolioContactValues (type)

What onContact receives: { name, email, projectType, message }.

### PortfolioOwner (type)

{ name, role, location?, avatar?, email, socials? }.

### PortfolioSocial (type)

{ label, href, icon? }; label is the link's accessible name.

### PortfolioFact (type)

{ value, label }.

### PortfolioProject (type)

One case study, from the card fields to the problem, process, outcome, metrics and gallery.

### PortfolioCoverKind (type)

"app" | "mobile" | "dashboard" | "brand" | "flow" | "type": the drawn covers.

### PortfolioStep (type)

{ title, description }: one step of a case study's process.

### PortfolioMetric (type)

{ value, label }: one outcome number.

### PortfolioFrame (type)

{ cover, caption, image? }: one picture of the case study's image strip.

### PortfolioExperience (type)

One role of the timeline.

### PortfolioService (type)

One service card with its starting price.

### PortfolioPost (type)

A post of the writing list (BlogCardsPost).

## Examples

- **List layout**: layout="list": one wide card per project with the cover beside the text, alternating sides, and two outcome numbers on each.
- **A case study open**: defaultProject opens the Northwind case study in the sheet: problem, process, outcome numbers and the image strip. The arrows step through the projects.
- **Booked, with brand accents**: available={false} turns the pill amber (Booked through January 2027); tone and ctaTone brand on an aurora page with tinted bands.
- **A developer, work and contact only**: Liam Brooks, a design engineer in Austin: his own owner and labels, square-ish cards, no services, testimonials or writing, on a grid background.
- **When sending fails**: onContact rejects: after the spinner the form keeps everything and shows the error above Send message.
- **Inside a site layout**: No header, footer or sticky bar, on a soft page: the sections alone.

## See also

- [About Page](https://wingo-ui.com/templates/about-page) ([Markdown](https://wingo-ui.com/templates/about-page.md))
- [Contact Page](https://wingo-ui.com/templates/contact-page) ([Markdown](https://wingo-ui.com/templates/contact-page.md))
- [Timeline Section](https://wingo-ui.com/components/timeline-section) ([Markdown](https://wingo-ui.com/components/timeline-section.md))
- [Testimonials Section](https://wingo-ui.com/components/testimonials-section) ([Markdown](https://wingo-ui.com/components/testimonials-section.md))
- [Sheet](https://wingo-ui.com/components/sheet) ([Markdown](https://wingo-ui.com/components/sheet.md))

---

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