Photo Grid Gallery
An even, responsive grid of captioned photos with no JavaScript.
6 frameworksBeginner
A grid whose visible tiles switch with single-select category chips.
<!--
Category chips are single-select, but they are not a form question, so they use
aria-pressed rather than radios. The pressed fill keys off the attribute
itself (aria-[pressed=true]:...) so the state a screen reader reads and the
fill a sighted user sees are one thing, not two that can drift.
The tab switching is wired in the React/TS tabs.
-->
<div class="w-full">
<div class="flex flex-wrap gap-2" role="group" aria-label="Filter by category">
<button type="button" aria-pressed="true" class="rounded-full border border-gray-300 px-3 py-1.5 text-[0.8125rem] font-medium text-gray-700 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 aria-[pressed=true]:border-blue-700 aria-[pressed=true]:bg-blue-700 aria-[pressed=true]:text-white dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950">All</button>
<button type="button" aria-pressed="false" class="rounded-full border border-gray-300 px-3 py-1.5 text-[0.8125rem] font-medium text-gray-700 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 aria-[pressed=true]:border-blue-700 aria-[pressed=true]:bg-blue-700 aria-[pressed=true]:text-white dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950">Nature</button>
<button type="button" aria-pressed="false" class="rounded-full border border-gray-300 px-3 py-1.5 text-[0.8125rem] font-medium text-gray-700 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 aria-[pressed=true]:border-blue-700 aria-[pressed=true]:bg-blue-700 aria-[pressed=true]:text-white dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950">Urban</button>
</div>
<ul class="mt-3 grid list-none grid-cols-2 gap-3 p-0 sm:grid-cols-3 lg:grid-cols-4">
<li>
<figure class="m-0">
<div class="aspect-square w-full rounded-xl bg-gradient-to-br from-emerald-400 to-teal-600" role="img" aria-label="Emerald to teal gradient"></div>
<figcaption class="pt-2 text-[0.8125rem] text-gray-700 dark:text-gray-300">Forest</figcaption>
</figure>
</li>
<li>
<figure class="m-0">
<div class="aspect-square w-full rounded-xl bg-gradient-to-br from-slate-400 to-slate-700" role="img" aria-label="Slate gradient"></div>
<figcaption class="pt-2 text-[0.8125rem] text-gray-700 dark:text-gray-300">Skyline</figcaption>
</figure>
</li>
</ul>
</div>| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | CategorisedTile[] | - | The array of items to render. |
categoriesrequired | { id: string; label: string }[] | - | Categories |
className | string | - | Additional classes merged onto the root element. |
The chips are `aria-pressed` toggles, not radios: category selection re-renders a list in place rather than answering a form question. The pressed fill keys off the attribute itself, so the state a screen reader reads and the fill a sighted user sees are one thing that cannot drift apart.