Files
intotheeast-com/docs/working/learnings/2026-06-22-mobile-ux-learnings.md
T

3.6 KiB

Mobile UX Session Learnings — 2026-06-22

Discoveries from the mobile polish session (stat scaling, map fullscreen, panel toggles, shared partials).

MapLibre GL JS v4 — Attribution starts expanded despite compact: true

Problem: new maplibregl.AttributionControl({ compact: true }) renders a <details> element. In MapLibre v4, this element has open set after map.on('load') fires, so the attribution panel starts expanded even though compact: true was passed.

Fix: In the load handler, explicitly remove the open attribute:

map.on('load', function () {
    var attrib = map.getContainer().querySelector('.maplibregl-ctrl-attrib');
    if (attrib) attrib.removeAttribute('open');
});

Also: To avoid the default attribution control conflicting with a custom button in bottom-right, disable it in the constructor and add it manually to bottom-left:

var map = new maplibregl.Map({ ..., attributionControl: false });
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');

CSS Panel Animation — max-height beats grid-template-rows: 0fr

Problem: grid-template-rows: 0fr → 1fr transition fails when the direct grid child has overflow: hidden. The child creates a Block Formatting Context (BFC) that prevents 0fr from collapsing to zero height.

Fix: Use max-height transition on the outer container:

.panel {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.4s ease;
}
.panel.is-open {
    max-height: 600px;
}

Fluid Font Sizing with clamp()

.stat-value {
    font-size: clamp(2rem, 6vw, var(--text-3xl));
}
  • clamp(min, preferred, max): scales linearly between min and max
  • 6vw at 333px viewport = 20px = 1.25rem, but floor is 2rem (32px)
  • Keep labels at --text-xs (0.75rem) intentionally — the contrast makes values pop

CSS Grid — Spanning the Lone Last Item in a 2-Column Grid

@media (max-width: 600px) {
    .my-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .my-grid .item:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
  • minmax(0, 1fr) — strictly equal columns (bare 1fr has a hidden auto minimum)
  • :last-child:nth-child(odd) — matches an item that is both last and in an odd position

PhotoSwipe v5 — Correct Element for CSS Animations

Problem: pswp.currSlide.el is undefined in PhotoSwipe v5.

Fix: Use pswp.currSlide.container — the DOM wrapper for the current slide:

var el = pswp.currSlide && pswp.currSlide.container;
if (!el) return;
el.classList.add('pswp-key-from-right');

Mobile Fullscreen Map Pattern

.map-col.is-fullscreen {
    position: fixed !important;
    inset: 0;
    z-index: 9999;
    height: 100dvh !important;
}
fsBtn.addEventListener('click', function() {
    var isFs = mapCol.classList.toggle('is-fullscreen');
    document.body.style.overflow = isFs ? 'hidden' : '';
    setTimeout(function() { map.resize(); }, 50);
});

Marker click while fullscreen: Exit fullscreen first, then scroll after the transition:

if (isFullscreen) {
    fsBtn.click();
    setTimeout(scrollAndHighlight, 450);
} else {
    scrollAndHighlight();
}

Shared Twig Partial Pattern

{% include 'partials/feed-map.html.twig' with {
    'map_entries': map_entries,
    'map_id': 'feed-map',
    'map_var': 'feedMap',
    'link_href': page.parent().url ~ '/map',
    'card_prefix': 'entry-',
    'trip_page': trip_page,
    'show_journey': true
} only %}

Grav's global Twig functions (url(), theme_var()) remain available with only. Only parent template variables are excluded.