Mobile-First Layout That Ships: How PrimaryLayout Solves Real UX Problems

Created: • Updated: • 4 min read
Person holding a phone displaying a clean reading app

Most of your readers are on phones. That's not an opinion - it's what the analytics say for virtually every content site. If mobile feels like a second-class experience, you're losing people before they finish a single article. But hiring a UX team or adopting a component library to solve basic layout problems is expensive overkill for a small operation.

PrimaryLayout is a single custom element that handles the hard parts of mobile reading UX: fixed-header offsets, safe-area insets, recommendation card interactions, and loading feedback. It works alongside HTMX for navigation (see the architecture overview) and keeps the total JavaScript budget well under control (more on that in The Minimal JavaScript Approach).

The problem: content hiding behind fixed headers

A fixed header keeps navigation accessible on every scroll, but it causes content to slide underneath whenever HTMX swaps fire. The typical fix - adding top padding to every page template - is fragile and creates coupling between the header height and every template in the system.

Instead, I baked the offset directly into PrimaryLayout:

.pl-container {
          padding-top: 56px;
        }
        .pl-main-spacer,
        .pl-nav-spacer,
        .pl-aside-spacer {
          flex: 0 0 56px;
          height: 56px;
        }
        

Those spacers live inside the custom element's shadow DOM. The rest of the site doesn't know or care about the header height - PrimaryLayout owns that detail. When HTMX finishes swapping in a new article, the layout's scrollMainToTop() method resets the scroll position so the reader starts at the top with no jarring jump.

This is the kind of encapsulation that pays off over time. Change the header height once, in one place, and every page just works.

Safe-area insets: respecting the hardware

Phones with gesture bars (iOS) or navigation pills (Android) can easily overlap bottom-positioned UI elements. This is the kind of bug that's invisible in desktop testing and infuriating to real users.

.pl-main,
        .pl-mobile-recs {
          padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));
        }
        

The env() function grabs the device's safe-area inset at render time, so the mobile "Show more" button always sits above the nav bar. No hardcoded pixel values, no device-specific hacks. One line of CSS that works on every phone.

Recommendation cards that feel native

Recommended articles need to work seamlessly on both desktop and mobile without duplicating logic. The static build process generates two containers (#desktop-recs and #mobile-recs), and PrimaryLayout exposes slots for each. HTMX's out-of-band swaps (hx-swap-oob) refresh both containers whenever a reader loads a new article - the card you just tapped disappears and the list repopulates without a full reload.

The interactions are designed to feel instant:

These are small details, but they're the difference between a site that feels like a web page and one that feels like a product.

Loading feedback without a skeleton screen

Full-page skeleton screens are a well-known pattern, but they're overkill for a content site where navigations take 100-300ms. Instead, I embedded a progress bar directly under the header:

<header class="pl-header">
          <div class="pl-header-container">...</div>
          <div class="pl-progress-line" aria-hidden="true"></div>
        </header>
        

The layout hooks into HTMX lifecycle events to manage the bar's state:

document.body.addEventListener('htmx:beforeRequest', (event) => {
          withLayout(event.detail, (layout) => layout.setLoading(true));
        });
        document.body.addEventListener('htmx:beforeSwap', (event) => {
          withLayout(event.detail, (layout) => layout.setLoading(false));
        });
        

setLoading(true) triggers a pulse animation; setLoading(false) fades it out so the bar never lingers. Because the bar lives inside the custom element, I can change the gradient, the height, or the animation without touching a single page template.

Scroll-to-top that doesn't break on navigation

Early versions tried to scroll the <article> element directly, which broke whenever HTMX replaced the node. The current approach is simpler: the layout exposes scrollMainToTop(), and an htmx:afterSwap listener calls it as soon as new content arrives. Readers always land at the start of the article, even after tapping through several recommendation cards.

Why this matters for small teams

The real value of PrimaryLayout isn't the code - it's the decision surface area it eliminates. Safe-area padding, header offsets, loading indicators, scroll management: these are problems you solve once, encapsulate in a component, and never think about again. Every hour not spent debugging mobile layout quirks is an hour spent writing content, building features, or talking to users.

For the analytics side of understanding how readers interact with this layout, see Zero-Server Analytics. For the full architecture that ties these pieces together, there's How This Blog Works.

What's next

I'm testing two additions:

  1. Highlighting the currently viewed card in the recommendation list so readers can orient themselves after navigating.
  2. A "back to top" affordance for long-form posts - but only if it can ship without meaningfully increasing the JavaScript footprint.

Until then, PrimaryLayout keeps doing the quiet work: respecting safe areas, coordinating HTMX swaps, and making every page feel like it was built for phones first - because it was.

Recommended

Anthropic Trained Its Replacement ai startups founders
Pydantic: The Open Source Layer Quietly Running the AI Economy ai open-source python pydantic anthropic tools
Karpathy Was Wrong: OpenClaw Still Outruns Its 5 Real Alternatives openclaw ai tools security

Recommended

Anthropic Trained Its Replacement ai startups founders
Pydantic: The Open Source Layer Quietly Running the AI Economy ai open-source python pydantic anthropic tools
Karpathy Was Wrong: OpenClaw Still Outruns Its 5 Real Alternatives openclaw ai tools security