YS :
cd ~/posts
~/posts/terminal-portfolio-redesign.md
--- · 7 min read

Why I Rebuilt My Portfolio as a Terminal

Phosphor green, a Cmd+K palette, a boot sequence that respects prefers-reduced-motion, and one Tailwind footgun that almost ate an afternoon.

terminal portfolio hero with boot sequence and repl

Why

My old portfolio was fine. Cards, gradients, a rotating avatar, a parallax hero driven by framer-motion. It looked like every other portfolio in 2024, which is precisely the problem — “fine” is the death of distinctiveness.

I wanted the site to do one thing: make someone who opens it remember me for ten seconds longer than average. That’s the whole goal of a personal site. Anything past that (SEO, conversion, “thought leadership”) is a bonus. The ten seconds are what count.

So I picked an aesthetic that commits: a monospace terminal. Phosphor green on near-black in dark mode, ink on warm paper in light mode. Cmd+K as navigation. A boot sequence instead of a splash. Every page framed as a terminal artifact — experience as a git log, projects as ls -la, 404 as stderr.

This post is about the decisions, the parts that actually shipped, and the one Tailwind bug that nearly ate an afternoon.

The tension I was solving

“Distinctive” and “not gimmicky” sit on opposite ends of one axis. Lean too far toward distinctive and you get portfolios that look like Geocities rewrites — memorable for the wrong reasons. Lean too far toward not-gimmicky and you get the card-grid portfolio that everyone’s seen 200 of.

The terminal aesthetic is an interesting point on that axis because it reads as identity, not costume. I actually live in a terminal. JetBrains Mono is my editor font. The site is what my brain’s chrome already looks like. It’s distinctive to visitors precisely because it’s not decorative for me.

The rule I kept reminding myself: if a design choice exists only to look cool, cut it. If a design choice exists because it’s genuinely how I work, keep it and commit harder.

What shipped

Typography

VT323 for display — a bitmap CRT font that lands hard at large sizes and disappears at body sizes. Paired with JetBrains Mono for everything that needs to be read. The hero name, the $ ls ./skills section labels, the 404 glyph — all VT323. Everything else JetBrains.

Avoided: Space Grotesk, Inter, Outfit, anything that would’ve made the site read “generic dev portfolio” the moment you saw the first heading.

The Cmd+K command palette

Terminal theme + not having a Cmd+K palette would’ve been malpractice. Hit ⌘K / Ctrl+K / / anywhere to open a fuzzy-searchable palette of pages, posts, socials, and actions (toggle theme, copy email, view source). Full keyboard nav.

This was the component I most suspected users might miss. Added a [ search ⌘K ] hint in the top bar so even non-keyboard users see it exists. Mobile gets a small search icon that opens the same palette — tap instead of hotkey.

Implementation notes:

  • React island with client:idle so it doesn’t block page nav. The palette hydrates during idle time.
  • Custom fuzzy-score function (exact match > startsWith > substring > subsequence). Didn’t need Fuse.js; ~30 lines of code.
  • Indexes posts via a tiny /posts.json endpoint generated at build.

The boot sequence hero

On page load:

  1. Window chrome renders (macOS-style red/amber/green dots, yash@web — -zsh — 82×24 title).
  2. Seven boot lines stream in over ~1 second ([ OK ] mounting /home/yash…).
  3. A fake REPL fires: $ ./introduce.sh.
  4. Five lines type out character-by-character: name, role, focus, location, status.
  5. CTA row fades in.
  6. Blinking cursor on the final prompt.

The whole thing clocks in around 3 seconds from navigation to interactive. Respects prefers-reduced-motion — if set, everything renders in final state immediately. Zero tradeoff between “cool entrance” and “accessible”.

Every page reframed

  • /experience — year-anchored timeline. Year on the left as the scan anchor, dashed rail in the middle, company + role + dates on the right. Active roles get a pulsing node and a HEAD tag. The earlier version of this page had fake commit hashes on every entry; I cut them in v2 because they were clutter disguised as flavor.
  • /projectsls -la output. Real Unix columns: mode, stars, forks, modified, name.
  • /contact — a compose.sh window with phospshor-bracketed radio pills for subject: [ general ], [ job opportunity ], [ freelance work ].
  • /postsls -lt listing. Date, read time, title, post number.
  • /posts/[slug] — post body wrapped in ~/posts/slug.md file chrome.
  • /404stderr window. “bash: /foo: No such file or directory” with [ cd ~ ] escape button.

