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

Fixing Navigation and Analytics: When Your Data Lies About User Behavior htmx analytics ux architecture behind-the-scenes
HAL: Cutting 100-300KB of JavaScript by Moving Routing to Build Time performance jamstack htmx
HAL: Build-Time Link Rewriting That Makes Navigation Feel Instant htmx performance architecture
Web Components + HTMX: A Lean Architecture for Content Sites That Ship architecture web-components htmx performance
Progressive Enhancement with HTMX: Ship Fast, Degrade Gracefully web development htmx progressive enhancement
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
OpenClaw 2026.2.23: The Agent Browser Upgrade openclaw ai automation tools
The YC S26 Deadline Just Closed. Here's What Separates the 1.5% From Everyone Else. startups yc founders
OpenClaw: What It Is and How to Get the Most Out of It ai automation ops tools
Why I Skipped the CMS architecture content tools
Documentation That Scales: Constitution, Contracts, and Runbooks documentation architecture automation process behind-the-scenes
Zero-Server Analytics: How I Replaced a SaaS Bill with Netlify Functions and GitHub analytics serverless netlify github
Why I Built My Own Analytics Pipeline (And What It Actually Costs) analytics automation behind-the-scenes
The Static Site Playbook: Shipping a Content Product on a Near-Zero Budget jamstack architecture static-site-generation performance
How This Site Works: Architecture for a One-Person Team web development architecture performance JAMstack
Web Components as a Business Decision web development web components javascript
Less JavaScript, More Leverage: Why I Ship With a 35KB Budget web development performance architecture
Static Site Generation: The Business Case for Pre-Rendered HTML web development JAMstack performance SEO

Recommended

Fixing Navigation and Analytics: When Your Data Lies About User Behavior htmx analytics ux architecture behind-the-scenes
HAL: Cutting 100-300KB of JavaScript by Moving Routing to Build Time performance jamstack htmx
HAL: Build-Time Link Rewriting That Makes Navigation Feel Instant htmx performance architecture
Web Components + HTMX: A Lean Architecture for Content Sites That Ship architecture web-components htmx performance
Progressive Enhancement with HTMX: Ship Fast, Degrade Gracefully web development htmx progressive enhancement
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
OpenClaw 2026.2.23: The Agent Browser Upgrade openclaw ai automation tools
The YC S26 Deadline Just Closed. Here's What Separates the 1.5% From Everyone Else. startups yc founders
OpenClaw: What It Is and How to Get the Most Out of It ai automation ops tools
Why I Skipped the CMS architecture content tools
Documentation That Scales: Constitution, Contracts, and Runbooks documentation architecture automation process behind-the-scenes
Zero-Server Analytics: How I Replaced a SaaS Bill with Netlify Functions and GitHub analytics serverless netlify github
Why I Built My Own Analytics Pipeline (And What It Actually Costs) analytics automation behind-the-scenes
The Static Site Playbook: Shipping a Content Product on a Near-Zero Budget jamstack architecture static-site-generation performance
How This Site Works: Architecture for a One-Person Team web development architecture performance JAMstack
Web Components as a Business Decision web development web components javascript
Less JavaScript, More Leverage: Why I Ship With a 35KB Budget web development performance architecture
Static Site Generation: The Business Case for Pre-Rendered HTML web development JAMstack performance SEO