Mega-Menu Product Rail (v2): A Library-Free Slider in a Theme Asset, Constructed per Panel When the Panel Is Seen
The second generation of Nudestix's menu rail — a merchandiser title, a shop-all link and a track of navigation cards — keeps its slider in megamenu-slider.js, a small class that measures slide width at move time and is constructed per panel on first intersection.
A product rail inside a mega-menu panel starts life hidden — the panel is display:none until the header opens it, so slide widths measure as zero at load. megamenu-slider.js defines a MegamenuSlider class that measures offsetWidth and the computed grid gap on every move rather than at construction, and each .megamenu-v2-container is constructed by an IntersectionObserver behind a dataset flag. No slider library, and no script inline in the snippet.
The Problem
A slider inside a mega-menu panel has a problem an ordinary carousel doesn't: the panel isn't visible when the page loads. The header renders on every page, its dropdowns render with it, and every one of them is display:none until the shopper opens it. Anything that measures a slide's width at DOMContentLoaded measures zero, and a slider whose transform math was computed against zero moves nothing when the panel finally opens.
This is the second rail of its kind in the Nudestix header, and the brief for it reads as a list of things the panel had to own: a title the merchandiser writes, a shop-all link with a sensible default, an arbitrary product count, and a looping track written in the theme's own asset rather than assembled from a library.
The Constraint
Mega-menu panels are created and revealed by the header's own JavaScript, and a panel nobody has opened has no laid-out width to read. Slide width can't be known until the panel is visible, so every transform has to be computed from a measurement taken at the moment of the move, not from a value cached at construction.
Touch had to respect the page. A menu panel on mobile sits over content that still scrolls, so a horizontal swipe on the rail has to be told apart from a vertical scroll before the rail claims the gesture, or the shopper can't scroll past the menu. The theme editor reloads header sections, so constructing a slider twice on the same container had to be impossible rather than merely unlikely. And the card had to be the theme's navigation-specific product card, with its add button, not a card of the rail's own.
What We Built
megamenu-collection-custom.liquid renders a .megamenu-v2 block: a merchandiser title from block.settings.title, a "SHOP ALL {title}" link pointing at block.settings.shopall_url with a /collections/makeup default, a .slider-track of card-product-nav slides — a navigation-specific variant of the product card, rendered with show_add_button — limited by section.settings.products_to_show, and a previous and next button pair. The "shop all" prefix is special-cased so a merchandiser who titles a block "shop all" doesn't get the phrase doubled. The snippet ends with a comment recording that its script lives in megamenu-slider.js.
megamenu-slider.js defines window.MegamenuSlider, loaded once from theme.liquid with defer. A move translates the track by index * (slideWidth + gap), where slideWidth is the first slide's offsetWidth and gap is read from the track's computed style — both measured when the move happens, which is the only time the panel is guaranteed to be visible.
Touch support tracks start and end X and Y. It only calls preventDefault() when horizontal movement exceeds vertical, so a vertical drag inside the panel scrolls the page as it should, and a 50px threshold separates a swipe from a tap. A resize listener re-applies the current transform with transitions disabled, so a rotated phone re-measures without the rail visibly sliding into its new position.
initializeMegamenuSliders() finds every .megamenu-v2-container, observes each with an IntersectionObserver, and constructs a MegamenuSlider on first intersection — guarded by a data-slider-initialized dataset flag so the same container is never wired twice. Where the browser has no IntersectionObserver, every slider is constructed immediately instead.
Why This Way
Measuring at move time is the whole trick. A slider that caches slide width at construction is correct only if it was constructed while visible, and inside a menu panel that's never true at load. Reading offsetWidth and the computed gap on every move gives a transform that's right regardless of when the panel was first opened, or whether the viewport changed in between.
Writing the slider rather than reaching for one was a decision about what the rail needs. It needs to move a track left and right by a measured distance, respond to a swipe, and not fight the page. That's a few dozen lines, and writing them keeps the rail's behavior in the same asset the rail loads. The dataset flag is what makes the construction idempotent, which is what the theme editor's section reloads demand.
The cost is the one every hand-rolled slider carries: it's ours. There are no upstream releases fixing edge cases we haven't hit, and every affordance the rail has is one somebody here wrote and somebody here maintains.
Why Not an App
Mega-menu apps render their own panels and, where they offer product rails, their own product cards. The requirement here was the opposite: the dropdown had to render the theme's own navigation card with the theme's own add button, so the price, the badge and the add-to-cart behavior match the collection grid the shopper lands on next. An injected menu has to reproduce those, and it drifts from the theme's card the first time either changes.
There's also the composition question. The panel this rail sits in is a theme-editor block bound to a nav item, and the rail's title, link and product count are that block's settings. Keeping the rail in the theme keeps it inside the merchandiser's existing menu workflow rather than adding a second admin surface beside it.
Implementation Notes
- The snippet ends with an explicit comment naming
megamenu-slider.jsas where its script lives, so the next developer finds the class from the markup. - The asset is loaded once from
theme.liquidwithdefer; one asset serves every panel the merchandiser has configured. - Without IntersectionObserver,
initializeMegamenuSliders()constructs every slider immediately. - The card is
card-product-nav, a navigation-specific variant of the product card, rendered withshow_add_button, not the standardcard-product. - The "SHOP ALL" link is special-cased for a title of "shop all", and
shopall_urldefaults to/collections/makeupwhen the merchandiser leaves it blank. - The single call site is
header-mega-menu.liquid, which is where the block settings the snippet reads are defined.
Edge Cases
- Missing track, slider or slides: the constructor returns early, so a panel with no products renders its title and link and never throws.
- A vertical drag inside the panel: the touch handler compares axes before preventing default, so the page scrolls and the rail stays put.
- Empty collection: placeholder cards render rather than an empty track with live arrows.
- Viewport resize or rotation: the transform is re-applied with transitions off, so the rail snaps to its re-measured position rather than animating there.
- Second observer hit or editor reload on the same container: the
data-slider-initializedflag stops a second construction.
Platform Primitives Used
- Intersection Observer API — one observer per
.megamenu-v2-container; construction on first intersection, unobserved afterwards. - Liquid snippets —
megamenu-collection-custom.liquidis a snippet rendered by the header's block loop, reading the block's title, link and count settings.
Where It Runs
In the header, so on every page of the storefront. The snippet has one call site in header-mega-menu.liquid, and every mega-menu block that carries a product rail gets a .megamenu-v2-container of its own; the asset is loaded once and wires up however many panels the merchandiser has configured.
What This Demonstrates
- Collection merchandising: rails, tiles and in-grid promotion — the primary capability: a merchandiser-titled product rail with a shop-all link and a configurable product count, using the theme's navigation card.
- Mega menu and mobile navigation system — the rail is a panel inside the title-matched mega menu; it's the product half of "the menu is a shoppable surface."
- Media and asset loading strategy — the slider is constructed on first intersection rather than at load, and its asset is loaded once from the layout with
defer.
How We Know
Two files read from the live theme: the snippet and the asset it loads. Documented from the code; no client-side record of the decision survives. The comment left in the snippet records the move out of inline script; the reasons for the move are reconstructed from what the asset does and from the shape of the first-generation rail it replaced.
Related Builds
- Title-matched mega menu with a full-depth link-list fallback — the menu this rail lives inside: how a theme-editor block becomes a nav item's dropdown at all.
- Mega-menu header with separate mobile menu and image blocks — Three Ships' menu on a licensed theme, where the panels carry imagery rather than a product rail.
- Layout shell rewritten as a load-order contract — the same storefront's layout, where this asset is enqueued with
deferalongside the rest of the load order. - Rotating announcement carousel above the header, with pause control — another small rotating component on Nudestix, sitting just above this one in the header stack.
The Buy-vs-Build Question
A mega-menu app buys panels and product cards rendered by the vendor, outside the theme's card and cart path. Building the rail bought the theme's own navigation card inside the merchandiser's existing block workflow, and cost a slider that's ours to maintain and extend. The wider navigation decision is argued at mega menu and navigation: buy or build?.
Provenance & Evidence
- Client: Nudestix — nudestix.com
- Surface: Global — the header, on every template
- Templates served: all of them, through the header section group
- Complexity: Medium
- Scale: two theme files — one snippet, one asset
- Attribution: Deploi-authored. The snippet and the slider class are ours. They run inside a theme built on Dawn 6.0.2 and render the theme's
card-product-navsnippet rather than a card of their own. - Status: Live, verified 2026-09-07
- Evidence: two theme files read from the theme
- Confidence: Moderate — documented from the code; no client-side record of the decision survives
- Primary capability: Collection merchandising: rails, tiles and in-grid promotion
Ready to Merchandise the Menu Itself?
If your dropdown is a list of links and your products are a click further away than they need to be, the menu is a surface you're not using. Contact us today and we'll show you what a product rail inside the panel looks like on your theme — with your cards, not a vendor's.