# From Dead Hamburger to Floating Dock

> Progressive portfolio enhancements building in real time

- Author: Alejandro Haydar
- Published: 2026-05-28
- Category: Product Design
- Canonical: https://alejandroastroport.netlify.app/showcases/docked-nav-rebuild/

---
import { Image } from "astro:assets";
import beforeHomeDesktop from "../../assets/images/nav-redesign/before-home-desktop.png";
import beforeHomeMobile from "../../assets/images/nav-redesign/before-home-mobile.png";
import afterHomeDesktop from "../../assets/images/nav-redesign/after-home-desktop.png";
import afterHomeMobile from "../../assets/images/nav-redesign/after-home-mobile.png";
import finalHomeDesktop from "../../assets/images/nav-redesign/final-home-desktop.jpeg";
import finalHomeMobile from "../../assets/images/nav-redesign/final-home-mobile.jpeg";
import afterFeedTopicFiltered from "../../assets/images/nav-redesign/after-feed-topic-filtered.png";

> Progressive portfolio enhancements building in real time. 

**Role:** Designer-engineer
**Project:** Personal portfolio (Astro + React)
**Skills spotlighted:** Design engineering, responsive interaction design, motion design

---

## Outcome

A floating dock replaced a header bar whose mobile hamburger opened nothing. Two responsive modes — a top-center pill on mobile, a slim left-edge sidebar on desktop — that hide while you read, surface topic shortcuts that deep-link to filtered work, and ship with a theme transition driven by the View Transitions API.

<div className="breakout grid grid-cols-1 md:grid-cols-2 gap-4 my-8">
  <figure>
    <Image src={beforeHomeDesktop} alt="Before — conventional header bar with logo, nav links, theme toggle, and hamburger" />
    <figcaption className="text-sm text-neutral-500 mt-2">Before — conventional header (desktop)</figcaption>
  </figure>
  <figure>
    <Image src={finalHomeDesktop} alt="After — slim left-edge column of icon-only dock buttons" />
    <figcaption className="text-sm text-neutral-500 mt-2">After — slim left-edge dock (desktop)</figcaption>
  </figure>
  <figure>
    <Image src={beforeHomeMobile} alt="Before — mobile view with hamburger button that did not open a menu" />
    <figcaption className="text-sm text-neutral-500 mt-2">Before — broken hamburger (mobile)</figcaption>
  </figure>
  <figure>
    <Image src={finalHomeMobile} alt="After — compact top-center pill with logomark and icon-only links" />
    <figcaption className="text-sm text-neutral-500 mt-2">After — top-center pill (mobile)</figcaption>
  </figure>
</div>

|  | Stage 1 — Top header | Stage 2 — Right dock | Stage 3 — Slim left dock (shipped) |
| --- | --- | --- | --- |
| Mobile menu | Broken hamburger | Top-center capsule | Compact icon-only pill |
| Desktop chrome | 96px header eating the hero | Right-edge column, off the reading path | Single slim left-edge column |
| Wayfinding | One flat Feed link | Topic shortcuts present, but off to the side | Topic shortcuts where the eye enters |
| Theme toggle | Class swap | View Transitions reveal | View Transitions reveal |

---

## Challenge

The shipped nav looked finished and worked on desktop. Three failures sat beneath:

1. **The mobile hamburger didn't open anything.** It rendered. It depressed on tap. It led nowhere.
2. **The header ate viewport.** 96px tall on desktop, pushing "Vision to shipped." below the fold on shorter laptops.
3. **No wayfinding into the work.** Every project sat under one flat Feed link, with no way to signal what was inside.

**Constraints:** static site (Astro), but the nav needed real-time behavior (scroll-hide, theme transitions), so it had to be a client-loaded React island. Subframe is sync-managed under `src/subframe`, so project-owned components live outside that folder.

---

## Process

### Three stages, two pivots

The dock didn't arrive in one move. It took two course-corrections — each driven by a different failure of the previous shape.

<div className="full-width grid grid-cols-1 md:grid-cols-3 gap-4 my-8">
  <figure>
    <Image src={beforeHomeDesktop} alt="Stage 1 — original top header bar" />
    <figcaption className="text-sm text-neutral-500 mt-2"><strong>Stage 1 — Top header.</strong> Conventional. Ate 96px of viewport, broken hamburger on mobile, no wayfinding into the work.</figcaption>
  </figure>
  <figure>
    <Image src={afterHomeDesktop} alt="Stage 2 — right-edge dock" />
    <figcaption className="text-sm text-neutral-500 mt-2"><strong>Stage 2 — Right dock.</strong> Off the reading path, which felt safe. But it parked wayfinding on the side the eye reaches last — every topic shortcut sat where readers look only after they've finished scanning the content.</figcaption>
  </figure>
  <figure>
    <Image src={finalHomeDesktop} alt="Stage 3 — slim left-edge dock, shipped" />
    <figcaption className="text-sm text-neutral-500 mt-2"><strong>Stage 3 — Slim left dock (shipped).</strong> Single thin column on the left edge, where left-to-right reading begins. Slim enough to guide without crowding the content. Icon-only, tooltip on hover/focus, hides on scroll-down.</figcaption>
  </figure>
