Skip to content

video-gallery, map and link-effects - #21

Merged
ecropolis merged 3 commits into
mainfrom
claude/video-gallery-map-link-effects
Sep 26, 2026
Merged

ecropolis merged 3 commits into
mainfrom
claude/video-gallery-map-link-effects

Conversation

@ecropolis

Copy link
Copy Markdown
Owner

Round 3, Chip C: three elements, one commit each. npm run check and npm run build are green. The website's npm run check:types against this branch (ELEMENTS_SOURCE, a scratch clone of SuperheroTech main) gives 0 errors and 0 warnings; its 12 hints are the ones main already has. The website's npm run build and check:elements also pass with this branch.

All three are checked by one new script, scripts/check-media-and-links.mjs, added last to the check chain. It greps every built page for anything fetched from Google, YouTube or Vimeo at load: an iframe, script, link, img, source, video poster, embed or object. <noscript> is not exempt. It also runs each check against broken copies of its input. There are 38 of these mutations, and one the check misses fails the run.

video-gallery

What it does. A grid of YouTube, Vimeo or self-hosted videos, each behind the site's own poster and play button, with captions under the tiles. Optional category chips filter the grid. mode="lightbox" plays in one <dialog> per gallery. mode="inline" turns the tile into the player, and pressing another tile puts the first one back, so one plays at a time. A tile without a poster is a neutral panel, never YouTube's thumbnail. The URL parser is a verbatim copy of video-player's block, not an import.

Accessibility.

  • Each tile has a <button type="button"> named "Play: <title>", with aria-haspopup="dialog" in lightbox mode.
  • The chips are a named group of <button aria-pressed>, "All" first and pressed. A polite live region says how many are shown ("Open films: 3 videos.").
  • The lightbox is one native modal dialog named by the video's title. Escape and a backdrop click close it, the close button is labelled, and the scroll lock keeps the scrollbar width.
  • On open, focus goes to the close button, because a cross-origin player keeps Escape. Closing empties the dialog so the sound stops, and focus returns to the tile.
  • Inline, focus moves into the player.
  • Self-hosted files keep native controls and captions; the build warns when a file has no captions track.
  • Under reduced motion there is no fade on filtering and no hover growth.

Without JavaScript. Every tile is a link to the video's own page, named "Watch “<title>” on YouTube", or to the file itself. Every video is listed and the chips are hidden. The one-line mount script straight after the gallery swaps links for buttons before the first paint.

What the check pins.

  • The parser copy is byte-identical to video-player's, plus golden cases.
  • Per tile: the link and its name, the hidden button and its name, and a youtube-nocookie or Vimeo dnt=1 embed.
  • File tiles have a captions track that exists.
  • No <iframe> or <video> at load.
  • The chip group's role, name, hidden and aria-pressed order, and the live region.
  • Exactly one closed, named dialog in lightbox mode and none inline, with a named close button.
  • The mount script sits straight after each gallery, and the runtime is on the page once.
  • Motion appears only under no-preference.

Search. video gallery: 590/mo, difficulty 20 (SE Ranking US, 2026-09-26). alsoRanks:

Query Volume/mo Difficulty
youtube gallery 210 8
video gallery website 40 13
video gallery wordpress 50 28

Demo. Big Buck Bunny and Sintel from Blender's YouTube channel (both CC BY 3.0, credited under each tile). Big Buck Bunny's copy on Vimeo, for a Vimeo tile. The self-hosted drift loop with its captions. These are the films video-player's demo uses and names.

map

Why not the Google Maps JavaScript API. It needs an API key and a billing account, loads Google's script on every visit whether or not anyone looks at the map, and adds Google's map origins to every client's CSP and consent policy. So this element is location cards with keyless links and a map that loads only on request. A styled or branded interactive map is the notFor case, quoted as its own piece of work.

What it does.

  • One card per location: the name as a heading, the address in an <address>, an optional tel: link and an optional hours line.
  • "Open in Google Maps" (maps/search/?api=1&query=) and "Directions" (maps/dir/?api=1&destination=).
  • With embed, a facade: the location's photo, or a neutral CSS street panel with Font Awesome Free's location-dot, read at build as the icon element does.
  • Pressed, the facade becomes <iframe src="https://www.google.com/maps?q=…&output=embed" loading="lazy" title="Map of <name>">.
  • layout="split" sets the map beside the details. Several locations make a list of cards.
  • LocalBusiness JSON-LD is out of scope because business-hours owns it; a comment in the file says so.
  • The file is LocationMap.astro, not Map.astro, so an import cannot shadow JavaScript's Map. The id is still map.

