DEV Community

Javed Iqbal
Javed Iqbal

Posted on Edited on AI-assisted

A filterable table that still works with JavaScript off

I made a small page for health.dailyappslab.com that filters 259 US fast-food menu items by protein and calories. The filtering is the easy part. What I cared about was that the page still shows everything when the script doesn't run.

The data is one TypeScript file. Each item has a chain, a name, calories, protein and a few optional fields like sodium. Each chain entry also stores the nutrition page the numbers came from and the date I read it, so the source sits next to the data instead of in my memory.

Astro renders every item as a table row at build time. Each row carries its values as data attributes:

<tr data-chain="subway" data-meal="main" data-protein="31" data-cal="400" data-per="7.8">
Enter fullscreen mode Exit fullscreen mode

"Protein per 100 calories" is computed once in the data file, not in the browser:

export const perHundred = (i: Item) => Math.round((i.protein / i.cal) * 1000) / 10;
Enter fullscreen mode Exit fullscreen mode

With JavaScript off, you get the full table, already sorted by that number. Crawlers that don't run scripts see the same page.

The script only hides and reorders rows. It reads the form, filters the rows by their data attributes, sorts what's left, and sets hidden on the rest:

all.forEach((tr) => (tr.hidden = true));
shown.forEach((tr) => {
  tr.hidden = false;
  tbody.appendChild(tr);
});
Enter fullscreen mode Exit fullscreen mode

There is no framework on the page and no fetch call. The table is already in the DOM, so the script only moves rows around.

The filters also live in the URL. After each change the script writes them with history.replaceState and URLSearchParams, and on load it reads them back, so a filtered list can be shared as a link. The form is a plain GET form to the same path, which makes it behave sensibly as markup. Since the site is static, though, the server ignores the query string and only the script acts on it.

Two limits. The numbers are a snapshot: I copied them from each chain's own US nutrition page on the dates shown in the page, so they will drift when menus change. And they are US menus only. All 259 rows were checked against the article tables they came from before the page went live. If the data grows, a script that runs that comparison on every build is what I'd add first.

If you want to see it run, the page is here: https://health.dailyappslab.com/macroscout/protein-finder/. It's free and needs no signup.

Disclosure: this is a page on my own site, built at Daily Apps Lab, and I used AI tools to help draft this post.

Top comments (0)