Skip to content

Instantly share code, notes, and snippets.

@watershed
Last active June 20, 2026 17:18
Show Gist options
  • Select an option

  • Save watershed/4049ffca75339cad170164af737b9a72 to your computer and use it in GitHub Desktop.

Select an option

Save watershed/4049ffca75339cad170164af737b9a72 to your computer and use it in GitHub Desktop.
SVG webmap for WMUCC
<style>
/* map context */
.hrmap {
margin-block: 1.5rem;
overflow-x: hidden;
--hmarMobile: 6vw;
--hmarWide : 11vw;
}
.hrmap,
.hrmap * {
box-sizing: border-box;
}
.hrmap [hidden] {
display: none !important;
}
.hrmap > aside {
font-size: 0.875rem;
padding: 0.33em 0.75em;
background: hsl(201deg 30% 90%);
opacity: 0.6;
text-align: right;
}
.hrmap > aside a {
color: currentColor;
}
.hrmap > aside:hover {
opacity: 1;
}
/* map controls */
.hrmap__zoom,
.hrmap__scales {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: baseline;
line-height: 1;
}
.hrmap__zoom {
background: #00263b;
color: rgb(255 255 255 / 0.75);
font-weight: 500;
padding: 0.5em;
justify-content: space-between;
}
.hrmap__zoom.stuck {
position: fixed;
top: 0;
width: 100%;
z-index: 2;
}
.hrmap__zoom button,
.hrmap__zoom span {
display: block;
padding: 0.33em 0.4em 0.25em;
}
/* zoom buttons */
.hrmap button {
background: transparent;
font: inherit;
border: 0;
color: white;
border-radius: 3px;
transition: 0.3s;
}
.hrmap button:not([aria-selected="true"]):focus,
.hrmap button:not([aria-selected="true"]):hover {
background: #e13f2f;
}
.hrmap [aria-selected="true"] {
background: white;
color: #00263b;
cursor: default;
}
/* map wrapper */
.pos-rel {
position: relative;
}
.hrmap__wrap.zoomed {
overflow-x: auto;
}
/* map */
.hrmap__img,
.hrmap__wbs img {
display: block;
}
.hrmap__img {
width: 100%;
max-width: unset !important;
}
/* wbs (walk,bike,scoot) */
.hrmap__wbs {
position: absolute;
border: 8px solid white;
top: 8rem;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 0 3rem rgb(0 0 0 / 0.65);
z-index: 1;
}
[data-wbs="outside"][data-active="true"] {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(4px);
}
.hrmap__wbs img {
width: clamp(300px, 80vw, 430px);
height: auto;
}
.hrmap__wbs [data-hide] {
position: absolute;
background: #00263b;
font-size: 24px;
display: block;
line-height: 1;
width: 40px;
height: 40px;
border-radius: 50%;
top: 50%;
right: -24px;
z-index; 1;
outline: 1.5px solid white;
}
.hrmap__zoom [data-wbs]::before {content: 'Show ';}
.hrmap__zoom [data-wbs][aria-selected="true"]::before {content: 'Hide ';}
/* inclusively hide */
.sr-only {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
/* viewport width sensitive layout */
@media (max-width: 768px) {
.hrmap {
margin-inline: calc(var(--hmarMobile) * -1);
padding-inline: calc((var(--hmarMobile) * -1) - 0.4em);
}
}
@media (min-width: 769px) and (max-width: 1399px) {
.hrmap {margin-inline: calc(var(--hmarWide) * -1);}
}
</style>
<!-- map controls and map -->
<div class="hrmap">
<div class="hrmap__controls" hidden>
<div class="hrmap__zoom">
<div class="hrmap__scales">
<span>Scale</span>
<button data-scale="100" aria-selected="true">1x</button>
<button data-scale="150">1.5x</button>
<button data-scale="200">2x</button>
<button data-scale="250">2.5x</button>
<button data-scale="300">3x</button>
<button data-scale="400">4x</button>
<span data-mapcta hidden>Swipe/scroll map area</span>
</div>
<button data-wbs>walk/bike/scoot detail</button>
</div>
</div>
<div class="pos-rel">
<div class="hrmap__wbs" hidden>
<img src="https://watershedcreative.com/wmucc-venue/wbs.jpg" alt="Satellite view of the route between sites for walking, biking or scooting.">
<button data-wbs="inside" data-hide="true"><span class="sr-only">Hide walk/bike/scoot detail</span><span aria-hidden="true">&times;</span></button>
</div>
<div class="hrmap__wrap">
<img class="hrmap__img"
src="https://watershedcreative.com/wmucc-venue/wmucc-web-map.svg?v=20260618"
alt="Maps of the tournament sites at Highfields and Riverside with an inset of the routes between them.">
<div tabindex="0" data-wbs="outside" data-hide="true" data-active="false" aria-hidden="true"></div>
</div>
</div>
<aside>Mapping by <a href="https://mastodon.social/@urlyman/116629094659805747">Jonathan Schofield</a></aside>
</div>
<script>
/* select elements to work with */
const hrmap = document.querySelector('.hrmap');
const contr = document.querySelector('.hrmap__controls');
const zoom = document.querySelector('.hrmap__zoom');
const scales = document.querySelectorAll('.hrmap__zoom [data-scale]');
const mapcta = document.querySelector('.hrmap__zoom [data-mapcta]');
const wrap = document.querySelector('.hrmap__wrap');
const mapimg = document.querySelector('.hrmap__img');
const wbs = document.querySelector('.hrmap__wbs');
const togglers = document.querySelectorAll('[data-wbs]');
const outside = document.querySelector('[data-wbs="outside"]')
// modify map scale
const applyScale = (btn) => {
const scale = btn.dataset.scale * 1;
const is100 = scale === 100;
const method = is100 ? 'remove' : 'add';
mapimg.style.width = `${btn.dataset.scale}%`;
wrap.classList[method]('zoomed');
if ( !is100 ) {
}
mapcta.hidden = is100;
scales.forEach((elem) => {
elem.setAttribute('aria-selected', elem === btn);
});
}
// toggle wbs overlay
const toggleWbs = (btn) => {
const hider = btn.dataset.hide;
const state = hider ? true : btn.getAttribute('aria-selected') === 'true';
wbs.hidden = state;
outside.dataset.active = !state;
if ( hider ) {
togglers[0].setAttribute('aria-selected', false);
}
else {
btn.setAttribute('aria-selected', !state);
wrap.style.minHeight = `${wbs.offsetHeight + 160}px`;
if ( !state ) {
console.log('hi');
wrap.scrollTo(0,0);
wbs.scrollIntoView({block: 'center'});
}
}
}
// create thresholds for IntersectionObserver
const threshArray = (n) => {
const arr = [];
for (let i = 1.0; i <= n; i++) {
const ratio = i / n;
arr.push(ratio);
}
arr.push(0);
return arr;
}
/* make zoom controls fixed on scroll
to work around SquareSpace complexity making `position:sticky` impractical to use :(
*/
let prevRatio = 0.0;
const controlStick = new IntersectionObserver((entries) => {
const isMap = entries[0].target.localName === 'img';
entries.forEach((entry) => {
const rect = entry.boundingClientRect;
let method = 'remove';
if (entry.intersectionRatio > prevRatio) {
method = isMap ? 'add' : 'remove';
}
else {
method = isMap ? 'remove' : rect.y < 44 ? 'add' : 'remove';
}
prevRatio = entry.intersectionRatio;
zoom.classList[method]('stuck');
if ( method === 'add' ) {
updateZoomMaxWidth();
}
});
});
// adjust max-width of zoom controls when fixed
const updateZoomMaxWidth = () => {
const wrapX = wrap.offsetWidth;
zoom.style.maxWidth = `${wrapX}px`;
}
// instantiate observers
const arr = threshArray(5);
controlStick.observe(contr, {scrollMargin: '44px 0px 44px 0px', threshold: arr});
controlStick.observe(mapimg, {threshold: arr});
// adjust max-width of zoom controls on resize
const obs = new ResizeObserver((entries) => {
updateZoomMaxWidth();
});
obs.observe(hrmap);
// show the map controls
contr.hidden = false;
// activate the map controls
scales.forEach((btn) => {
btn.addEventListener('click', () => {
applyScale(btn);
});
});
// enable wbs
togglers.forEach((btn) => {
btn.addEventListener('click', () => {
toggleWbs(btn);
});
});
// hide wbs on 'Escape'
document.addEventListener('keyup', (e) => {
if ( e.code === 'Escape' && outside.dataset.active === 'true' ) {
toggleWbs(outside);
}
});
</script>
@watershed

watershed commented Jun 18, 2026

Copy link
Copy Markdown
Author

There are 3 discrete areas of code:

  1. CSS to invoke visual styling: lines 1 to 181
  2. HTML rendered in the page: lines 185 to 215
  3. JavaScript to invoke interactive behaviour on the rendered HTML: lines 218 to 365.

Included in the scope

  • An SVG encoded map of both sites and the route between them.
  • An overlay for the route between sites, hidden by default.
  • Controls for scaling the map so it can be swiped and scrolled at a higher zoom level than the viewport provides by default.
  • A control for toggling the between-sites overlay.
  • A credit for me!

Demo page

watershedcreative.com/wmucc-venue.

Next steps

If the code is published in the above order it should be fine:

  • The HTML needs the CSS that precedes it.
  • The JavaScript needs the HTML and the CSS that precedes it.

There are potential changes needed to the CSS and HTML needed, but maybe not… (fingers crossed)

HTML

On line 204 and 209 the two images invoked are referenced from my website. If SquareSpace is OK with calling files from a third party domain, then no changes to those lines are needed.

But if there are issues, you will need to upload both images to your SquareSpace account and change the respective src values to where SquareSpace stores the files.

For example, it looks like the base URL for the folder in which your site’s file assets are stored is:

https://images.squarespace-cdn.com/content/v1/69022b9231f16044542b602a/67c1b810-3d8d-4cae-bd89-c827ac186985/

So if there are issues, you might need to change line 209 to:

src="https://images.squarespace-cdn.com/content/v1/69022b9231f16044542b602a/67c1b810-3d8d-4cae-bd89-c827ac186985/wmucc-web-map.svg?v=20260618"

…or something like it.

CSS

Lines 6 and 7 set two variables which attempt to make allowance for the way in which the SquareSpace template sets the left and right margins around page content – i.e. the horizontal margins, shortened in the variable names to hmar.

  • One for mobile viewports: --hmarMobile
  • One for wider viewports: --hmarWide

The values set for each are in viewport width units (vw). For example, 6vw is 6% of the active viewport (browser window) width.

I think the threshold at which the template makes the switch between these two paradigms is 768 CSS pixels, so:

  • At 768 and narrower, --hmarMobile will apply.
  • At 769 and wider, --hmarWide will apply.

Adjust the values given to each of these variables if needed. They are currently 6vw and 11vw respectively, as that seems to be what nets out from SquareSpace’s viper’s nest of code.

Images used

SVG: maps of the tournament sites at Highfields and Riverside with an inset of the routes between them:

JPEG: satellite view of the route between sites for walking, biking or scooting:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment