Refining the Portfolio: Table of Contents, Button Consistency, and Visual Hierarchy

On This Page

    After launching the initial responsive design for my portfolio, I spent another session focused on polish: adding navigation aids, creating visual consistency, and establishing clearer hierarchy. This post documents the implementation of a sticky table of contents, button style unification, and category badge refinement.

    The Challenge: Navigation and Visual Consistency

    Two problems became apparent after the responsive redesign:

    1. Long-form content needed structure - Project writeups ran hundreds of lines. Readers had no way to jump to specific sections or track their position in the document.

    2. Visual hierarchy was muddy - Category badges, tech tags, and action buttons all competed for attention. Nothing stood out as the primary call to action.

    The solution required balancing functionality (navigation) with aesthetics (clean visual hierarchy).

    Table of Contents: Desktop Sidebar, Mobile Inline

    Design Requirements

    The table of contents needed to:

    • Auto-generate from heading structure
    • Stay visible while scrolling (desktop)
    • Highlight the current section
    • Position appropriately on mobile without cluttering the header

    Implementation Approach

    I built a JavaScript-powered TOC that adapts to viewport size:

    Desktop (≥1024px): Sticky sidebar using CSS Grid

    .page-container {
      display: grid;
      grid-template-columns: 280px 1fr;
      gap: 2.5rem;
    }
    
    .toc-wrapper {
      position: sticky;
      top: 80px;
      max-height: calc(100vh - 100px);
      overflow-y: auto;
    }

    Mobile (<1024px): Inline between metadata and content

    const isMobile = window.innerWidth < 1024;
    
    if (isMobile && titleSection) {
      // Insert after project metadata
      titleSection.parentElement?.insertBefore(tocWrapper, titleSection.nextSibling);
    } else if (pageContainer) {
      // Insert as first grid column
      pageContainer.insertBefore(tocWrapper, pageContainer.firstChild);
    }

    Scroll Spy with Intersection Observer

    The TOC highlights the current section using the Intersection Observer API:

    const observerOptions = {
      rootMargin: '-100px 0px -66%',
      threshold: 0
    };
    
    const observerCallback = (entries) => {
      entries.forEach((entry) => {
        const id = entry.target.getAttribute('id');
        const link = tocNav?.querySelector(`a[href="#${id}"]`);
        if (entry.isIntersecting) {
          tocNav?.querySelectorAll('a').forEach((a) => a.classList.remove('active'));
          link?.classList.add('active');
        }
      });
    };

    The rootMargin creates a “viewport sweet spot” where headings trigger active state changes when they’re roughly 100px from the top and still 66% visible.

    Handling Long Headings

    Some technical headings exceeded the sidebar width. I used CSS line clamping with a hover title for full text:

    .toc-nav a {
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }
    a.title = headingText; // Full text on hover

    Button Unification: Consistent Calls to Action

    The Problem

    The portfolio had three button styles:

    • Homepage “Contact Me” - green with shadow and lift effect
    • Project “Visit Site” - blue background, basic hover
    • Project “View GitHub” - green background, opacity hover

    This inconsistency undermined the design’s credibility. Users expect buttons to behave similarly across a site.

    The Solution: One Button Style

    I standardized all action buttons to match the Contact Me style:

    .live-link,
    .repo-link,
    .contact-button {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.5rem 1rem;
      background: var(--accent);
      color: white;
      border-radius: 8px;
      font-weight: 600;
      transition: all 0.3s ease;
      box-shadow: 0 2px 8px rgba(74, 103, 65, 0.2);
    }
    
    .live-link:hover,
    .repo-link:hover,
    .contact-button:hover {
      background: var(--accent-dark);
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(74, 103, 65, 0.3);
    }

    This creates a cohesive experience where all actions feel like part of the same system.

    Inline Metadata Positioning

    Previously, project links appeared in a separate section below tech stack tags. This created unnecessary vertical distance and fragmented the metadata area.

    I moved them inline with Status, Version, and Started fields:

    <div class="meta">
      <div class="meta-item">
        <span class="category">{categoryLabels[category]}</span>
      </div>
      <div class="meta-item">
        Status: <span class={`status ${status}`}>{status.replace('-', ' ')}</span>
      </div>
      {/* ... other metadata ... */}
      {liveUrl && (
        <div class="meta-item">
          <a href={liveUrl} class="live-link" target="_blank">
            🌐 Visit Site
          </a>
        </div>
      )}
    </div>

    The flexbox layout handles wrapping automatically, keeping everything scannable.

    Visual Hierarchy: Category Badges vs Action Buttons

    The Conflict

    With buttons now using solid green backgrounds, category badges (also solid green) competed for attention. Everything looked equally important, which meant nothing stood out.

    The Fix: Semi-Transparent Categories

    I reduced category badge opacity to 65% while keeping buttons at 100%:

    .category {
      background: rgba(74, 103, 65, 0.65); /* Faded */
      color: white;
      text-transform: uppercase;
      font-size: 0.75rem;
      letter-spacing: 0.5px;
    }
    
    .live-link,
    .repo-link {
      background: var(--accent); /* Solid */
    }

    This creates a clear hierarchy:

    1. Action buttons (solid green) - Primary interactive elements
    2. Category badges (faded green) - Informational labels
    3. Tech tags (light gray) - Secondary information

    Responsive Project Grid: Three Columns to One

    The projects listing page needed a more controlled layout. The original auto-fit grid was unpredictable at certain viewport widths.

    Explicit Column Breakpoints

    I replaced automatic grid with explicit breakpoints:

    /* Desktop: 3 columns */
    ul {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: var(--space-lg);
    }
    
    /* Tablet: 2 columns */
    @media (min-width: 768px) and (max-width: 1023px) {
      ul {
        grid-template-columns: repeat(2, 1fr);
      }
    }
    
    /* Mobile: 1 column */
    @media (max-width: 767px) {
      ul {
        grid-template-columns: 1fr;
      }
    }

    Viewport-Relative Container

    To prevent horizontal scrolling on narrow viewports, I used min() for responsive width:

    main {
      width: min(1200px, 95%);
      margin: 0 auto;
    }

    This means:

    • Viewports ≤ 1200px: Container is 95% width (2.5% margin each side)
    • Viewports > 1200px: Container caps at 1200px

    No more horizontal scrollbars, no more content cut off at screen edges.

    Additional Polish

    Email Icon in Header

    Added a mailto link to the header social icons:

    <a href="mailto:user@example.com">
      <span class="sr-only">Send email</span>
      <svg viewBox="0 0 24 24"><!-- mail icon --></svg>
    </a>

    Implemented flexbox sticky footer to prevent awkward spacing on short pages:

    body {
      display: flex;
      flex-direction: column;
      min-height: 100vh;
    }
    
    main {
      flex: 1;
    }

    Lessons Learned

    1. Navigation aids are non-negotiable for long content - The TOC immediately made project writeups more accessible. Before, readers had to scroll blindly; now they can jump directly to relevant sections.

    2. Consistency builds trust - Unifying button styles seems minor, but inconsistent UI signals lack of attention to detail. Users notice.

    3. Visual hierarchy requires restraint - Making everything bold and colorful means nothing stands out. Strategic use of opacity and size creates clear priority.

    4. Responsive design is about control - Auto-fitting grids are convenient but unpredictable. Explicit breakpoints give you control over the user experience at every viewport size.

    5. The details compound - Sticky footer, truncated headings, viewport-relative widths—none of these alone transform the experience, but together they eliminate friction.

    What’s Next

    The portfolio now has solid information architecture and consistent UI patterns. Next priorities:

    • Performance audit - Run Lighthouse, optimize images, check bundle size
    • Content expansion - Add more project writeups, document recent lab work
    • Accessibility review - Test with screen readers, verify keyboard navigation

    The foundation is stable. Time to build on it.


    All code for this portfolio is open source at github.com/hmbldv/portfolio. The site is built with Astro and deployed on Vercel.