From 82bf799206e9addd60cce5f948f3e859a852c293 Mon Sep 17 00:00:00 2001 From: zlshames Date: Fri, 18 Sep 2026 16:48:17 -0400 Subject: [PATCH] Add an ambient strands field behind every hero, and a shimmer on the yearly plan Two pieces of motion, both deliberately restrained. Strands: a slow field of luminous ribbons behind the hero, inspired by React Bits' component of that name. Written from scratch on a 2D canvas rather than adding ogl and a WebGL shader -- that library is ~25kB gzipped, against a page whose entire JS budget is about 2kB, for something purely decorative. Brand blues only, low alpha, and slow enough to read as ambient. It renders on all 13 pages: in PageHeader, which ten of them use, plus the home hero and the two pages that build their own (404 and privateAPI). The component takes multiple instances, selecting by attribute rather than id. It only animates when it is actually on screen and the tab is visible, via an IntersectionObserver and a visibilitychange handler, and it draws one still frame up front so it is never blank. prefers-reduced-motion leaves that still frame and starts no loop, and a change of preference is honoured without a reload. Shimmer: a highlight that travels around the edge of the yearly plan card. A conic gradient rotates behind the card and is masked down to the border ring with mask-composite, so one highlight runs the whole perimeter, corners included. React Bits' "Star Border" slides two radial gradients along the top and bottom edges instead, which reads as two streaks passing rather than a shimmer going around -- the wrong feel for a card, so this is built differently on purpose. The angle is a registered @property, since a bare custom property cannot be interpolated; where that is unsupported the ring simply sits still. Both are aria-hidden and cannot take pointer events. The shimmer masks itself rather than clipping its parent, so the "Best value" badge overhanging the card is unaffected. Co-Authored-By: Claude Opus 5 --- src/components/HeroStrands.astro | 129 +++++++++++++++++++++++ src/components/PageHeader.astro | 8 +- src/components/ShimmerBorder.astro | 84 +++++++++++++++ src/pages/404.astro | 6 +- src/pages/index.astro | 3 + src/pages/install/privateAPI/index.astro | 6 +- src/pages/pricing/index.astro | 4 +- 7 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 src/components/HeroStrands.astro create mode 100644 src/components/ShimmerBorder.astro diff --git a/src/components/HeroStrands.astro b/src/components/HeroStrands.astro new file mode 100644 index 0000000..5fc7b99 --- /dev/null +++ b/src/components/HeroStrands.astro @@ -0,0 +1,129 @@ +--- +/* + * A slow, luminous ribbon field behind the hero. + * + * Inspired by React Bits' "Strands", but written from scratch on a 2D canvas + * rather than pulling in ogl and a WebGL shader. That library is ~25kB gzipped; + * this is a couple of hundred bytes, on a page whose entire JS budget is about + * 2kB. The look is the point, not the implementation. + * + * Deliberately restrained: brand blues only, low alpha, and slow enough that it + * reads as ambient rather than as something demanding attention. + */ +--- + + + + diff --git a/src/components/PageHeader.astro b/src/components/PageHeader.astro index 1893f96..4846e2b 100644 --- a/src/components/PageHeader.astro +++ b/src/components/PageHeader.astro @@ -1,4 +1,6 @@ --- +import HeroStrands from './HeroStrands.astro'; + interface Props { title: string; lead?: string; @@ -6,8 +8,10 @@ interface Props { const { title, lead } = Astro.props; --- -
-
+
+ + {/* Above the canvas, which is absolutely positioned across this container. */} +

{title}

{lead &&

{lead}

} diff --git a/src/components/ShimmerBorder.astro b/src/components/ShimmerBorder.astro new file mode 100644 index 0000000..e8a4005 --- /dev/null +++ b/src/components/ShimmerBorder.astro @@ -0,0 +1,84 @@ +--- +/* + * A shimmer that travels around the edge of its parent. + * + * A conic gradient is rotated behind the card and masked down to just the + * border ring, so a single highlight runs continuously around the whole + * perimeter -- corners included. + * + * This is not React Bits' "Star Border", which slides two radial gradients + * along the top and bottom edges. That reads as two streaks passing by rather + * than one shimmer going around, which is the wrong feel for a card. + * + * Drop it inside a `position: relative` element. Decorative, so aria-hidden and + * no pointer events. It masks itself rather than clipping the parent, so + * anything overhanging the card -- a badge, say -- is unaffected. + */ +interface Props { + /** One full lap. */ + speed?: string; + /** Ring thickness. */ + thickness?: string; +} +const { speed = '5s', thickness = '1.5px' } = Astro.props; +--- + + + + diff --git a/src/pages/404.astro b/src/pages/404.astro index bfb72a5..1ece16e 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -1,5 +1,6 @@ --- import BaseLayout from '@layouts/BaseLayout.astro'; +import HeroStrands from '@components/HeroStrands.astro'; import { LINKS } from '@data/site'; // NOTE: this is the org site's 404, so GitHub Pages serves it for any @@ -13,7 +14,9 @@ import { LINKS } from '@data/site'; path="/404" noindex > -
+
+ +

404

We couldn't find that page. @@ -51,4 +54,5 @@ import { LINKS } from '@data/site'; >.

+
diff --git a/src/pages/index.astro b/src/pages/index.astro index 824a55c..e79fa41 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,6 +2,7 @@ import { Image } from 'astro:assets'; import BaseLayout from '@layouts/BaseLayout.astro'; import StoreButton from '@components/StoreButton.astro'; +import HeroStrands from '@components/HeroStrands.astro'; import { SITE, LINKS } from '@data/site'; import { STORES } from '@data/stores'; import devices from '@assets/devices.png'; @@ -14,6 +15,8 @@ import devices from '@assets/devices.png'; path="/" >
+ + {/* Ambient brand wash -- the modern descendant of the old #bg-circle. */}