A small filter should not decide whether a visitor can reach a service page.

In the Laravel storefront I run, Blade renders the available service cards and their links. JavaScript narrows that list. It does not fetch or create the cards.

The implementation has three parts: useful HTML, initially hidden controls, and a script that reveals those controls after wiring up their behaviour.

Render the useful content first

The real Blade template loops over service definitions and renders a card when the corresponding product is available. Each card already contains an ordinary link.

Here is a reduced HTML example with two categories. It illustrates the structure rather than reproducing the whole storefront:

<section data-explorer> <div role="group" data-filter-controls hidden aria-label="Filter services"> <button type="button" data-filter="all" aria-pressed="true">All</button> <button type="button" data-filter="content" aria-pressed="false">Content</button> <button type="button" data-filter="pages" aria-pressed="false">Pages</button> </div> <article data-category="content"> <a href="/blog-articles">Blog articles</a> </article> <article data-category="pages"> <a href="/sales-pages">Sales pages</a> </article> </section> 

The cards start visible. The buttons start hidden because they have no useful behaviour until the script runs.

Attach the behaviour before revealing the controls

This follows the filtering logic in the storefront script:

document.querySelectorAll('[data-explorer]').forEach((explorer) => { const buttons = [...explorer.querySelectorAll('[data-filter]')]; buttons.forEach((button) => { button.addEventListener('click', () => { buttons.forEach((item) => { item.setAttribute('aria-pressed', String(item === button)); }); explorer.querySelectorAll('[data-category]').forEach((item) => { item.hidden = button.dataset.filter !== 'all' && item.dataset.category !== button.dataset.filter; }); }); }); explorer.querySelector('[data-filter-controls]')?.removeAttribute('hidden'); }); 

Run the script after the markup has been parsed. My layout loads the external script with defer.

Each click updates the pressed state and hides cards outside the selected category. Selecting All makes every card visible again. The query stays inside its own explorer, so another explorer on the page has its own controls and cards.

The reveal step comes last. If the script never runs, the initial HTML still contains the complete available list and working links.

Make the hidden state explicit in the stylesheet

My storefront stylesheet has a scoped rule for the hidden attribute. For this reduced example, the equivalent is:

[data-explorer] [hidden] { display: none !important; } 

That keeps a card's layout styling from overriding its hidden state. It also applies to the controls before the script reveals them.

This is a visibility filter, not an authorization boundary. Anything in these cards is already in the page's HTML.

Review the baseline and the enhancement separately

Here is the checklist I would use when reviewing a change to this pattern:

  • With JavaScript disabled, every available card is visible, its links work, and the filter controls stay hidden.
  • With JavaScript enabled, each category shows the expected cards and All restores the list.
  • The pressed state follows the selected button.
  • The buttons can be reached and activated with the keyboard.
  • The site's card and button styles respect the hidden state.

For a small catalog, keeping the baseline useful makes the filter a convenience. Visitors can still reach the service pages without it.