跳到正文
DEV Community· Samira N Ray·· 2 小時前AI 評分47

我用「時光機」打造作品集:一個網站、兩套完整主題

I Built a Portfolio With a Time Machine: Two Complete Themes, One Site

AI 導讀

Samira 建立了一個作品集網站,透過單一屬性在專業風格與復古 90 年代桌面風格之間切換。主題切換會播放年份倒數至 1996 的時光旅行動畫;裝置設定為減少動態效果時,則即時切換。網站以 localStorage 儲存選擇,並在瀏覽器繪製頁面前執行內嵌腳本,避免短暫閃現錯誤主題。

正文

Most portfolios make you pick a lane: polished and professional, or personal and fun. I couldn't pick, so my portfolio has both. The professional side is what you'd expect: soft blue-gray, serif headlines, project cards. Click the little clock in the top right, though, and the page tears apart, a year counter rewinds to 1996, and you land on a 90s desktop with windows, pixel icons and a taskbar.

It isn't two websites. It's one set of pages with two complete visual identities, switched by a single attribute. Here's how it works, and the three problems I had to solve along the way.

The core idea: one attribute, two identities

Everything hangs off one attribute on the <html> element:

<html data-mode="professional">
<!-- or -->
<html data-mode="fun">

Content for both modes lives side by side in the same HTML. Each block is tagged with the mode it belongs to:

<h1 class="mode-professional">Hi, I'm Samira. I turn messy data into things people can actually use.</h1>
<h1 class="mode-fun">Welcome to my world</h1>

Then CSS shows only the active mode's content:

.mode-fun,
.mode-professional {
  display: none;
}
:root[data-mode='fun'] .mode-fun {
  display: revert;
}
:root[data-mode='professional'] .mode-professional {
  display: revert;
}

display: revert matters here. It hands the element back its natural display value, so a <span> goes back to inline and a <div> goes back to block, without me hard-coding block everywhere. Flex and grid containers need their own rule, because revert would drop them back to block:

:root[data-mode='fun'] .mode-fun.flex {
  display: flex;
}

All the colors, fonts and spacing for each mode are scoped the same way, under :root[data-mode='…']. No build step, no framework, no second set of pages.

Problem 1: the flash of the wrong mode

If you saved the fun side and come back, the site should open on the fun side. The obvious approach is to read the saved choice in my main JavaScript file and set the attribute. But that script loads after the page starts rendering, so for a split second you'd see the professional side, then a jarring jump to the 90s.

The fix is a tiny inline script in the <head>, which runs before the browser paints anything:

<script>
  // Mode + theme init — runs before paint to avoid flash.
  (function () {
    var mode = 'professional';
    try {
      var saved = window.localStorage.getItem('portfolio-mode');
      if (saved === 'professional' || saved === 'fun') mode = saved;
    } catch (e) {
      /* sandboxed/private contexts block storage — default to professional */
    }
    document.documentElement.setAttribute('data-mode', mode);
  })();
</script>

Two details I'd point out:

  • It only accepts the two values it knows. If localStorage holds anything else, it falls back to professional instead of setting a mode with no styles.
  • It's wrapped in try/catch. Some private-browsing and sandboxed contexts throw when you touch localStorage. Without the catch, the whole page would break for those visitors. With it, they just get the default side. Every page carries this same script, so the mode follows you around the site.

Problem 2: the switch itself

Flipping the mode is a small function in app.js:

function setMode(next) {
  document.documentElement.setAttribute('data-mode', next);
  applyModeIcon();
  try {
    window.localStorage.setItem('portfolio-mode', next);
  } catch (e) {
    /* storage blocked — mode just won't persist */
  }
}

Instantly swapping the page felt flat, though. The whole point of the fun side is the feeling of going back in time, so the switch plays a short transition instead:

  • Professional → fun: the page tears apart, a glitch screen holds while a year counter rewinds to 1996 and two loading lines type out, then it fades onto the desktop.
  • Fun → professional: one last glitch settles into a calm "Back to the present" screen, the year counts forward to today, and the page comes back into focus. I keep every timing in one object, so tuning the feel never means hunting through the code:
var OLD_YEAR = 1996;
var THIS_YEAR = new Date().getFullYear();
var WARP = {
  tearIn: 700,     // pro -> fun: the page tears apart
  funHold: 3100,   // ...the glitch screen holds
  funOut: 500,     // ...and fades away (4.3s in all)
  settleCalm: 350, // fun -> pro: glitch alone, before the calm screen fades in
  settleIn: 900,   // ...calm screen fully in; the page swaps underneath
  proHold: 1150,   // ...calm screen holds
  proOut: 650      // ...and the page comes into focus (2.7s in all)
};

The trick that makes it feel seamless: the actual setMode call happens underneath the transition screen, while it covers the page. You never see the swap. You only see the time travel.

Problem 3: respecting people who don't want the show

A four-second glitch animation is fun for me. For someone with motion sensitivity, it's a problem. Visitors whose device is set to reduce motion get the plain instant swap instead:

var warpReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// ...inside the switch handler:
if (warpReduced) {
  setMode(next); // no time warp, just the switch
  return;
}
if (next === 'fun') warpToFun();
else warpToPro();

It costs a few lines, and it means the creative part of the site never gets in anyone's way.

Why I built it this way

I work in customer operations, and I'm finishing a data science degree. My professional work is dashboards, pipelines and AI tools for support teams. The fun side is where the rest of me lives: early-2000s internet nostalgia, Win95 windows, glitch art.

A two-sided portfolio let me show both without splitting them into two sites I'd have to keep in sync. And the fun side isn't decoration. It's a working project in its own right: the state handling, the transition timing and the accessibility fallback are exactly the kind of front-end problems I want to keep solving.

What I'd take from this if you're building your own:

  1. One attribute on <html> can drive an entire visual identity. You don't need a framework for theming.
  2. Set it in an inline <head> script, or you'll get a flash of the wrong theme.
  3. Guard localStorage with try/catch and validate what you read back.
  4. Swap the content while something covers the page, and the change feels like magic instead of a jump.
  5. Always give reduced-motion users the quiet version. Go take the tour at samira-ray.dev, and don't forget the clock in the top right. 🕰️

I'm Samira, a data science student building AI chatbots, data tools and full-stack apps, especially for customer operations. Data brain, frontend hands.

來源:DEV Community · dev.to