Folio
Minimal personal portfolio with rotating role, project list and experience timeline.
app/page.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { ArrowUpRight, Github, Linkedin, Mail, MapPin } from "lucide-react";
import { useEffect, useState } from "react";
const ease = [0.22, 1, 0.36, 1] as const;
const up = {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.3 },
transition: { duration: 0.7, ease },
};
const roles = ["design engineer", "frontend developer", "interface craftsman"];
function Role() {
const [i, setI] = useState(0);
useEffect(() => {
const id = setInterval(() => setI((v) => (v + 1) % roles.length), 2400);
return () => clearInterval(id);
}, []);
return (
<span className="relative inline-flex overflow-hidden align-bottom">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={roles[i]}
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: "-100%", opacity: 0 }}
transition={{ duration: 0.5, ease }}
className="text-sky-600"
>
{roles[i]}
</motion.span>
</AnimatePresence>
</span>
);
}
const projects = [
{ name: "Orbit Design System", desc: "A token-driven component library used by 40 product teams.", tags: ["React", "Tokens", "Storybook"], year: "2026" },
{ name: "Canvas Editor", desc: "Real-time collaborative whiteboard with multiplayer cursors.", tags: ["WebSockets", "Canvas", "CRDT"], year: "2025" },
{ name: "Atlas Dashboard", desc: "Analytics UI rendering 1M data points at 60fps.", tags: ["WebGL", "D3", "TypeScript"], year: "2025" },
{ name: "Motion Primitives", desc: "Open-source animation helpers with 6k GitHub stars.", tags: ["Open source", "Motion"], year: "2024" },
];
const experience = [
{ company: "Lumora", role: "Senior Design Engineer", period: "2024 — Now" },
{ company: "Stackline", role: "Frontend Engineer", period: "2021 — 2024" },
{ company: "Northwave", role: "UI Developer", period: "2019 — 2021" },
];
export default function FolioTemplate() {
return (
<div className="min-h-screen bg-white text-zinc-900 antialiased">
<div className="mx-auto max-w-3xl px-6">
<header className="flex items-center justify-between py-8">
<span className="font-semibold">Sam Rivera</span>
<nav className="flex gap-6 text-sm text-zinc-500">
<a href="#work" className="hover:text-zinc-900">Work</a>
<a href="#about" className="hover:text-zinc-900">About</a>
<a href="#contact" className="hover:text-zinc-900">Contact</a>
</nav>
</header>
<section className="py-20">
<motion.div initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.6, ease }} className="grid size-16 place-items-center rounded-2xl bg-gradient-to-br from-sky-400 to-indigo-600 text-xl font-semibold text-white shadow-lg shadow-sky-600/20">
SR
</motion.div>
<motion.h1 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, delay: 0.1, ease }} className="mt-8 text-4xl font-semibold leading-tight tracking-tight sm:text-5xl">
Hi, I'm Sam — a <Role /><br />
building calm, fast interfaces.
</motion.h1>
<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, delay: 0.25, ease }} className="mt-6 max-w-xl text-lg text-zinc-500">
I work at the intersection of design and engineering, turning complex products into interfaces that feel simple.
</motion.p>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.4 }} className="mt-8 flex flex-wrap items-center gap-5 text-sm text-zinc-500">
<span className="flex items-center gap-1.5"><MapPin className="size-4" /> Lisbon, Portugal</span>
<span className="flex items-center gap-1.5">
<span className="relative flex size-2"><span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" /><span className="relative inline-flex size-2 rounded-full bg-emerald-500" /></span>
Available for projects
</span>
</motion.div>
</section>
<section id="work" className="py-16">
<motion.h2 {...up} className="text-sm font-medium uppercase tracking-widest text-zinc-400">Selected projects</motion.h2>
<ul className="mt-8 divide-y divide-zinc-100 border-y border-zinc-100">
{projects.map((p, i) => (
<motion.li key={p.name} {...up} transition={{ ...up.transition, delay: i * 0.06 }}>
<a href="#" className="group -mx-4 flex items-start justify-between gap-6 rounded-2xl px-4 py-6 transition-colors hover:bg-zinc-50">
<div>
<h3 className="flex items-center gap-2 font-medium text-zinc-900">
{p.name}
<ArrowUpRight className="size-4 -translate-x-1 translate-y-1 text-zinc-400 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
</h3>
<p className="mt-1 text-sm text-zinc-500">{p.desc}</p>
<div className="mt-3 flex flex-wrap gap-1.5">
{p.tags.map((t) => (
<span key={t} className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs text-zinc-600">{t}</span>
))}
</div>
</div>
<span className="text-sm tabular-nums text-zinc-400">{p.year}</span>
</a>
</motion.li>
))}
</ul>
</section>
<section id="about" className="py-16">
<motion.h2 {...up} className="text-sm font-medium uppercase tracking-widest text-zinc-400">Experience</motion.h2>
<ul className="mt-8 space-y-6">
{experience.map((e, i) => (
<motion.li key={e.company} {...up} transition={{ ...up.transition, delay: i * 0.08 }} className="flex items-baseline gap-4">
<span className="w-28 shrink-0 text-sm tabular-nums text-zinc-400">{e.period}</span>
<span className="flex-1 border-b border-dashed border-zinc-200" />
<span className="text-right">
<span className="block font-medium text-zinc-900">{e.company}</span>
<span className="block text-sm text-zinc-500">{e.role}</span>
</span>
</motion.li>
))}
</ul>
</section>
<section id="contact" className="py-24">
<motion.div {...up} className="rounded-3xl border border-zinc-200 p-10 text-center">
<h2 className="text-3xl font-semibold tracking-tight">Let's build something together</h2>
<p className="mx-auto mt-3 max-w-md text-zinc-500">I'm currently taking on a small number of freelance projects for 2027.</p>
<a href="#" className="group mt-8 inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition hover:-translate-y-0.5">
<Mail className="size-4" /> Get in touch
</a>
<div className="mt-8 flex justify-center gap-4 text-zinc-400">
<a href="#" aria-label="GitHub" className="transition hover:-translate-y-0.5 hover:text-zinc-900"><Github className="size-5" /></a>
<a href="#" aria-label="LinkedIn" className="transition hover:-translate-y-0.5 hover:text-zinc-900"><Linkedin className="size-5" /></a>
</div>
</motion.div>
</section>
<footer className="pb-10 text-center text-xs text-zinc-400">© {new Date().getFullYear()} Sam Rivera</footer>
</div>
</div>
);
}
What's included
- Header
- Intro
- Projects
- Experience
- Contact
Use it in 3 steps
1. Install dependencies in your Next.js + Tailwind project:
$ npm i motion lucide-react- 2. Copy the code into
app/page.tsx. - 3. Replace the copy, colors and links with your own — then deploy.