# 3d-portfolio turns a developer's skills into keycaps on a playable 3D keyboard

> 3d-portfolio is a Next.js 16 and React 19 template whose navigation is a 3D keyboard where each keycap is a skill, with optional realtime features, a Resend contact form and a Chibi keycap customiser. It is free to use, personal information is centralised in one config file, and the licence metadata does not match the badge in the README.

**Naresh-Khatri/3d-portfolio** — Interactive 3D developer portfolio with a keyboard where every keycap is a skill. Built with Next.js, React, TypeScript, GSAP & Motion. Free to use!

- Repository: https://github.com/Naresh-Khatri/3d-portfolio
- Website: https://nareshkhatri.dev
- Stars: 1,343 · Forks: 263
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/naresh-khatri-3d-portfolio

## The keyboard is the navigation, not a decoration

The pitch in the README is short. It is a developer portfolio you can actually play with: press keys on a 3D keyboard, watch other visitors' cursors fly by, and chat with whoever is online.

The central idea is that the keyboard is not an illustration of a developer sitting at a desk, it is the site map. Each keycap represents one skill, and hovering or pressing it reveals the title and the description. The keyboard is generated programmatically rather than modelled by hand, which is what makes it practical to rebrand: the number of keycaps follows the number of skills in the data file.

The 3D layer is @chibi3d/runtime at 0.5.0, with three at 0.185.1 and React Three Fiber at 9.8.1 underneath. The animation layer is a deliberate double stack, GSAP for the heavier timeline work and Motion for the component-level transitions, plus Lenis for smooth scrolling and a space theme with floating particles on a dark canvas.

There is also a customiser. The Chibi keycap customiser at chibi.nareshkhatri.dev/demo/keycaps lets you try colours, logos and layouts in the browser before committing them to the source, which is the difference between a template you can rebrand in an hour and one you end up rebuilding.

The rest of the feature list is a normal portfolio's list with a 3D coat: full light and dark mode, responsive layout, a contact form with email delivery through Resend, optional Umami analytics, and a set of easter eggs that the README says you find by opening DevTools and following the clues.

## Everything personal lives in one config file

The rebrand story is the part of a template that decides whether it is usable, and this one has been thought about.

All personal information is centralised in src/data/config.ts, and the README shows the shape of the object.

```ts
const config = {
  title: "Your Name | Your Title",
  description: {
    long: "Your long description for SEO...",
    short: "Your short description...",
  },
  keywords: ["your", "keywords"],
  author: "Your Name",
  email: "you@example.com",
  site: "https://yoursite.com",

  // GitHub stars button in the header
  githubUsername: "your-github-username",
  githubRepo: "your-repo-name",

  social: {
    twitter: "https://x.com/you",
    linkedin: "https://linkedin.com/in/you",
    instagram: "https://instagram.com/you",
    facebook: "https://facebook.com/you",
    github: "https://github.com/you",
  },
};
```

Two fields are worth noting because they are the ones people forget. The description object is separate because there are two lengths, a long one for search engines and a short one for the interface, and the keywords array exists for the same reason. The githubUsername and githubRepo pair drives the stars button in the header, which is the kind of small dynamic element that usually ends up hard-coded in a template.

Three other files are listed for customisation. src/data/projects.tsx holds the projects, their screenshots, descriptions and tech stacks. src/data/constants.ts holds the skills list, with name, description and icon, and the work experience. And public/assets/ holds the images, the Open Graph image and the project screenshots.

So a fork touches four places, and only one of them, constants.ts, has a structure you need to read before you can fill it in.

## The realtime layer is optional, and it lives outside the repository

The realtime features are the most interesting part of the template and the part with the sharpest dependency.

They are powered by a separate backend API, and they cover live cursors, so you can see other visitors' cursors in realtime, online presence, so you can see who is currently on the site, live chat between visitors, and a zombie survival game with multiplayer and leaderboards on the live site.

The way they switch on is a single environment variable. These features activate automatically when NEXT_PUBLIC_WS_URL is set, and the .env.example file is explicit that leaving it empty disables the realtime features entirely. The client side is socket.io-client at 4.8.3, so the backend is expected to speak that protocol, and the URL is a WebSocket server URL that the browser connects to directly, which is why it is prefixed with NEXT_PUBLIC.

That detail explains the design of the whole feature set. Cursors, presence and chat are all things that only work when someone else is also connected, so on a freshly forked site with no backend they are dead code. The portfolio still works, and the README frames the whole group as optional.

The same pattern applies to analytics. Umami is optional, and the .env.example says nothing loads unless UMAMI_SITE_ID is set, with UMAMI_DOMAIN overriding the script URL for a self-hosted instance. There is also UMAMI_DEPLOY_SITE_ID, described as the website id that the collect endpoint records deploy hostnames into, with the note that when it is unset the endpoint only writes to the runtime log, which is the kind of detail that tells you the analytics integration was built by someone who had to make it work on a fork.

## The keycap customiser and the SKILLS array have to agree

The keyboard is driven by the SKILLS array in src/data/constants.ts, and the customiser is how you discover what the options are.

Each skill entry can override the keycap's appearance and behaviour.

* keyboardColor: the keycap colour, such as "#ff9408".
* keyboardIcon: an SVG URL copied from thesvg.org, or a local file under public/assets/keyboard/logos/.
* label and shortDescription: the text shown when interacting with a key.
* shortcut: an optional physical key code, such as "KeyQ", which must be unique, with the defaults following QWERTY letter order.

Without keyboard overrides, a key falls back to the skill's own colour and icon, so the data file works on its own and the overrides are progressive.

The layout is a separate lever. Keys follow the order of SKILLS, and to change the default six columns you pass a second argument to createKeyboardScene in keyboard-scene.tsx, with the rows adjusting automatically to the skill count. That is a small, well-contained piece of configuration, and it is the difference between a keyboard that looks like a keyboard and one that looks like a spreadsheet.

One warning is worth repeating from the README, because the demo and the source disagree slightly. The demo accepts logo names, but keyboardIcon needs an image URL or a local asset path, so a name that looks right in the customiser will not work when you paste it into the source.

## Next.js 16 changed the lint rules, and the README is precise about it

The code checks section is short and unusually informative, which suggests the maintainer has already hit the consequences.

```bash
pnpm lint
pnpm typecheck
```

The README asks for both before opening a pull request, and then explains what has changed. Next.js 16 no longer runs lint during builds, and the ESLint CLI uses eslint.config.mjs rather than the older configuration file. The package.json confirms the shape, with lint set to eslint . and typecheck set to tsc --noEmit --incremental false, and the repository carrying eslint.config.mjs, a components.json for shadcn, next.config.mjs, postcss.config.mjs, tailwind.config.ts and tsconfig.json at the root.

The most useful part is the grading of the React Compiler diagnostics. New diagnostics for refs, immutability, effect state updates and manual memoization report warnings, while the existing animation and realtime components are still being migrated, and hook ordering violations remain errors.

That is a precise statement of the state of the codebase: a modern React codebase with the compiler enabled, a set of warnings the maintainer has accepted while migrating the animation-heavy parts, and a hard error category that still holds. It also means the lint output from a fresh fork will not be clean, and the README is telling you that in advance rather than letting you discover it.

The toolchain around it is current but pinned. Node.js 20.9 or later is required for Next.js 16, with a current LTS release and pnpm 11, and the packageManager field pins pnpm at 11.20.0. The stack itself is Next.js 16.2.2, React 19.2.4, TypeScript 5, Tailwind CSS 3.4.1, GSAP 3.12.5, motion 12.23.24, three 0.185.1 and Zod 3.23.8.

## Blog frontmatter and asset caching are the two content contracts

Two content rules are documented, and both are the kind of thing that produces a confusing error if you do not know them.

The first is the blog frontmatter. It requires a title, a summary and a publishedAt date in YYYY-MM-DD format, with image, author and a list of tags as optional fields. Invalid frontmatter raises a content error, and an unknown blog slug returns a 404. The frontmatter is read with gray-matter and the posts are rendered with next-mdx-remote, so the blog is a real part of the template rather than a leftover.

The second is caching. Files under public/assets can be replaced at the same URL and cache for one hour before revalidation, while Next.js content-hashed static assets retain immutable caching. That is a useful thing to know before you ship: if you swap a screenshot at the same path, a visitor may see the old image for up to an hour, whereas the hashed assets in the build output never go stale.