Accessibility.

  • The links are named with the place ("Open in Google Maps: Navy Pier", "Directions to Navy Pier"). The names start with the visible words, per WCAG 2.5.3, and the links are at least 44px tall.
  • The facade is a <button> named "Show the map of ". It is described by the visible notice "Loads a map from Google.", which stays after loading.
  • Pressed, focus moves into the titled iframe.
  • Reduced motion removes the pill's hover growth, which is the only motion.

Without JavaScript. The cards and links only. The facade is rendered hidden and the mount script straight after the map shows it, so a button that cannot work never shows.

What the check pins.

  • The URL builders: encoding, zoom and tel: digits.
  • Every card has a heading, an <address>, both links to Google's keyless URLs with no key=, and names that start with the visible words and include the place.
  • Facades are hidden, with a type="button" named "Show the map of " and described by a notice that says Google.
  • The embed URL is exactly www.google.com/maps?q=…(&z=)&output=embed, and the iframe title is "Map of ".
  • Photo facades are alt="". The pin is Font Awesome Free's location-dot, path for path.
  • No iframe at load. The runtime creates the iframe with loading="lazy" and the title, and is on the page once. The mount script sits straight after each map.
  • The fallback contrast pairs pass:
Pair Contrast
Card text 14.75:1
Links 9.02:1
Notice 7.59:1
Button text 7.27:1

Search. google map embed code for website: 170/mo, difficulty 35 (SE Ranking US, 2026-09-26). The round-3 note's "map embed without api key" and "google map embed without api key" have no data. alsoRanks:

Query Volume/mo Difficulty
google maps embed 590 41
google maps directions link 390 34
google maps link 320 20

"store locator" (2,400/65) is a different product and stays in aka.

Demo. Public Chicago landmarks (Millennium Park, Navy Pier, Garfield Park Conservatory), so the map has something real to show. The one phone number is a 555-01xx number, a range reserved for fiction.

link-effects

What it does. Eleven CSS-only hover-and-focus styles for links. Eight are familiar: underline-slide, underline-grow, brackets, highlight, box, strike-to-underline, arrow and circle. Three are ours:

  • peek: the link's data-peek words slide up in its place, "Pricing" to "$49/mo". Both share one grid cell, so the width is kept and nothing shifts.
  • swash: an inline SVG hand-drawn underline that draws itself, stroke-dashoffset 1 to 0.
  • tag: a pill that fills from the left, the text turning with the fill.

<LinkEffects /> emits the stylesheet once per page, with the modal's Astro.locals guard. The same file imported as Link is the <Link effect href> wrapper, which adds the label span for peek and the SVG for swash. The classes are prefixed lk- (class="lk-underline-slide"), because bare box, tag or highlight would collide with host CSS. The effect names stay unprefixed.

Accessibility.

  • Every effect is one :is(:hover, :focus-visible) rule, and the browser's focus ring is never removed.
  • All transitions sit under prefers-reduced-motion: no-preference, so otherwise the end state appears at once.
  • Text colour is never transitioned. highlight pins --lk-ink at rest and when active, so a host's own a:hover colour cannot land on the marker.
  • tag paints its text colour and its fill as two layers of one background, one of them background-clip: text, moved by a single background-position. So mid-sweep each pixel of text is ink on the page or white on the fill, never in between. This was checked on a frame captured halfway through a slowed transition.
  • Brackets and the arrow are silent to screen readers (content: "→" / ""), and the swash SVG is aria-hidden.
  • The peek words are part of the link's name ("Pricing $49/mo").
  • Forced-colours mode gets a plain underline and system colours.

Without JavaScript. Identical; there is none.

What the check pins.

  • An active rule for each of the eleven, and no bare :hover.
  • No transition outside no-preference, and no transition of color or -webkit-text-fill-color.
  • No outline: none, and an empty alternative on decorative content.
  • highlight's pinned colour, and the tag sweep's layer structure. Tag's hover may move only background-position.
  • The fallback contrast pairs pass:
Pair Contrast
Ink on page 14.75:1
Ink on marker 11.68:1
Tag text on fill 7.27:1
Accent text on page 7.27:1
  • On the built pages: the stylesheet appears once, the demo shows all eleven with their class names visible, swash's SVG is aria-hidden with pathLength="1", and peek's markup is complete. No script appears in the demo.

Search. underline animation css: 590/mo, difficulty 20 (SE Ranking US, 2026-09-26). The round-3 note's "link hover effects" is 90/30. Five of the eleven effects are underlines, so the page leads with them. alsoRanks:

Query Volume/mo Difficulty
css link hover effects 110 26
link hover effects 90 30
css text hover effects 90 27

