Portfolio Filter Grid — Isotope-Style HTML CSS JS

Portfolio Filter Grid · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Data-attribute filtering
Items declare data-cat and tabs data-filter; filterItems shows matches and hides the rest with a class — no library.
Staggered scale-in
Each visible item gets an incremental animation-delay and a re-triggered pf-in keyframe, so matches cascade into the grid.
Reflow-restarted animation
The remove-class / read offsetWidth / add-class trick replays the entrance on every filter change.
Compositor-friendly motion
The reveal animates only transform and opacity, so it stays smooth and survives the Tailwind/React export.
Per-tab counts
Tabs display the number of items in each category, setting expectations before a click.
Active tab state
The selected tab highlights in the accent colour, a clear indication of the current filter.
Empty state
If a filter matches nothing, a friendly message replaces the blank grid.
Responsive grid
A 3-column grid of square aspect-ratio tiles collapses to 2 columns on mobile, with a legibility gradient per tile.

About this UI Snippet

Portfolio Filter Grid — Category Tabs, Staggered Reveal & Per-Tab Counts

Screenshot of the Portfolio Filter Grid snippet rendered live

A filterable grid — the "Isotope" pattern — lets visitors narrow a portfolio, gallery, or catalogue by category with a single tap, and the items that match animate into place. It is a staple of portfolio sites, case-study pages, and project galleries. This snippet implements it in plain HTML, CSS, and vanilla JavaScript with a smooth, dependency-free reveal: category tabs with counts, a responsive grid, a staggered scale-in for matching items, and an empty state.

Tag-based filtering

Every grid item carries a data-cat attribute, and every tab a data-filter. filterItems moves the active class to the clicked tab, then loops the items: those whose category matches the filter (or the special all) stay visible, the rest get a .hide class that sets display: none. This is the simplest robust filtering model — no layout library, no DOM reordering, just a class toggle per item driven by data attributes.

Staggered scale-in reveal

Matching items don't just appear — they animate in. For each visible item, filterItems sets an animation-delay based on a running visible-count, then re-triggers a pf-in keyframe (opacity + scale + translate) using the remove-class / force-reflow / add-class trick. The result is a pleasing cascade where the filtered items pour into the grid left-to-right, every time you change tabs. Because the animation uses only transform and opacity, it runs on the compositor and exports cleanly to utility frameworks (which animate transforms but not layout).

Why display:none over a FLIP reorder

True Isotope animates items physically sliding to new positions (the FLIP technique), which is impressive but fragile and heavy. This snippet deliberately uses display: none for non-matching items and an entrance animation for matching ones — the matching items fade and scale in while the grid reflows instantly underneath. It is far simpler, has no edge cases, and reads as polished for the vast majority of portfolio use cases.

Counts and empty state

Each tab shows a count of items in that category, helping users gauge what's there before clicking. After filtering, filterItems reveals a "No projects in this category" message if nothing matched — a graceful end state instead of a blank grid. The grid itself is a responsive three-column layout that collapses to two on mobile, with square aspect-ratio tiles and a legibility gradient over each.

Swap the tiles for real project thumbnails and link them out. Pair this with an image lightbox for full views, a masonry grid for varied heights, or a chip filter for multi-tag filtering.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA portfolio appears with category tabs (All, Design, Development, Photography) above a 3-column grid of nine project tiles.
  2. 2
    Click a categoryClick "Design" — only design projects remain and they scale-in one after another in a staggered cascade.
  3. 3
    Switch tabsEach tab change re-runs the entrance animation for the newly matching items, so filtering always feels alive.
  4. 4
    Read the countsEach tab shows how many projects it contains, so visitors know what to expect before clicking.
  5. 5
    Back to AllClick "All" to bring every project back, again with the staggered reveal.
  6. 6
    Swap in real workReplace each tile's gradient and emoji with a project thumbnail and set its data-cat; the filtering needs no changes.

Real-world uses

Common Use Cases

Portfolio and case-study galleries
The classic use — filter projects by discipline. Pair with an image lightbox for full-size views.
Product and shop catalogues
Filter products by type from a grid; for multi-tag filtering combine with a chip filter or faceted filter sidebar.
Photo and media galleries
Group shots by album or subject; works alongside a masonry grid for varied aspect ratios.
Blog and resource libraries
Filter articles or downloads by topic, with counts showing how much each category holds.
Team and directory pages
Filter people by department or role; reuse the tabs + grid with a team card layout.
Component / template showcases
Filter UI examples by type — the same pattern this very snippet library could use to browse categories.

Got questions?

Frequently Asked Questions

Add a tab with a data-filter value and grid items with the matching data-cat. filterItems works generically off those attributes, so no code changes are needed. Update each tab's count span (or compute counts in JS on load by counting items per data-cat).

Give items a space-separated data-cat (e.g. "design web") and change the match test to check whether the item's tag list includes the active filter. For multi-select filtering (show items matching ANY selected tag), track a set of active filters and show an item if its tags intersect that set — pair it with a chip filter UI.

Position-shuffle animations use the FLIP technique (measure first/last positions, animate the delta), which is impressive but adds significant complexity and edge cases, especially with responsive grids. Using display: none for non-matches plus an entrance animation for matches is far simpler, robust, and looks polished — the right trade-off for most portfolios.

Use real <button> tabs (this snippet does) so they are keyboard-operable, and add aria-pressed to reflect the active filter. Consider grouping the tabs with role="group" and an accessible label, and announcing the result count via an aria-live region so screen-reader users hear "Showing 3 design projects" after filtering.

In React, hold the active filter in useState and derive the visible items by filtering the array in render — no class toggling; use a CSS animation keyed on the filter (or a key change) to replay the entrance. In Vue, use a computed filtered list with <transition-group> for animation. In Angular, an *ngFor over a filtered array with @animations. The keyframe and grid CSS port unchanged.