The Mobile Tabs Bug Was Actually Three State Machines

A desktop content-tabs component looked simple: click a tab, reveal its panel. The mobile design asked for accordion behavior instead, which also sounded simple: tap a heading, open its content. My first mental model was that one component merely needed two layouts. The implementation proved otherwise. It contained three state machines—desktop selection, mobile disclosure, and the transition between viewports—and the bugs lived where those systems disagreed.
Desktop selection assumes one answer
A conventional tab list has one active trigger and one associated panel. Arrow-key behavior, focus, selected state, and hidden panels all reinforce that single-selection model. The component can initialize the first tab and remain stable until a person selects another.
That assumption is useful on a wide screen because the triggers and panel share one visual region. It becomes awkward on a phone, where the selected heading may be far above the revealed content and the reader may want to compare two sections by scrolling.
Mobile disclosure asks a different question
An accordion is not merely tabs stacked vertically. It decides whether multiple sections may stay open, how headings and content remain adjacent, what a second tap does, and whether the URL or history should reflect state. I chose explicit disclosure controls and preserved a predictable initial section rather than pretending the desktop tab selection could be styled into an accordion.
The underlying content did not change, but the relationships exposed to assistive technology did. Roles, aria attributes, focus behavior, and keyboard controls had to follow the active interaction pattern rather than the CSS breakpoint alone.
The viewport transition was the hidden third system
Resize from desktop to mobile after selecting the fourth tab. Should only the fourth accordion item open? Resize back after opening two mobile items. Which desktop tab becomes selected? If the code leaves stale hidden attributes or duplicate active classes, the visible result may look acceptable while keyboard and screen-reader state becomes contradictory.
I wrote transition rules instead of allowing each mode to initialize independently. Entering mobile carries the meaningful desktop selection into disclosure state. Returning to desktop chooses a deterministic active panel, clears mobile-only state, and restores the tab contract. The transition is an event with policy, not an incidental side effect of a media query.
Existing accordion code was both help and risk
The project already had accordion behavior. Reusing it reduced duplicated event handling, but only after I identified which parts were generic and which were tied to old markup. Shared primitives are useful; shared assumptions are dangerous. I expanded the common behavior while keeping component-specific selectors and initialization explicit.
This prevented a fix for content tabs from silently changing standalone accordions elsewhere. Regression tests covered both families because a shared helper makes every consumer part of the change surface.
Saved content and no-script output still mattered
The server renders headings and panels in a useful order. JavaScript enhances that structure; it does not invent the content. If script fails, a reader sees the full material rather than an empty shell. I also avoided changing the stored field model just to match the two front-end modes.
That boundary made the feature easier to reason about: PHP owns complete content and stable identifiers, CSS owns layout, and JavaScript owns interaction state. None of the layers is allowed to become the only place the information exists.
I tested journeys, not isolated clicks
- Load directly at mobile and desktop widths.
- Navigate every desktop tab with keyboard controls.
- Open and close mobile sections in different orders.
- Resize in both directions with a non-default item active.
- Reload after interaction and confirm the documented reset behavior.
- Verify unrelated accordion components still behave as before.
Those journeys found failures that a snapshot of the first panel never could.
Analytics added another wrinkle. A desktop tab selection and a mobile accordion expansion may represent the same content interest, but they are not identical interactions. I kept one semantic event name with mode and item identifiers rather than two unrelated tracking schemes. That lets product teams compare engagement without pretending a mobile open and a desktop selection have the same surrounding context.
I also tested repeated initialization because content can enter the page through previews or partial updates. Event listeners are guarded, generated IDs remain stable, and enhancement can run again without duplicating controls. Responsive components often fail not on first load but when another system refreshes their markup. Treating initialization as idempotent made that path much less fragile.
One final restraint helped: I did not store viewport mode in WordPress. Mobile and desktop are runtime contexts, not editorial properties. The saved data describes items and order; the browser selects the interaction. Keeping device assumptions out of content means a future layout can evolve without migrating every tab set.
The design handoff now includes state transitions
When I receive another responsive component, I will ask more than “what does it look like on mobile?” I want to know what state exists in each mode, what survives a viewport change, what is announced, and what happens without enhancement. Screens communicate appearance. The product contract also needs the transitions between them.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


