web design
COMPONENT G / TIMELINE RAIL · RAIL-DRAW + NODE STAGGER
  1. 2024 - now

    Design Systems Lead

    Lumen Studio

    Own the component library and token pipeline. Cut surface-level duplication across twelve product teams.

  2. 2021 - 2024

    Senior Frontend Engineer

    Forge Labs

    Built the trading dashboard front end. Introduced scroll-driven motion with a strict reduced-motion contract.

  3. 2019 - 2021

    Frontend Engineer

    Quill & Co

    Editorial platform work. Migrated a legacy jQuery codebase to a typed component model.

  4. 2018 - 2019

    Independent

    Contract

    Shipped marketing sites and small tools for early-stage founders.

GUIDE / TIMELINE RAIL

Overview

A vertical experience rail. The hairline draws top to bottom, nodes pop as the line reaches them, and each entry's text settles in just after. Left-aligned, no alternating sides, so it reads as a single column of time.

When to use

The about page, or a short career section under the hero. Keep to four to six entries, anything longer belongs on a dedicated CV page. Pair date ranges with a hyphen, never an em-dash.

Spec

Tokens
--line rail, --white nodes, --grey body
Motion
rail scaleY 0.9s, nodes + text stagger
A11y
ordered list semantics, reduced-motion static
Layout
single column, 40px left padding for rail

Anti-patterns

No alternating left/right sides, it breaks the column. No icons on nodes, the square is enough. Do not animate the rail width, only its height.

<!-- Requires GSAP: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js -->

<style>
:root{
  --black:#050505;--surface:#0C0C0C;--white:#FFFFFF;--grey:#A3A3A3;--grey-dim:#6B6B6B;--accent:#22D3EE;
  --line:rgba(255,255,255,.16);--line-faint:rgba(255,255,255,.07);
  --font-display:'Space Grotesk',system-ui,sans-serif;--font-mono:'JetBrains Mono',ui-monospace,monospace;
  --dur-fast:150ms;--dur-base:300ms;--ease-out:cubic-bezier(.16,1,.3,1);
}
*{margin:0;padding:0;box-sizing:border-box;border-radius:0!important}
body{background:var(--black);color:var(--white);font-family:var(--font-display);min-height:100vh;padding:64px 48px}
.tl{position:relative;list-style:none;max-width:720px;margin:0 auto;visibility:hidden;--rail:1}
.tl::before{content:"";position:absolute;left:3px;top:4px;bottom:4px;width:1px;background:var(--line);transform:scaleY(var(--rail));transform-origin:top center}
.tl-item{position:relative;padding:0 0 48px 40px}
.tl-item:last-child{padding-bottom:0}
.tl-node{position:absolute;left:0;top:6px;width:7px;height:7px;background:var(--white)}
.tl-date{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--grey);display:block;margin-bottom:8px}
.tl-role{font-size:clamp(1.25rem,2.2vw,1.625rem);font-weight:500;letter-spacing:-.01em;line-height:1.2}
.tl-org{font-family:var(--font-mono);font-size:13px;color:var(--grey);margin-top:4px}
.tl-desc{color:var(--grey);font-size:.9375rem;line-height:1.6;margin-top:12px;max-width:52ch}
</style>

<ol class="tl" id="tl">
  <li class="tl-item">
    <span class="tl-node"></span>
    <span class="tl-date">2024 - now</span>
    <h3 class="tl-role">Design Systems Lead</h3>
    <p class="tl-org">Lumen Studio</p>
    <p class="tl-desc">Own the component library and token pipeline. Cut surface-level duplication across twelve product teams.</p>
  </li>
  <li class="tl-item">
    <span class="tl-node"></span>
    <span class="tl-date">2021 - 2024</span>
    <h3 class="tl-role">Senior Frontend Engineer</h3>
    <p class="tl-org">Forge Labs</p>
    <p class="tl-desc">Built the trading dashboard front end. Introduced scroll-driven motion with a strict reduced-motion contract.</p>
  </li>
  <li class="tl-item">
    <span class="tl-node"></span>
    <span class="tl-date">2019 - 2021</span>
    <h3 class="tl-role">Frontend Engineer</h3>
    <p class="tl-org">Quill &amp; Co</p>
    <p class="tl-desc">Editorial platform work. Migrated a legacy jQuery codebase to a typed component model.</p>
  </li>
  <li class="tl-item">
    <span class="tl-node"></span>
    <span class="tl-date">2018 - 2019</span>
    <h3 class="tl-role">Independent</h3>
    <p class="tl-org">Contract</p>
    <p class="tl-desc">Shipped marketing sites and small tools for early-stage founders.</p>
  </li>
</ol>

<script>
const tl = document.getElementById("tl");
const items = gsap.utils.toArray(tl.querySelectorAll(".tl-item"));
const nodes = items.map(i => i.querySelector(".tl-node"));
const texts = items.flatMap(i => [i.querySelector(".tl-date"), i.querySelector(".tl-role"), i.querySelector(".tl-org"), i.querySelector(".tl-desc")]);

function intro(){
  return gsap.timeline({delay:0.3, defaults:{ease:"power3.out"}})
    .set(tl, {autoAlpha:1})
    .fromTo(tl, {"--rail":0}, {"--rail":1, duration:0.9, ease:"power2.inOut"}, 0)
    .from(nodes, {scale:0, duration:0.4, stagger:0.2}, 0.2)
    .from(texts, {autoAlpha:0, y:14, duration:0.5, stagger:0.04}, 0.35);
}

let timeline = null;
const mm = gsap.matchMedia();
mm.add({
  full:   "(prefers-reduced-motion: no-preference)",
  reduce: "(prefers-reduced-motion: reduce)"
}, (ctx) => {
  if (ctx.conditions.full) timeline = intro();
  else gsap.set(tl, {autoAlpha:1});
});
</script>