<ol class="ui-scroll-grid">
  <li>
    <ol>
      <li id="r1-c1">You can enter Nowhere from Anywhere.</li>
      <li id="r1-c2">I came from the North.</li>
      <li id="r1-c3">I am now here.<br>In Nowhere.</li>
      <li id="r1-c4">The map, a sheet of paper, with a single sentence.</li>
      <li id="r1-c5">&OpenCurlyDoubleQuote;The only way out is in&CloseCurlyDoubleQuote;</li>
    </ol>
  </li>
  <li>
    <ol>
      <li id="r2-c1">My canister if half full.<br>Or half empty?</li>
      <li id="r2-c2">The path can lead to Anywhere.<br>Or Everywhere.</li>
      <li id="r2-c3">Just keep walking.<br>Aimlessly.<br>Or determined.</li>
      <li id="r2-c4">Did I just spot Yin and Yang?</li>
      <li id="r2-c5">Nothing is permanent.</li>
    </ol>
  </li>
  <li>
    <ol>
      <li id="r3-c1">You can enter Nowhere from Everywhere.</li>
      <li id="r3-c2">I came from the West.</li>
      <li id="r3-c3">I am now in the Middle of Nowhere.</li>
      <li id="r3-c4">I came from the East.</li>
      <li id="r3-c5">You can enter Nowhere from Everywhere.</li>
    </ol>
  </li>
  <li>
    <ol>
      <li id="r4-c1">Nothing is permanent.</li>
      <li id="r4-c2">Did I just spot Yang and Yin?</li>
      <li id="r4-c3">Just keep walking.<br>Determined.<br>Or aimlessly.</li>
      <li id="r4-c4">The path can lead to Everywhere.<br>Or Anywhere.</li>
      <li id="r4-c5">My canister if half empty.<br>Or half full?</li>
    </ol>
  </li>
  <li>
    <ol>
      <li id="r5-c1">&OpenCurlyDoubleQuote;The only way out is in&CloseCurlyDoubleQuote;</li>
      <li id="r5-c2">The map, a sheet of paper, with a single sentence.</li>
      <li id="r5-c3">I am now here.<br>In Nowhere.</li>
      <li id="r5-c4">I came from the South</li>
      <li id="r5-c5">You can enter Nowhere from Anywhere.</li>
    </ol>
  </li>
</ol>

<nav class="ui-scroll-grid-nav">
  <a href="#r1-c1" title="Row 1, Cell 1"></a>
  <a href="#r1-c2" title="Row 1, Cell 2"></a>
  <a href="#r1-c3" title="Row 1, Cell 3"></a>
  <a href="#r1-c4" title="Row 1, Cell 4"></a>
  <a href="#r1-c5" title="Row 1, Cell 5"></a>
  <a href="#r2-c1" title="Row 2, Cell 1"></a>
  <a href="#r2-c2" title="Row 2, Cell 2"></a>
  <a href="#r2-c3" title="Row 2, Cell 3"></a>
  <a href="#r2-c4" title="Row 2, Cell 4"></a>
  <a href="#r2-c5" title="Row 2, Cell 5"></a>
  <a href="#r3-c1" title="Row 3, Cell 1"></a>
  <a href="#r3-c2" title="Row 3, Cell 2"></a>
  <a href="#r3-c3" title="Row 3, Cell 3"></a>
  <a href="#r3-c4" title="Row 3, Cell 4"></a>
  <a href="#r3-c5" title="Row 3, Cell 5"></a>
  <a href="#r4-c1" title="Row 4, Cell 1"></a>
  <a href="#r4-c2" title="Row 4, Cell 2"></a>
  <a href="#r4-c3" title="Row 4, Cell 3"></a>
  <a href="#r4-c4" title="Row 4, Cell 4"></a>
  <a href="#r4-c5" title="Row 4, Cell 5"></a>
  <a href="#r5-c1" title="Row 5, Cell 1"></a>
  <a href="#r5-c2" title="Row 5, Cell 2"></a>
  <a href="#r5-c3" title="Row 5, Cell 3"></a>
  <a href="#r5-c4" title="Row 5, Cell 4"></a>
  <a href="#r5-c5" title="Row 5, Cell 5"></a>
</nav>
body {
  font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
  font-weight: normal;
  height: 100dvh;
  margin: 0;
  scrollbar-color: #0003 #0001;
  scrollbar-width: thin;
  width: 100vw;
}

