This system uses a combination of Eleventy data, Nunjucks templating, vanilla CSS, and minimal JavaScript to create a performant hover-expand interaction for the portfolio list.
All portfolio data is stored in _data/portfolio.json.
{
"name": "Ansa Biotechnologies",
"url": "https://www.ansabio.com/",
"detail": "Enzymatic DNA synthesis that eliminates the length and accuracy ceiling of phosphoramidite chemistry.",
"press": {
"label": "press",
"url": "https://www.biospace.com/article/releases/ansa-biotechnologies-closes-oversubscribed-68-million-series-a-financing-to-power-the-next-era-of-dna-enabled-industries/?s=80"
},
"acquired": true,
"tba": true
}
detail: The description that appears on expansion. If missing, the item won't be expandable.acquired: Adds an "(Acquired)" badge.tba: Adds a "(TBA)" badge and mutes the styling.The list is rendered in index.html using a Nunjucks loop. Each item with a detail property receives:
class="portfolio-item"aria-expanded="false".portfolio-detail div.The loop also generates the Structured Data (JSON-LD) block at the bottom of the page, ensuring the SEO metadata is always in sync with the visible list.
css/styles.css)height and opacity transitions for a smooth expansion.aria-expanded="true" and aria-hidden="false".1em square flexbox to ensure rotate(90deg) doesn't cause visual drift.will-change on all elements to conserve GPU memory, as the number of items is small ( ~15).js/portfolio.js)The interaction is split into two modes:
mouseenter and mouseleave triggers.offsetHeight once at initialization and caches it in a Map.(hover: none) devices, a monospace chevron (▶) is injected via JS.<li> row (number + chevron + text) acts as the toggle trigger for expansion.list-style-position: inside is applied to #portfolio-list on mobile.tabindex="-1" to prevent it from becoming a redundant focus stop.<a> tag within the row still navigates normally; the expansion toggle only triggers if the tap occurs on the number, chevron, or non-link text.pretext or esbuild are required.