Live now-playing widget

The small green equalizer bars in the top right of the navbar are a Last.fm-backed now-playing widget. Polls /api/nowplaying.json every 15 seconds (pauses when the tab is hidden, resumes on focus). When a track is playing it shows ▌▌▌ now · Track — Artist with animated bars; when idle, it shows the most recently played track in muted grey.

Why Last.fm and not Spotify? Because as of February 2026, Spotify requires the app owner to have a Premium subscription for their Web API to work at all — a policy that makes no sense for a personal-site use case. Last.fm’s API is free, has been for 15+ years, and every Spotify account I know already scrobbles to it.

The favicon

Black rounded-square terminal screen, phosphor-green > prompt, phosphor-green cursor block. Rendered sharp at 16×16 thanks to pixel-aligned coordinates and a chunky 3px stroke. One of the smallest files in the repo, one of the decisions I’m most satisfied with.

The Tailwind bug that nearly ate an afternoon

Worth a standalone section because it taught me a thing I’ll remember forever.

I had a color token called base in my Tailwind config — the page background. Perfectly normal:

colors: {
  base: 'rgb(var(--color-base) / <alpha-value>)',
  surface: 'rgb(var(--color-surface) / <alpha-value>)',
  // ...
}

Company names in one section of the homepage were rendering near-invisible. Dark text on near-dark background. Inspecting showed color: rgb(10, 10, 11) — the base color — applied somehow, despite the class being text-text-primary.

I tried the obvious:

  • Overriding with !important inline? Ignored.
  • Checking specificity? .text-text-primary clearly had higher specificity than the a tag rule.
  • Stripping classes one at a time? Bug vanished when I removed md:text-base.

md:text-base. A font-size utility.

Here’s what happened: Tailwind generates .text-base for font-size (the default: 1rem / 1.5rem). When you register a color key named base, Tailwind also generates .text-base — for color. Both rules have the same selector. They merge. The later declaration wins for each property.

So md:text-base, which I used for font-size, was also applying color: rgb(var(--color-base)) — the very color I used as a background. On every element at md+ breakpoint.

The fix: rename the color key. basecanvas. No more collision. bg-basebg-canvas in three files.

The meta-lesson is sharper than the specific fix. Tailwind v3’s text-* namespace is overloaded for colors and font-sizes. Any color key that matches a default font-size key (xs, sm, base, lg, xl, 2xl…) creates a silent collision. Tailwind v4 fixes this with namespaced theme tokens (--color-*, --font-size-*) — another reason v4 is worth the migration when you have the appetite for it.

What I didn’t ship

  • An actual scrolling “ticker” of recent commits. Prototyped it, felt like a gimmick. Cut.
  • Keyboard vim-motion navigation (hjkl). Tempting for the aesthetic. Also a usability disaster for the 99% of visitors who don’t know vim. Cut.
  • Per-visitor boot log personalization. “Boot line: visitor from Germany, 14:32 UTC.” Privacy-squeamish, zero value. Cut.
  • A “glitch on hover” effect on every link. Tried it. Was noisy. Cut to just the most important hover targets.

The through-line: restraint in service of the aesthetic. Maximalism is easy in a terminal theme; the discipline is deciding which one or two flourishes earn their place, and dropping the rest.

What the next iteration might do

Two ideas I haven’t committed to:

  1. A “shell history” page at /.bash_history — every URL visited on the site with a tiny timestamp, like actual shell history. Fun and on-brand. Unclear if anyone would use it.
  2. A built-in mini-shell — click any $ command on the site and it runs against a tiny sandboxed command set (ls, cat, whoami, cd). Pure novelty. Hard to stop at “fun” before hitting “bloat”.

If I ship either, I’ll write it up.

The closing bit

My old portfolio was a product of copying the current meta. My new one is a product of committing to one specific aesthetic that actually reflects how I work. That’s the whole thesis of why I did this — design for yourself first, and strangers will come along if the voice is real.

If you want the source, it’s at github.com/yashsuhagiya/portfolio. If you want to argue about any of the choices, drop me a line. And if your portfolio currently has a gradient hero and a rotating avatar — no judgment, but you already know what I’m going to say.

exit 0