Portfolio Page React template for Tailwind CSS v4
v1.0.0Added in 1.14.0
A React portfolio template for designers and developers: hero, case studies in a sheet, experience, priced services, writing and contact.
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.
Portfolio Page is a Pro React template in the React website templates category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add portfolio-page, together with Marketing Section, Marketing Header, Marketing Footer, Timeline Section, Testimonials Section and 14 more and the npm packages lucide-react and motion.
Built with
The sections this page is made of. In the full-screen preview, hold Alt to outline each one on the page.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add portfolio-pagePortfolio Page is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import PortfolioPage from @/components/templates/portfolio-page.
// 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
ownerdefault"PORTFOLIO_OWNER"PortfolioOwner{ 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.
projectsdefault"PORTFOLIO_PROJECTS"PortfolioProject[]{ 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.
experiencedefault"PORTFOLIO_EXPERIENCE"PortfolioExperience[]{ company, role, start, end?, location?, description, highlight? } roles for the timeline; a role without end is the current one and gets the Now pill.
servicesdefault"PORTFOLIO_SERVICES"PortfolioService[]{ 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.
testimonialsdefault"PORTFOLIO_TESTIMONIALS"Testimonial[]Client quotes for the testimonials grid (the TestimonialsSection shape).
postsdefault"PORTFOLIO_POSTS"PortfolioPost[]Posts for the writing list (the BlogCardsSection post shape); they link to /blog/<slug>.
postCategoriesdefault"PORTFOLIO_POST_CATEGORIES"Record<string, BlogCardsCategory>The categories the posts point to: { label, icon?, color? }.
factsdefault"PORTFOLIO_FACTS"PortfolioFact[]{ value, label } key facts under the hero actions; an empty array hides them.
labelsPartial<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).
availabledefaulttruebooleanOpen 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.
layoutdefault"grid""grid" | "list"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.
projectstringThe slug of the open case study (controlled); "" closes the sheet.
defaultProjectdefault""stringThe case study open on first render (uncontrolled), e.g. northwind-planner.
onProjectChange(slug: string) => voidFires 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.
currencydefault"USD"stringISO currency of the service prices.
localedefault""stringIntl locale for prices and post dates; empty uses the UIProvider's, else en-US.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"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.
ctaTonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Hire me, Start a project, Book this on the featured service and Send message.
radiusdefault"lg""none" | "sm" | "md" | "lg"Corners of the project and service cards: 0 / 12 / 16 / 24px; the covers inside follow.
backgrounddefault"glow""plain" | "soft" | "glow" | "aurora" | "grid" | "dots"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.
backgroundColordefault"oklch(0.62 0.14 275)"stringThe tint and glow color of the main background; left at the default it follows tone when tone is not neutral.
bandBackgrounddefault"muted""muted" | "tinted" | "gradient" | "dots"The secondary background of the experience, testimonials and contact bands. Cards and fields inside are lifted one step above it.
bandColordefault"oklch(0.62 0.14 275)"stringThe tint of tinted and gradient bands; left at the default it follows backgroundColor.
showExperiencedefaulttruebooleanThe experience timeline (and its link in the header).
showServicesdefaulttruebooleanThe services with starting prices; off also removes them from the form's choices.
showTestimonialsdefaulttruebooleanThe testimonials band.
showWritingdefaulttruebooleanThe writing list.
showContactdefaulttruebooleanThe contact band with the email and the form. Off, Hire me opens a mailto link instead.
stickyBardefaulttruebooleanPhones 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.
headerdefaulttruebooleanThe sticky header with the name, the section links and Hire me. Off when a site layout already has one.
footerdefaulttruebooleanThe simple footer with the section links and the copyright line.
headerPropsPartial<MarketingHeaderProps>Anything for MarketingHeader (layout, variant, items...); wins over the page's own values.
footerPropsPartial<MarketingFooterProps>Anything for MarketingFooter (variant, columns, socials...).
experiencePropsPartial<TimelineSectionProps>Anything for the experience TimelineSection (layout, card, marker...); wins over the page's values.
testimonialsPropsPartial<TestimonialsSectionProps>Anything for the TestimonialsSection (layout, columns, card...).
writingPropsPartial<BlogCardsSectionProps>Anything for the writing BlogCardsSection (layout, limit, viewAllHref, basePath...).
animateIndefaulttruebooleanThe hero rises in once when the page mounts on the client; never during hydration, under reduced motion or in thumbnails.
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for the header, footer and post links (next/link in an app).
classNamestringClasses for the root.
classNamesPartial<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
constantAll the sample content in one object: owner, facts, projects, experience, services, testimonials, posts and postCategories.
PORTFOLIO_OWNER
constantEmma Carter, product designer in New York, with an email and three socials.
PORTFOLIO_FACTS
constantThree hero facts: years, products shipped, startups.
PORTFOLIO_PROJECTS
constantFour case studies for Northwind, Umbrella Health, Globex and Wayne Studio.
PORTFOLIO_EXPERIENCE
constantFour roles from 2015 to today, newest first.
PORTFOLIO_SERVICES
constantA design sprint, a design system (featured) and a monthly design partner retainer.
PORTFOLIO_TESTIMONIALS
constantThree client quotes with ratings and metrics.
PORTFOLIO_POSTS
constantFour essays from 2026.
PORTFOLIO_POST_CATEGORIES
constantProcess, design systems, research, data and career, with icons and colors.
DEFAULT_PORTFOLIO_PAGE_LABELS
constantThe English strings.
PortfolioPageProps
typeProps of PortfolioPage, including the attributes of the root element.
PortfolioPageLabels
typeEvery string of the page.
PortfolioPageLayout
type"grid" | "list".
PortfolioPageRadius
type"none" | "sm" | "md" | "lg".
PortfolioPageSlot
typeThe keys of classNames.
PortfolioContactValues
typeWhat 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
typeOne 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
typeOne role of the timeline.
PortfolioService
typeOne service card with its starting price.
PortfolioPost
typeA 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.
Dependencies
Files
- components/templates/portfolio-page.tsx
- components/templates/portfolio-page-data.ts
FAQ
Is the React Portfolio Page template free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Portfolio Page?
Run npx wingo-ui@latest add portfolio-page. It also adds the Wingo UI items it builds on (Marketing Section, Marketing Header, Marketing Footer, Timeline Section, Testimonials Section and 14 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/portfolio-page.
How does Portfolio Page behave on phones?
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.
See also
Related guides
- Nextjs Agency Template That Turns Visitors Into Briefs
Build it: screens and apps8 min read
- Nextjs Portfolio Template That Wins Clients
Build it: screens and apps9 min read