The environment file has one more small courtesy worth knowing about. NEXT_PUBLIC_LEGACY_HOST takes the old domain you moved away from, and when it is set the site shows a one-time dismissible we moved notice. That is a small migration helper for the very common case of a portfolio that changes its domain.

The install path is short. Clone the repository, run pnpm install, copy .env.example to .env.local and fill in the values, then run pnpm dev and open localhost:3000. The only required variable is RESEND_API_KEY, the API key from Resend for the contact form, and everything else in the table is optional: the WebSocket URL for the realtime features, and the Umami domain and site id for analytics.

## The licence is MIT in the badge and unknown in the metadata

There is a discrepancy to flag. The README carries a badge reading License: MIT and links it to a LICENSE file in the repository, and the project states that it is free to use with a credit or link back appreciated. The repository metadata, on the other hand, reports the licence as unknown, a custom licence that GitHub cannot classify.

For a template that people are invited to copy, this matters more than it would for a library, because the template is the thing being redistributed when someone forks and publishes their own portfolio. The MIT grant is the permissive one and covers use, modification and redistribution with the notice retained, but the file at the root is what governs, and the metadata is what a scanner reads.

The credit request is a request rather than a licence condition, and the README is polite about it: if you use it, a credit or link back would be really appreciated. That is a reasonable thing to honour, and the live demo at nareshkhatri.dev plus the Vercel deploy button at the top of the README make the intent clear, that the site is meant to be forked, deployed to your own account and pointed at your own domain.

On maintenance, the repository is not archived, the default branch is main, and the last push was on 2026-10-11, with a live demo running the current build. There are no GitHub releases, so the version history lives in the commits, and the package version in package.json is 0.1.0 with the package marked private, which is the normal arrangement for an application rather than a published library.

## Conclusion

3d-portfolio fits a developer who wants a memorable portfolio built on a modern Next.js stack, and who is happy to replace the sample content, the keycap colours and the skills list. It does not fit someone who needs the realtime features out of the box, since they depend on a separate WebSocket backend this repository does not include, and it does not fit a server-rendered marketing site, since the whole navigation is a client-side 3D scene. Before deploying, run pnpm lint and pnpm typecheck as the README asks, set RESEND_API_KEY or remove the contact form, and check the licence file directly, since the README badge says MIT while the repository metadata reports the licence as unknown.

## FAQ

### How do I run the 3d-portfolio template locally?

You need Node.js 20.9 or later with a current LTS release and pnpm 11. Clone the repository, run pnpm install, copy .env.example to .env.local and fill in RESEND_API_KEY, then run pnpm dev and open http://localhost:3000.

### How do I change the skills shown on the 3D keyboard?

The skills list lives in src/data/constants.ts, and keys follow the order of that SKILLS array. Each entry can override keyboardColor, keyboardIcon, label, shortDescription and an optional shortcut key code, and the number of columns is a second argument to createKeyboardScene in keyboard-scene.tsx. The Chibi keycap customiser lets you try these in the browser first.

### Are the live cursors and chat features included?

They need a separate backend. The realtime features activate automatically when NEXT_PUBLIC_WS_URL is set, and leaving it empty disables live cursors, presence and chat entirely. The client uses socket.io-client, so the WebSocket server has to speak that protocol.

### What do I have to change to make the portfolio mine?

Most personal information is centralised in src/data/config.ts, covering the title, the two description lengths, keywords, author, email, site, GitHub username and repository for the stars button, and the social links. You also edit src/data/projects.tsx for projects and screenshots, src/data/constants.ts for the skills and work experience, and public/assets/ for images and the Open Graph image.

### What does the lint step check in the 3d-portfolio project?

Next.js 16 no longer runs lint during builds, so the README asks you to run pnpm lint and pnpm typecheck before opening a pull request. React Compiler diagnostics for refs, immutability, effect state updates and manual memoization report warnings while the animation and realtime components are migrated, and hook ordering violations remain errors.

## Sources

- [Issues](https://github.com/Naresh-Khatri/3d-portfolio/issues)
- [Naresh-Khatri/3d-portfolio on GitHub](https://github.com/Naresh-Khatri/3d-portfolio)
- [Project website](https://nareshkhatri.dev)
- [README](https://github.com/Naresh-Khatri/3d-portfolio/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/naresh-khatri-3d-portfolio
