Product gallery
A main image with clickable thumbnails, pure CSS, keyboard-friendly, up to six images.
Gallery
<div class="mn-gallery" role="group" aria-label="Product images" style="max-width:26rem">
<input type="radio" name="gal" id="gal-1" aria-label="Image 1: three vases" checked><input type="radio" name="gal" id="gal-2" aria-label="Image 2: glaze detail"><input type="radio" name="gal" id="gal-3" aria-label="Image 3: the studio">
<div class="mn-gallery-main">
<div class="mn-media mn-media-portrait"><svg viewBox="0 0 400 320" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Illustration: three ceramic vases" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="320" fill="var(--mn-surface-2)"/>
<rect y="262" width="400" height="58" fill="color-mix(in srgb, var(--mn-fg) 7%, var(--mn-surface-2))"/>
<path d="M88 262c-30-24-34-70-10-96 8-9 10-20 6-34h40c-4 14-2 25 6 34 24 26 20 72-10 96Z" fill="var(--mn-primary)"/>
<path d="M186 262c-22-4-36-24-34-58 2-40 22-70 26-110h40c4 40 24 70 26 110 2 34-12 54-34 58Z" fill="var(--mn-surface)" stroke="var(--mn-fg)" stroke-width="2.5"/>
<path d="M170 150h56" stroke="var(--mn-fg)" stroke-width="2.5" opacity=".35"/>
<path d="M270 262c-6-30 0-64 22-84h52c22 20 28 54 22 84Z" fill="var(--mn-accent)"/>
<ellipse cx="318" cy="178" rx="26" ry="6" fill="color-mix(in srgb, #000 22%, var(--mn-accent))"/></svg></div>
<div class="mn-media mn-media-portrait"><svg viewBox="0 0 600 450" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Glaze detail" xmlns="http://www.w3.org/2000/svg"><rect width="600" height="450" fill="var(--mn-surface)"/><g fill="none" stroke="var(--mn-fg)" stroke-width="1.5"><circle cx="300" cy="225" r="12"/><circle cx="300" cy="225" r="26"/><circle cx="300" cy="225" r="40"/><circle cx="300" cy="225" r="54"/><circle cx="300" cy="225" r="68"/><circle cx="300" cy="225" r="82"/><circle cx="300" cy="225" r="96"/><circle cx="300" cy="225" r="110"/><circle cx="300" cy="225" r="124"/><circle cx="300" cy="225" r="138"/><circle cx="300" cy="225" r="152"/><circle cx="300" cy="225" r="166"/><circle cx="300" cy="225" r="180"/><circle cx="300" cy="225" r="194"/></g><circle cx="300" cy="225" r="12" fill="var(--mn-primary)"/></svg></div>
<div class="mn-media mn-media-portrait"><svg viewBox="0 0 600 400" preserveAspectRatio="xMidYMid slice" role="img" aria-label="The studio’s hills" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="hills-sky-1" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="var(--mn-surface-2)"/><stop offset="1" stop-color="var(--mn-surface)"/></linearGradient></defs>
<rect width="600" height="400" fill="url(#hills-sky-1)"/>
<circle cx="430" cy="120" r="46" fill="var(--mn-accent)" opacity=".85"/>
<path d="M0 250c80-40 150-70 230-40s140 20 210-20 120-10 160 10v200H0Z" fill="color-mix(in srgb, var(--mn-primary) 30%, var(--mn-surface))"/>
<path d="M0 300c90-30 170-40 250-10s170 10 230-10 90 0 120 10v110H0Z" fill="color-mix(in srgb, var(--mn-primary) 55%, var(--mn-surface))"/>
<path d="M0 350c120-20 220-24 320-6s200 4 280-6v62H0Z" fill="var(--mn-primary)"/></svg></div>
</div>
<div class="mn-gallery-thumbs">
<label class="mn-gallery-thumb" for="gal-1"><span class="mn-media mn-media-square"><svg viewBox="0 0 400 320" preserveAspectRatio="xMidYMid slice" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="320" fill="var(--mn-surface-2)"/>
<rect y="262" width="400" height="58" fill="color-mix(in srgb, var(--mn-fg) 7%, var(--mn-surface-2))"/>
<path d="M88 262c-30-24-34-70-10-96 8-9 10-20 6-34h40c-4 14-2 25 6 34 24 26 20 72-10 96Z" fill="var(--mn-primary)"/>
<path d="M186 262c-22-4-36-24-34-58 2-40 22-70 26-110h40c4 40 24 70 26 110 2 34-12 54-34 58Z" fill="var(--mn-surface)" stroke="var(--mn-fg)" stroke-width="2.5"/>
<path d="M170 150h56" stroke="var(--mn-fg)" stroke-width="2.5" opacity=".35"/>
<path d="M270 262c-6-30 0-64 22-84h52c22 20 28 54 22 84Z" fill="var(--mn-accent)"/>
<ellipse cx="318" cy="178" rx="26" ry="6" fill="color-mix(in srgb, #000 22%, var(--mn-accent))"/></svg></span></label>
<label class="mn-gallery-thumb" for="gal-2"><span class="mn-media mn-media-square"><svg viewBox="0 0 600 450" preserveAspectRatio="xMidYMid slice" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><rect width="600" height="450" fill="var(--mn-surface)"/><g fill="none" stroke="var(--mn-fg)" stroke-width="1.5"><circle cx="300" cy="225" r="12"/><circle cx="300" cy="225" r="26"/><circle cx="300" cy="225" r="40"/><circle cx="300" cy="225" r="54"/><circle cx="300" cy="225" r="68"/><circle cx="300" cy="225" r="82"/><circle cx="300" cy="225" r="96"/><circle cx="300" cy="225" r="110"/><circle cx="300" cy="225" r="124"/><circle cx="300" cy="225" r="138"/><circle cx="300" cy="225" r="152"/><circle cx="300" cy="225" r="166"/><circle cx="300" cy="225" r="180"/><circle cx="300" cy="225" r="194"/></g><circle cx="300" cy="225" r="12" fill="var(--mn-primary)"/></svg></span></label>
<label class="mn-gallery-thumb" for="gal-3"><span class="mn-media mn-media-square"><svg viewBox="0 0 600 400" preserveAspectRatio="xMidYMid slice" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="hills-sky-2" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="var(--mn-surface-2)"/><stop offset="1" stop-color="var(--mn-surface)"/></linearGradient></defs>
<rect width="600" height="400" fill="url(#hills-sky-2)"/>
<circle cx="430" cy="120" r="46" fill="var(--mn-accent)" opacity=".85"/>
<path d="M0 250c80-40 150-70 230-40s140 20 210-20 120-10 160 10v200H0Z" fill="color-mix(in srgb, var(--mn-primary) 30%, var(--mn-surface))"/>
<path d="M0 300c90-30 170-40 250-10s170 10 230-10 90 0 120 10v110H0Z" fill="color-mix(in srgb, var(--mn-primary) 55%, var(--mn-surface))"/>
<path d="M0 350c120-20 220-24 320-6s200 4 280-6v62H0Z" fill="var(--mn-primary)"/></svg></span></label>
</div>
</div>Installation
Everything ships in one stylesheet. There is nothing to import per component.
<link rel="stylesheet" href="https://mian.ro/dist/mian.css">Prefer to own the files? /r/product-gallery.json is a registry item with this component's CSS and example markup, ready to copy into your project with any tool that reads the open registry-item format.
Class reference
| Class / attribute | Description |
|---|---|
| mn-gallery | The wrapper, with one radio input per image first. |
| mn-gallery-main | The large images (mn-media), in the same order as the radios. |
| mn-gallery-thumbs | The thumbnails: one <label for> per radio. |
| mn-gallery-thumb | A thumbnail label. |
Accessibility
- The radios are a real group: Tab to it, then arrow keys switch images. Name the group (role="group" aria-label) and each radio (aria-label).
- Give the main images alt text; thumbnails repeat them, so their images can be decorative.