Tabbed Mobile Shop Menu With Featured Banner Panel
A custom <shop-tabs> element turns the drawer's first-level Shop menu into horizontal tabs with panels of category links, merges a theme-editor banner block into the tab order by position, and leaves every other drawer item on the vendor's accordion.
A mobile drawer accordion puts a store's main Shop links three taps deep. On Three Ships' licensed Palo Alto theme, the first top-level item is rendered instead as a <shop-tabs> strip built from the same link list the accordion uses, with a merchandiser's featured banner inserted as a tab at a chosen position, and the open tab resolved in Liquid so the drawer is correct before any script runs.
The Problem
A mobile navigation drawer is almost always an accordion, and an accordion treats every top-level item the same way: tap to open a list, tap again to open a list inside it. For "About" that's fine. For "Shop" — the item that holds the whole catalog — it means a shopper opens the drawer, expands Shop, expands a category, and only then sees a link they can buy from. Three taps to reach the thing the store exists to sell, with the categories themselves hidden behind the first two.
Merchandising had a second ask. The menu is a surface shoppers open on purpose, and the team wanted a promotable slot inside it — a launch, a bundle, a page — rather than a menu that could only ever be a list of categories.
The Constraint
Only the first top-level link in the mobile menu should behave this way. Every other item in the drawer had to keep the vendor's existing accordion, with its collapsible triggers, three levels and highlight item, untouched. The tabbed menu also had to be built from the same link list the accordion reads — the header's mobile_main_menu setting — so the merchandising team keeps editing one menu in Shopify Navigation rather than maintaining a tab structure somewhere else.
The featured banner is a theme-editor block, and its place in the strip is a position — an index among the menu's children. That puts two sources, a link list and a section block, into one ordered sequence that has to survive a menu whose child count changes.
And the drawer is licensed code. Palo Alto 5.8.0 by Presidio Creative is a paid premium theme, so this was an addition inside the vendor's nav-item-mobile.liquid, not a replacement of it.
What We Built
nav-item-mobile.liquid gained an index == 0 branch: when the drawer renders the first top-level link and that link has children, it renders a <shop-tabs> element in place of the accordion item.
Before any markup, Liquid resolves the featured block and the first active tab. It scans section.blocks for a mobile_featured_image block, reads its position setting as a number, and decides which panel opens first through a fixed chain: the featured panel if its position is one; otherwise the first child link that itself has children; otherwise, if a block exists, the featured panel. That verdict is a handle every tab and panel compares itself against at render time, so the correct tab carries is-active and aria-selected="true" and the correct panel is aria-hidden="false" in the server HTML.
The tab strip is a role="tablist" labelled with the link's title. It walks the Shop link's children in order, and at the child whose one-based index equals the block's position it emits the Featured tab, then the child's own. A child with grandchildren becomes a <button role="tab"> with aria-controls pointing at ShopTab--{handle}; a child with no grandchildren becomes a plain link in the strip, because there's nothing to open. If the loop never reaches the featured position — position eight in a five-item menu — the Featured tab is appended at the end rather than dropped.
shop-tabs.js only has to maintain state from there.Panels follow the same walk. Each child with grandchildren gets a role="tabpanel" with id ShopTab--{handle} and a list of its grandchild links. Where the header's show_images_mobile setting is on, each grandchild link carries a thumbnail resolved by link type: a collection or product link uses the linked object's featured_image, and a page link reads metafields.custom.featured_image off the page, so a content page gets a menu image without a developer. The image goes through the theme's own image-fill renderer at the header's image aspect ratio. A "Shop All" link to the parent's URL sits under the strip.
The featured panel renders up to two banner cards from the block: image, optional link, an overlay with its opacity from a range setting, a title and description with their own font sizes and a shared text color, and a border radius. An aspect preset — wide, square, portrait — maps to 1.778, 1.0 or 0.8; "adapt" leaves the image at its own ratio.
shop-tabs.js is the whole client side, a little under thirty lines. Registered behind customElements.get('shop-tabs'), its connectedCallback binds a click on each [data-shop-tab] button, and activate(handle) toggles is-active and aria-selected across the tabs and is-active and aria-hidden across the panels, matching on the ShopTab-- id. header.liquid enqueues it deferred.
Why This Way
Resolving the first active tab in Liquid is the decision the rest depends on. A tabs component that decides its open panel in JavaScript shows every panel, or none, until the script runs. Rendering is-active, aria-selected and aria-hidden server-side means the drawer is correct as HTML, and the script only ever changes state, never establishes it.
A custom element keeps that behavior scoped. The component owns its own buttons and panels through this.querySelectorAll, so there's no global click handler in the drawer and nothing for the vendor's accordion triggers to collide with. The registration guard means a second load of the script — a duplicated tag, a theme-editor re-render — can't throw on a name already defined.
The trade is in what the strip is. Tab reaches each tab button in document order, because they are buttons; the element adds no arrow-key movement between them, so this is a row of buttons that behaves like tabs rather than a full tabs widget. And the position setting is an index into a menu the merchandising team edits separately: reorder the Shop menu's children and the Featured tab stays second, whatever is now second. By design — and something to know before reordering.
Why Not an App
The mobile half of a navigation app is a mega-menu module: it stores the menu structure in the app, draws the drawer from that, and injects it after the theme has rendered. That gives up the one thing this build kept. The tabs are the mobile Shop link list — the same one the vendor's accordion reads for every other item in the drawer — so a category added in Shopify Navigation appears in the strip with no second admin to update. The featured banner is a theme block in the theme editor, next to every other block the merchandising team already composes the header from. And the thumbnails come off the linked collection, product or page, so there is no image library to keep in sync with the catalog. An app's drawer would have had to reproduce all three from its own copy of the data.
Implementation Notes
- The custom element is registered only when
customElements.get('shop-tabs')returns nothing, so the script can be enqueued twice or re-run by the theme editor without a redefinition error. - Panel ids follow
ShopTab--{handle}for link-derived panels andShopTab--featuredfor the block, andactivatematches on those ids rather than on position, so tab and panel stay paired if either list is reordered. - The featured tab's position is a one-based select compared against
forloop.indexwhile the children are walked; afeatured_tab_renderedflag prevents a second emission, and the fallback append runs only if that flag is still false. - Page links pull
metafields.custom.featured_image; collection and product links pullfeatured_imagefrom the linked object, and every image passes through the theme'simage-fillrenderer with a ratio computed from the header's image aspect setting. - The aspect presets map wide, square and portrait to 1.778, 1.0 and 0.8 before the ratio reaches
image-fill; "adapt" passes the image's ownaspect_ratiothrough instead. - The rest of
nav-item-mobile.liquid— the accordion for levels one through three, its collapsible triggers and the highlight item — is the vendor's; theindex == 0branch is the only addition to the file. - The block carries fifteen settings across two banner cards: position, aspect ratio, overlay opacity, border radius, two images, two titles, two descriptions, two links, heading and text font sizes, and a text color.
Edge Cases
- A Shop menu in which no child has grandchildren still gets a Featured tab when a block exists, and the featured panel becomes the first active one, so the strip never renders with nothing open.
- A featured position beyond the child count appends the Featured tab at the end of the strip instead of silently dropping the block.
- A child link without grandchildren renders as a plain link in the strip, not as a tab with an empty panel behind it.
- Grandchild images are optional twice over: gated by the header's
show_images_mobilesetting, and resolved only for collection, product and page links, so any other link type yields no image object. - A banner card with neither an image nor a title is skipped, so a block with one card filled in renders one card rather than an empty second frame.
aria-selectedandaria-hiddenare set in the server markup and then maintained by the element, so the state is expressed in the DOM before and after the script runs.
Platform Primitives Used
- Custom elements —
<shop-tabs>owns its buttons and panels through its own subtree, with no global handlers. - Metafields — a page's
custom.featured_imagemetafield is how a content page gets a thumbnail in the menu without code. - Liquid section schema and settings — the
mobile_featured_imageblock, its position setting, and the header'sshow_images_mobileand aspect settings are the whole merchandising interface. - Theme blocks — the featured banner is a block on the header section, composed in the theme editor like the header's other panels.
- Section groups — the header lives in the header group, so the tabbed drawer reaches every template at once.
Where It Runs
The header section group, so every template's mobile drawer. On the live storefront the first item of the mobile link list renders as a strip of four link-derived tabs with the Featured tab in second position, the first tab open in the server HTML, and thumbnails on the grandchild links because the header's mobile image setting is on.
What This Demonstrates
- Mega menu and mobile navigation system — the primary capability: the mobile half of a navigation system, where the Shop menu is restructured into tabs from the same link list, a merchandiser's banner is composed into it as a block, and the open state is right before any script runs.
How We Know
The drawer snippet, the header section that declares the block and enqueues the element, and the element's script were read from the theme. The local branch runs to roughly 180 lines of the vendor snippet's 242; the rest of that file is the vendor's. Three client-side task-register records place the work as the mobile side of a wider menu refresh. The strip, the Featured tab's position and the server-resolved active state were checked in the live storefront's markup. Code and record agree on what was built; the reasoning above is documented from the code.
Related Builds
- Mega-menu header with separate mobile menu and image blocks — the same header: the
mobile_main_menulink list this strip reads, the image blocks on the desktop side, and the two-line summary of the tab switcher this page expands on. - Detached mobile drawer with three-level native-details navigation — LUS Brands' mobile drawer on Dawn, where the answer to depth was a native
<details>accordion with the current branch pre-opened in Liquid. - Title-matched mega menu with a full-depth link-list fallback — Nudestix's desktop menu, where blocks are joined to nav items by title rather than spliced in by position.
- Metafield-driven breadcrumbs — LUS Brands using page metafields to carry navigation context the platform doesn't, the same instinct as the page thumbnail here.
The Buy-vs-Build Question
A navigation app can draw a tabbed drawer. What it cannot do is draw it from the link list your merchandising team already edits, or take its banner from the theme editor they already work in, without keeping its own copy of both. The case for owning navigation, and the situations where an app's drawer is the right call, is set out in the mega menu and navigation build-or-buy page.
Provenance & Evidence
- Client: Three Ships — threeshipsbeauty.ca
- Surface: Global — the header's mobile drawer, on every page
- Mounted by: one section group
- Block settings: fifteen, on one
mobile_featured_imageblock - Complexity: High
- Attribution: Built on top of Palo Alto 5.8.0 by Presidio Creative, a paid premium theme the client licenses. The
index == 0tabs branch, the featured banner block andshop-tabs.jsare our additions inside the vendor's drawer snippet; the drawer, its accordion and theimage-fillrenderer are the vendor's. - Status: Live, verified 2026-09-07
- Evidence: three theme files read from the theme, plus three client-side task-register records
- Confidence: Strong — code and client-side records agree on the scope; the reasoning is documented from the code
- Primary capability: Mega menu and mobile navigation system
Ready to Bring Your Shop Menu Up to the Surface?
If your mobile drawer hides the catalog behind two taps of accordion, that's the drawer's default doing your merchandising for you. Contact us today and we'll show you what the first tap could open.