(() => {
for (const root of document.querySelectorAll('[data-snippet="habitat-project-gallery"]')) {
if (root.dataset.initialized) continue;
root.dataset.initialized = "true";
const pictures = [
{
src: "images/living.webp",
alt: "Tall living room with pale seating and soft curtains",
cap: "Material & proportion.",
},
{
src: "images/window.webp",
alt: "Green sofa beneath a tall arched window",
cap: "Room for light.",
},
{
src: "images/arch.webp",
alt: "Arched mirror above timber cabinetry in a planted interior",
cap: "The quieter details.",
},
],
dialog = root.querySelector("dialog");
let current = 0,
last = null;
const show = (i) => {
current = (i + pictures.length) % pictures.length;
const p = pictures[current],
img = dialog.querySelector("img");
img.src = p.src;
img.alt = p.alt;
dialog.querySelector("figcaption").textContent = p.cap;
dialog.querySelector("[role=status]").textContent = current + 1 + " / " + pictures.length;
};
root.addEventListener("click", (e) => {
const a = e.target.closest("[data-gallery-index]");
if (a && !e.ctrlKey && !e.metaKey && typeof dialog.showModal === "function") {
e.preventDefault();
last = a;
show(Number(a.dataset.galleryIndex));
dialog.showModal();
}
});
root.querySelector("[data-close]").addEventListener("click", () => dialog.close());
dialog.addEventListener("close", () => last?.focus());
root.querySelector("[data-prev]").addEventListener("click", () => show(current - 1));
root.querySelector("[data-next]").addEventListener("click", () => show(current + 1));
dialog.addEventListener("keydown", (e) => {
if (["ArrowLeft", "ArrowRight"].includes(e.key)) {
e.preventDefault();
show(current + (e.key === "ArrowRight" ? 1 : -1));
}
});
}
})();
---
// Generated, escaped static markup. Use a distinct prefix for each instance.
const markup = "\u003csection class=\"fct-habitat-project-gallery\" data-snippet=\"habitat-project-gallery\" aria-labelledby=\"fct-habitat-project-gallery-title\"\u003e\u003cdiv class=\"r-inner\"\u003e\u003cdiv class=\"r-gallery-heading\"\u003e\u003cp class=\"r-micro\"\u003eHABITAT / SPACES \u0026 STORIES\u003c/p\u003e\u003ch2 id=\"fct-habitat-project-gallery-title\"\u003eIn good\u003cbr\u003e\u003cspan class=\"r-italic\"\u003ecompany.\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eA study in warm materials,\u003cbr\u003enatural light and room to live.\u003c/p\u003e\u003c/div\u003e\u003cdiv class=\"r-gallery-grid\"\u003e\u003cfigure class=\"r-view r-view-0\"\u003e\u003ca href=\"/snippet-assets/habitat-project-gallery/images/living.webp\" data-gallery-index=\"0\"\u003e\u003cimg src=\"/snippet-assets/habitat-project-gallery/images/living.webp\" width=\"1120\" height=\"747\" alt=\"Tall living room with pale seating and soft curtains\" loading=\"eager\"\u003e\u003c/a\u003e\u003cfigcaption\u003e\u003cspan\u003e01 / Material \u0026 proportion.\u003c/span\u003e\u003cspan aria-hidden=\"true\"\u003e↗\u003c/span\u003e\u003c/figcaption\u003e\u003c/figure\u003e\u003cfigure class=\"r-view r-view-1\"\u003e\u003ca href=\"/snippet-assets/habitat-project-gallery/images/window.webp\" data-gallery-index=\"1\"\u003e\u003cimg src=\"/snippet-assets/habitat-project-gallery/images/window.webp\" width=\"1120\" height=\"747\" alt=\"Green sofa beneath a tall arched window\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cfigcaption\u003e\u003cspan\u003e02 / Room for light.\u003c/span\u003e\u003cspan aria-hidden=\"true\"\u003e↗\u003c/span\u003e\u003c/figcaption\u003e\u003c/figure\u003e\u003cfigure class=\"r-view r-view-2\"\u003e\u003ca href=\"/snippet-assets/habitat-project-gallery/images/arch.webp\" data-gallery-index=\"2\"\u003e\u003cimg src=\"/snippet-assets/habitat-project-gallery/images/arch.webp\" width=\"1120\" height=\"747\" alt=\"Arched mirror above timber cabinetry in a planted interior\" loading=\"lazy\"\u003e\u003c/a\u003e\u003cfigcaption\u003e\u003cspan\u003e03 / The quieter details.\u003c/span\u003e\u003cspan aria-hidden=\"true\"\u003e↗\u003c/span\u003e\u003c/figcaption\u003e\u003c/figure\u003e\u003caside class=\"r-project-index\"\u003e\u003cspan class=\"r-micro\"\u003ePortfolio notes / Example content\u003c/span\u003e\u003cp\u003eThoughtful spaces,\u003cbr\u003emade for \u003cspan class=\"r-italic\"\u003eliving.\u003c/span\u003e\u003c/p\u003e\u003cdl\u003e\u003cdiv\u003e\u003cdt\u003eFocus\u003c/dt\u003e\u003cdd\u003eInterior inspiration\u003c/dd\u003e\u003c/div\u003e\u003cdiv\u003e\u003cdt\u003eMaterials\u003c/dt\u003e\u003cdd\u003eNatural textures and light\u003c/dd\u003e\u003c/div\u003e\u003cdiv\u003e\u003cdt\u003ePhotography\u003c/dt\u003e\u003cdd\u003eSeparate illustrative settings\u003c/dd\u003e\u003c/div\u003e\u003c/dl\u003e\u003ca class=\"r-link\" href=\"contact.html\"\u003eDiscuss a space \u003cspan aria-hidden=\"true\"\u003e↗\u003c/span\u003e\u003c/a\u003e\u003c/aside\u003e\u003c/div\u003e\u003cdialog aria-label=\"Enlarged project image\"\u003e\u003cbutton type=\"button\" data-close\u003eClose ×\u003c/button\u003e\u003cfigure\u003e\u003cimg width=\"1120\" height=\"747\" alt=\"\"\u003e\u003cfigcaption\u003e\u003c/figcaption\u003e\u003c/figure\u003e\u003cdiv class=\"r-dialog-controls\"\u003e\u003cbutton type=\"button\" data-prev\u003ePrevious\u003c/button\u003e\u003cspan role=\"status\"\u003e\u003c/span\u003e\u003cbutton type=\"button\" data-next\u003eNext\u003c/button\u003e\u003c/div\u003e\u003c/dialog\u003e\u003c/div\u003e\u003c/section\u003e";
---
<Fragment set:html={markup} />
<style is:global>
@font-face{font-family:'fct-habitat-project-gallery-sans';src:url('/snippet-assets/habitat-project-gallery/fonts/instrument-sans-400-normal.woff2') format('woff2');font-weight:400;font-display:swap}@font-face{font-family:'fct-habitat-project-gallery-sans';src:url('/snippet-assets/habitat-project-gallery/fonts/instrument-sans-600-normal.woff2') format('woff2');font-weight:600;font-display:swap}@font-face{font-family:'fct-habitat-project-gallery-serif';src:url('/snippet-assets/habitat-project-gallery/fonts/instrument-serif-400-italic.woff2') format('woff2');font-style:italic;font-display:swap}@font-face{font-family:'fct-habitat-project-gallery-display';src:url('/snippet-assets/habitat-project-gallery/fonts/barlow-condensed-700.woff2') format('woff2');font-weight:700;font-display:swap}.fct-habitat-project-gallery{--paper:#f0eee6;--ink:#252a25;--accent:#e1e56b;background:var(--paper);color:var(--ink);font:16px/1.6 'fct-habitat-project-gallery-sans',Arial,sans-serif}.fct-habitat-project-gallery,.fct-habitat-project-gallery *{box-sizing:border-box}.fct-habitat-project-gallery :is(h2,h3,p,figure,ul){margin:0}.fct-habitat-project-gallery :is(h2,h3){line-height:1.05;font-weight:400;letter-spacing:-.055em}.fct-habitat-project-gallery a{color:inherit;text-underline-offset:5px}.fct-habitat-project-gallery button,.fct-habitat-project-gallery input,.fct-habitat-project-gallery select,.fct-habitat-project-gallery textarea{font:inherit}.fct-habitat-project-gallery :is(a,button,summary,input,select,textarea):focus-visible{outline:3px solid #285331;outline-offset:4px}.fct-habitat-project-gallery .r-inner{max-width:1440px;margin:auto;padding:48px clamp(20px,4vw,64px)}.fct-habitat-project-gallery .r-micro{font-size:.8125rem;letter-spacing:.06em;text-transform:uppercase}.fct-habitat-project-gallery .r-rule{border-top:1px solid #252a2555}.fct-habitat-project-gallery .r-link{display:inline-flex;min-height:48px;align-items:center;justify-content:space-between;gap:25px;text-decoration:none;border-bottom:1px solid currentColor;padding:10px 0}.fct-habitat-project-gallery .r-pill{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:12px 24px;border:1px solid currentColor;border-radius:100px;color:inherit;background:transparent;text-decoration:none;cursor:pointer}.fct-habitat-project-gallery .r-italic{font-family:'fct-habitat-project-gallery-serif',Georgia,serif;font-style:italic}.fct-habitat-project-gallery .r-muted{color:#565e54}.fct-habitat-project-gallery img{display:block;max-width:100%;height:auto}.fct-habitat-project-gallery .r-visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
.fct-habitat-project-gallery{background:#edece5}.fct-habitat-project-gallery .r-gallery-heading{display:grid;grid-template-columns:.7fr 1.3fr .7fr;gap:40px;align-items:end;margin-bottom:45px}.fct-habitat-project-gallery .r-gallery-heading>.r-micro{align-self:start;padding-top:10px;max-width:160px}.fct-habitat-project-gallery h2{font-size:clamp(4rem,8vw,8rem);line-height:.9}.fct-habitat-project-gallery .r-gallery-heading>p:last-child{font-size:1.0625rem;max-width:26ch}.fct-habitat-project-gallery .r-gallery-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:50px 40px;align-items:start}.fct-habitat-project-gallery .r-view-0{grid-column:1/-1}.fct-habitat-project-gallery .r-view img{width:100%;object-fit:cover;aspect-ratio:4/3}.fct-habitat-project-gallery .r-view-0 img{aspect-ratio:2.15/1}.fct-habitat-project-gallery .r-view-2{margin-top:120px}.fct-habitat-project-gallery figcaption{display:flex;justify-content:space-between;gap:20px;padding-top:15px;font-size:.875rem}.fct-habitat-project-gallery .r-project-index{padding:35px 0;border-top:1px solid #7b8175;grid-column:1;grid-row:3;margin-top:-45px;max-width:480px}.fct-habitat-project-gallery .r-project-index>p{font-size:clamp(2.3rem,4vw,3.7rem);line-height:1.05;letter-spacing:-.04em;margin:35px 0}.fct-habitat-project-gallery dl{margin:0 0 25px}.fct-habitat-project-gallery dl>div{display:grid;grid-template-columns:1fr 1.5fr;border-top:1px solid #b3b8aa;padding:12px 0;font-size:.875rem}.fct-habitat-project-gallery dd{margin:0}.fct-habitat-project-gallery dialog{max-width:min(1100px,calc(100vw - 24px));max-height:calc(100dvh - 24px);border:0;padding:20px;background:#f0eee6;color:#252a25}.fct-habitat-project-gallery dialog::backdrop{background:#172019e8}.fct-habitat-project-gallery dialog[open]{display:flex;flex-direction:column;gap:15px}.fct-habitat-project-gallery dialog img{max-height:65dvh;object-fit:contain;width:auto;margin:auto}.fct-habitat-project-gallery dialog button{min-height:44px;background:transparent;border:1px solid #697660;padding:8px 16px;cursor:pointer;color:inherit}.fct-habitat-project-gallery [data-close]{align-self:end}.fct-habitat-project-gallery .r-dialog-controls{display:flex;align-items:center;justify-content:space-between;gap:20px}@media(max-width:800px){.fct-habitat-project-gallery .r-gallery-heading{grid-template-columns:1fr;gap:25px}.fct-habitat-project-gallery .r-gallery-heading>.r-micro{max-width:none}.fct-habitat-project-gallery h2{font-size:clamp(4rem,18vw,7rem)}.fct-habitat-project-gallery .r-gallery-grid{grid-template-columns:1fr;gap:30px}.fct-habitat-project-gallery .r-view-0{grid-column:auto}.fct-habitat-project-gallery .r-view-0 img{aspect-ratio:4/3}.fct-habitat-project-gallery .r-view-2{margin-top:0}.fct-habitat-project-gallery .r-project-index{grid-row:auto;grid-column:auto;margin-top:10px;max-width:none}}
</style>
<script is:inline>
(()=>{for(const root of document.querySelectorAll('[data-snippet="habitat-project-gallery"]')){if(root.dataset.initialized)continue;root.dataset.initialized='true';const pictures=[{"src":"/snippet-assets/habitat-project-gallery/images/living.webp","alt":"Tall living room with pale seating and soft curtains","cap":"Material & proportion."},{"src":"/snippet-assets/habitat-project-gallery/images/window.webp","alt":"Green sofa beneath a tall arched window","cap":"Room for light."},{"src":"/snippet-assets/habitat-project-gallery/images/arch.webp","alt":"Arched mirror above timber cabinetry in a planted interior","cap":"The quieter details."}],dialog=root.querySelector('dialog');let current=0,last=null;const show=i=>{current=(i+pictures.length)%pictures.length;const p=pictures[current],img=dialog.querySelector('img');img.src=p.src;img.alt=p.alt;dialog.querySelector('figcaption').textContent=p.cap;dialog.querySelector('[role=status]').textContent=(current+1)+' / '+pictures.length;};root.addEventListener('click',e=>{const a=e.target.closest('[data-gallery-index]');if(a&&!e.ctrlKey&&!e.metaKey&&typeof dialog.showModal==='function'){e.preventDefault();last=a;show(Number(a.dataset.galleryIndex));dialog.showModal();}});root.querySelector('[data-close]').addEventListener('click',()=>dialog.close());dialog.addEventListener('close',()=>last?.focus());root.querySelector('[data-prev]').addEventListener('click',()=>show(current-1));root.querySelector('[data-next]').addEventListener('click',()=>show(current+1));dialog.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();show(current+(e.key==='ArrowRight'?1:-1));}});}})();
</script>
Where this design fits
The gallery uses different image scales to tell a project story instead of placing every photograph in identical cards. Captions and a compact project index preserve a clear reading order on mobile.
Panoramic lead image
Staggered editorial image grid
Keyboard lightbox and focus return
What is included
The HTML ZIP includes a standalone demo, reusable markup, scoped CSS, its small JavaScript runtime, a license and setup notes. The Astro ZIP includes Snippet.astro, self-hosted fonts and asset license notes. It can be imported into an existing Astro page or layout.
Replace the sample photographs
Included photographs are licensed illustrative assets. They are not work created by the demonstration business. Update image paths, dimensions, captions and alternative descriptions for your own files. The ZIP includes the original image-source and license notes.
Add it to an HTML website
Copy snippet.html into the relevant page section.
Load snippet.css after your existing site stylesheet. The class prefix identifies this design.
Load snippet.js once with defer. Keep the included keyboard behavior when adapting interactions.
Replace example content and check mobile widths, zoom, links and keyboard focus in your actual website.
Use it in Astro
Copy Snippet.astro into src/components/, import it into your page or shared layout, and insert the component where the section belongs. Copy the fonts and any images folders into public/snippet-assets/habitat-project-gallery/; the component uses those public paths. Keep each generated prefix unique if you duplicate a component on one page.
Keep the section maintainable
Start with the variables at the top of the CSS for background, text and accent colors. Preserve semantic labels and focus styles. Existing site selectors can still affect imported markup, so test the combined result. Read the complete setup notes.