"css hover effects" (480/31) covers buttons and images and stays unclaimed.

Verified in a browser

I ran headless Chromium against this gallery's build, and against the website built from this branch on both the element pages and the /elements/ hub. On the hub the runtimes become external scripts.

  • video-gallery: nothing from YouTube or Vimeo at load; chips, counts and live-region text; the lightbox by keyboard (the focus, Escape, backdrop, focus return and scroll lock described above); the Vimeo dnt=1 embed; a file with a captions track; inline one-at-a-time; one column at 375px; the no-JS links; no fade under reduced motion.
  • map: nothing from Google at load; the named facade and its description; the exact embed URL; focus in the iframe; no overflow at 375px or 1280px; 44px links; the no-JS render.
  • link-effects: each of the eleven changes on hover, looks the same on focus, keeps its width, has no transition under reduced motion, and keeps the focus ring.

Follow-ups (not in this PR)

  • The skill. As round 3 plans, one agent-skills chip after all nine elements land covers these three, with house-stack 0.39.0 → 0.40.0.
  • superherotech.ai CSP. The report-only CSP's frame-src allows only challenges.cloudflare.com. The video-player and video-gallery iframes (youtube-nocookie.com, player.vimeo.com) and the map embed (www.google.com) are report-only violations today. They would be blocked once that header is promoted to enforced, unless those origins are added first.

🤖 Generated with Claude Code

ecropolis and others added 3 commits September 26, 2026 11:27
…be until a click

A grid of YouTube, Vimeo or self-hosted videos behind the site's own posters,
with optional category chips (<button aria-pressed>, "All" first, a polite
count). mode="lightbox" plays in one <dialog> per gallery (Escape, backdrop,
focus return, scroll lock, autoplay on open); mode="inline" turns the tile
into the player and puts the previous one back. Without JavaScript each tile
links to the video's own page and every video is listed.

The URL parser is a verbatim copy of video-player's block; the new
scripts/check-media-and-links.mjs fails if the copies drift, greps every built
page for anything fetched from Google, YouTube or Vimeo at load, pins the
no-JS render and the ARIA contract, and runs each check against broken copies
of its input (15 mutations) so a check that cannot fail is caught.

Search: "video gallery" 590/mo, difficulty 20 (SE Ranking US, 2026-09-26).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One card per location: the address in an <address>, a tel: link, an
optional hours line, and "Open in Google Maps" / "Directions" links to
Google's keyless Maps URLs, named with the place. `embed` adds a facade (the
location's photo, or a neutral panel with Font Awesome Free's pin) that is a
<button> named "Show the map of <name>" and described by "Loads a map from
Google."; pressed, it becomes a lazy google.com/maps?…&output=embed iframe
titled "Map of <name>". Nothing is fetched from Google before that. Without
JavaScript: the cards and links, no facade.

Not the Maps JavaScript API: it needs a key and billing, loads Google's script
on every visit, and adds Google's origins to every client CSP and consent
policy. That is the notFor case. LocalBusiness JSON-LD stays with
business-hours. The file is LocationMap.astro so an import cannot shadow
JavaScript's Map.

check-media-and-links pins the URL builders, the no-JS render, the ARIA, the
FA Free pin and the fallback contrast, with 10 more mutations.

Search: "google map embed code for website" 170/mo, difficulty 35 (SE
Ranking US, 2026-09-26); "map embed without api key" has no data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
underline-slide, underline-grow, brackets, highlight, box,
strike-to-underline, arrow, circle, and three of ours: peek (the link's
data-peek words slide in, width kept so nothing shifts), swash (an inline SVG
underline drawn by stroke-dashoffset) and tag (a pill that fills, the text
turning with it). Classes on any <a> (lk-<effect>), or this same file as a
<Link effect> wrapper; <LinkEffects /> emits the stylesheet once per page
through an Astro.locals guard.

Every effect is one `:is(:hover, :focus-visible)` rule, keeps the focus ring,
and has its transitions only under prefers-reduced-motion: no-preference.
Text colour is never transitioned: highlight pins --lk-ink, and tag paints
text and fill as two layers of one background moved by one
background-position, so each pixel of text is a passing pair mid-sweep.

check-media-and-links computes the fallback pairs (14.75, 11.68, 7.27 and
7.27 to 1), pins the rules above and the demo page, with 13 more mutations.

Search: "underline animation css" 590/mo, difficulty 20 (SE Ranking US,
2026-09-26); "link hover effects" is 90/30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ecropolis
ecropolis merged commit 8d87987 into main Sep 26, 2026
2 checks passed
@ecropolis
ecropolis deleted the claude/video-gallery-map-link-effects branch September 26, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant