Portfolio Expansion System

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.

Data Model

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
}

Templating

The list is rendered in index.html using a Nunjucks loop. Each item with a detail property receives:

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 Implementation (css/styles.css)

Collapse Animation: The 0fr Grid Pattern

The expand/collapse animation uses grid-template-rows: 0fr → 1fr, which is the modern way to animate content to/from an unknown intrinsic height. The structure is:

.portfolio-detail          ← grid container, transitions grid-template-rows
  .portfolio-detail-text   ← grid child, overflow: hidden

How it works: When grid-template-rows is 0fr, the row track collapses to the minimum size of its children. Because .portfolio-detail-text has overflow: hidden, its automatic minimum size resolves to 0 (per CSS Grid spec), so the track can fully collapse.

Critical constraint: The grid child (.portfolio-detail-text) must have zero box-model contribution — no padding, no margin, no border — in its collapsed state. Any of these would create a non-zero minimum height that the grid track cannot shrink below, causing the list item to be taller than items without a detail section.

This is why padding-top is set to 0 by default on .portfolio-detail-text and only applied when .is-open is active:

.portfolio-detail-text {
  overflow: hidden;    /* allows grid track to collapse to 0 */
  padding-top: 0;      /* no box-model contribution when collapsed */
}

.portfolio-detail.is-open .portfolio-detail-text {
  padding-top: 0.3rem; /* spacing restored when expanded */
}

If you add padding, margin, or border to .portfolio-detail-text, always guard it behind .is-open. Otherwise the collapsed detail will leak height and break the vertical rhythm of the list.

JavaScript Interaction (js/portfolio.js)

The interaction is split into two modes:

1. Desktop (Hover)

2. Mobile / Touch (Full-Row Tap)

Performance Notes