.ui-scroll-grid {
  container-type: inline-size;
  height: 100dvh;
  list-style-type: none;
  margin: 0;
  overflow: clip auto;
  padding: 0;
  scroll-behavior: smooth;
  scroll-snap-type: y mandatory;
  width: 100vw;

  ol {
    display: flex;
    list-style-type: none;
    overflow: auto clip;
    padding: 0;
    scroll-behavior: smooth;
    scroll-snap-type: x mandatory;
  }

  li {
    background: var(--bg);
    box-sizing: border-box;
    color: #FFFD;
    display: grid;
    flex: 0 0 calc(100vw - 1rem);
    font-size: 7.5cqi;
    height: 100dvh;
    padding-inline: 1rem;
    place-content: center;
    scroll-snap-align: start;
    text-align: center;
    text-wrap: balance;
  }

  & > li {
    &:nth-of-type(1) { --bg: #123; }
    &:nth-of-type(2) { --bg: #187; }
    &:nth-of-type(3) { --bg: #C90; }
    &:nth-of-type(4) { --bg: #D32; }
    &:nth-of-type(5) { --bg: #594; }
  }
}

.ui-scroll-grid-nav {
  bottom: 2rem;
  display: grid;
  gap: .25rem;
  grid-template-columns: repeat(5, 1fr);
  position: fixed;
  right: 2rem;

  a {
    aspect-ratio: 1;
    background-color: #FFFD;
    border-radius: 50%;
    display: block;
    width: .5rem;
    &.active {
      background-color: #0EF;
    }
  }
}
function handleNavigation(links, lists, activeClass = 'active') {
  if (!links?.length || !lists?.length) return;

  let linkClicked = false;
  let currentRow = 0;
  let currentCol = 0;

  const syncScroll = target => {
    const parent = target.parentNode;
    lists.forEach(ol => ol !== parent && (ol.scrollLeft = parent.scrollLeft));
  };

  const navigateToCell = (row, col) => {
    const targetId = `r${row + 1}-c${col + 1}`;
    const target = document.getElementById(targetId);
    const link = [...links].find(link => link.hash === `#${targetId}`);
    
    if (target && link) {
      linkClicked = true;
      target.scrollIntoView({ behavior: 'smooth' });
      links.forEach(l => l.classList.remove(activeClass));
      link.classList.add(activeClass);
      
      requestAnimationFrame(() => {
        setTimeout(() => {
          syncScroll(target);
          linkClicked = false;
        }, 1000);
      });
    }
  };

  const handleKeydown = (e) => {
    switch (e.key) {
      case 'ArrowLeft':
        currentCol = Math.max(0, currentCol - 1);
        break;
      case 'ArrowRight':
        currentCol = Math.min(4, currentCol + 1);
        break;
      case 'ArrowUp':
        currentRow = Math.max(0, currentRow - 1);
        break;
      case 'ArrowDown':
        currentRow = Math.min(4, currentRow + 1);
        break;
      default:
        return;
    }
    e.preventDefault();
    navigateToCell(currentRow, currentCol);
  };

  const IO = new IntersectionObserver(entries => 
    entries.forEach(({ isIntersecting, intersectionRatio, target }) => {
      if (isIntersecting && intersectionRatio >= 0.5) {
        links.forEach(link => link.classList.remove(activeClass));
        const link = [...links].find(link => link.hash === `#${target.id}`);
        link?.classList.add(activeClass);
        
        // Update current position when scrolling
        const [_, row, col] = target.id.match(/r(\d+)-c(\d+)/);
        currentRow = parseInt(row) - 1;
        currentCol = parseInt(col) - 1;
        
        if (!linkClicked) syncScroll(target);
      }
    }), { 
      threshold: [0, 0.5, 1.0] 
    }
  );

  links.forEach(link => {
    const target = document.getElementById(link.hash.slice(1));
    if (!target) return;
    
    IO.observe(target);

    link.addEventListener('click', e => {
      e.preventDefault();
      const [_, row, col] = target.id.match(/r(\d+)-c(\d+)/);
      currentRow = parseInt(row) - 1;
      currentCol = parseInt(col) - 1;
      navigateToCell(currentRow, currentCol);
    });
  });

  // Add keyboard event listener
  document.addEventListener('keydown', handleKeydown);
}

/* Initialize navigation with links and lists from DOM */
handleNavigation(
  document.querySelectorAll('.ui-scroll-grid-nav a'),
  document.querySelectorAll('.ui-scroll-grid ol')
);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.