</div>

### The first pivot — from header to dock

The plan was small: wire up a slide-down menu, ship it, move on. Forty minutes in, I realized I was about to ship a marginally better version of the same wrong thing. A drawer would fix the broken interaction, but the bar would still eat 96px of viewport, desktop would still be a wall of text links, and the topic problem wouldn't move.

I stopped. New file: `src/components/nav/DockNav.tsx`. New question: **if the nav weren't a header, what would it be?**

Mobile needed something compact and thumb-reachable. Desktop had dead vertical edge space the header wasn't using. Both modes needed to get out of the way while reading. That shape is a dock.

### The second pivot — from right edge to left, kept slim

My first dock landed on the **right** edge. The logic seemed sound: keep controls out of the reading path, the way OS chrome (Finder sidebar, file tree, IDE rails) sits off to the side. It shipped. It worked. But it parked the navigation on the side the eye reaches *last*.

Reading runs left to right. The eye enters every page from the left and ends on the right. A right-edge dock put the topic shortcuts — the whole point of the rebuild, the wayfinding into the work — in the spot a reader looks at only after they've finished scanning. The nav was technically present and effectively invisible. It also meant the first thing on screen was a wall of content with no signposting, and the signposting only when you'd given up looking.

The fix was two moves at once:

1. **Move to the left edge.** Where reading begins. The dock now sits in the eye's natural entry point, so wayfinding is the first thing a reader can act on — not the last.
2. **Keep it slim.** The earlier instinct to slim the chrome stays: a single thin column of small icon buttons, not a heavy stacked rail. On the left edge slimness matters even more — a fat left rail would crowd the content and read like a 2018 SaaS dashboard. Thin enough to guide, light enough to stay out of the way.

### Decisions and tradeoffs

**Two layout shells, one component.** A horizontal mobile pill and a vertical desktop sidebar solve different problems (mobile = reach, desktop = real estate). `DockNav.tsx` houses shared primitives (`DockLink`, `ThemeDockButton`) and composes them into two distinct shells, swapped at the `md` breakpoint — not one container morphing with five conditionals.

**Hide on scroll-down, return on scroll-up.** I know hide-on-scroll has costs: keyboard users scrolling to orient lose the nav, and motion-sensitive readers can find it twitchy. I weighed those against the cost of a floating dock that competes with the reading on every page. Mitigations: focus on any nav link forces the dock visible, `useReducedMotion` disables the animation entirely, and the dock returns the instant the scroll direction reverses (12px threshold, RAF-throttled, three states: `top` / `up` / `down`).

**Icon-only on desktop, text on mobile.** Costs discoverability and is fragile on touchscreen laptops. The bet: desktop visitors are mouse-first, tooltips appear on hover *and* keyboard focus, and the mobile pill uses text labels for the touch case. If discoverability shows up as a real problem, the fix is small — icon plus 11px label.

**Topic shortcuts deep-link to a filtered feed.** Each shortcut routes to `/feed/?topic=design-systems`. The feed page already filtered on the query param, so the nav surfaced filtering that was already there.

<figure className="my-8">
  <Image src={afterFeedTopicFiltered} alt="Feed page filtered to Design Systems topic, reached in one click from the dock's topic shortcut" />
  <figcaption className="text-sm text-neutral-500 mt-2">One click from anywhere → topic-filtered feed</figcaption>
</figure>

### Iteration

The first mobile build broke the homepage. The fixed pill at `top: 16` overlapped the hero headline because the old header used to provide top padding the hero relied on. A Playwright screenshot diff caught it; `pt-20 lg:pt-0` on the hero fixed it.

The first theme toggle was a class swap. It felt cheap. I rebuilt it with the View Transitions API — a circular clip-path expanding from the button's center to fill the viewport in 700ms, with an instant fallback in unsupported browsers. Same outcome, but the transition does the work of telling you something just changed across the whole page.

`[INTERACTION PLACEHOLDER: 6-second loop of the theme toggle's circular reveal, dark→light and light→dark]`

---

## Solution

**Mobile pill (`< md`)**
Compact rounded capsule pinned to top-center. Custom Ale logomark as home on the left, then a small row of icon-only buttons (About, Feed, theme toggle) — no text labels at this size. Backdrop-blurred. Hides on scroll-down via a Framer Motion spring.

<figure className="my-6">
  <Image src={finalHomeMobile} alt="Mobile pill dock — compact backdrop-blurred capsule pinned top-center with logomark and icon-only About, Feed, theme toggle" />
</figure>

**Desktop slim dock (`md+`)**
A single thin column of small circular icon buttons pinned to the left edge, vertically centered — where left-to-right reading begins. Theme toggle on top, then primary nav (Home, About, Feed), topic shortcuts (Design Systems, Product Design, AI Building, Prototypes), and back-to-top at the bottom — all icon-only, all with tooltips that open to the right on hover and keyboard focus. The slim form keeps the dock present without crowding the reading column, and the homepage hero and feed filter were given extra horizontal padding so the dock never overlaps their content.

<figure className="breakout my-6">
  <Image src={finalHomeDesktop} alt="Desktop slim dock — single thin column of small circular icon buttons on the left edge" />
</figure>

`[INTERACTION PLACEHOLDER: 10-second loop — scroll-hide on scroll-down, return on scroll-up, topic-shortcut click jumping to a filtered feed, hover-reveal tooltip]`

**Engineering grain**
- `useScrollDirection` hook: three states, 12px threshold, RAF-throttled
- Custom SVG logomark mixing `currentColor` (theme-reactive) with `hsl(var(--primary))` (brand accent)
- Subframe Feather icons for the rest
- Framer Motion springs (`stiffness: 420, damping: 34`)
- `useReducedMotion` honored — animations disable for motion-sensitive users
- `<nav aria-label="Primary">` on both modes, `aria-current="page"` on the active link, `focus-visible` rings on every interactive element

---

## Impact

**Homepage, current build (May 2026)**

| Metric | Value | Tool |
| --- | --- | --- |
| Lighthouse Performance (mobile) | **76** | `lighthouse --form-factor=mobile` |
| Lighthouse Accessibility | **96** | same |
| Lighthouse Best Practices | **100** | same |
| Cumulative Layout Shift | **0** | Lighthouse |
| Total Blocking Time | **0 ms** | Lighthouse |
| Largest Contentful Paint | 5.9 s | Lighthouse (mobile-throttled) |
| `DockNav.tsx` bundle | **5.66 KB gzipped** (18.7 KB raw) | `gzip -c dist/_astro/DockNav.*.js` |
| Total site JS | 203 KB gzipped (577 KB raw) | `find dist -name "*.js"` |
| axe-core violations | **1** (`color-contrast`, serious) | `axe-core` 4.11 in headless Chrome |
| axe-core passes | 35 rules | same |

The one a11y violation is a `p.text-sm.tracking-wide` element on the homepage (neutral-500 on neutral-100, ratio 4.24 — below the 4.5 WCAG AA threshold). It's outside the dock itself; the fix is a one-token contrast bump in the homepage copy. CLS 0 and TBT 0 confirm the scroll-hide animation and theme transition don't shift layout or block interaction.

**Structural shifts**
- Hero headline now sits at the top of the viewport — no header tax.
- One component (`DockNav.tsx`) replaces `Nav.astro`, the dead hamburger, and the inline theme toggle.
- `useScrollDirection` is portable — reusable for any future floating UI on the site.

**What I'd do differently**
- I left the old `Nav.astro`, `Logo.astro`, and original `ThemeToggle` in the repo for a "later cleanup pass." That cleanup is still pending. I should have deleted them in the same PR; dead code creates ambiguity about which component is current.
- I hardcoded the nav items inside `DockNav.tsx` instead of reading from the existing `src/data/navData.js`. That breaks the pattern the rest of the site uses. A small refactor I owe.

---

## Reflection

The smallest fix isn't always the right fix — and the first redesign isn't always the right one either. I sat down to wire a hamburger; forty minutes later I scrapped it for a dock. I shipped that dock on the right edge feeling done; days later I moved it to the left edge, slim, because that's where reading starts and where wayfinding should be. The "out of the way" instinct had quietly buried the most useful part of the nav. Each course-correction cost time. Each one moved the dock closer to the shape that actually serves the reading instead of competing with it.

The dock also taught me something about responsive design. The standard playbook (one component, breakpoint classes, hide-this-show-that) works when desktop and mobile are the same idea at different sizes. When they're solving different problems, two shells sharing primitives beats one container with five conditionals.