Hand-built components

A few components, built by hand.

No framework, no component library — hand-written CSS and vanilla JS, same rule as the rest of this site. Each one below reuses this site's own tokens (the conic-gradient dial is the same technique Live Sessions/Analytics use for their timers). Hover, drag, and click through them.

Want to see the backend? Everything on this site is managed through a full custom-built admin system — analytics, a self-learning bot classifier, content editors, and more. See how it's put together, or try it yourself with a safe demo login. See the admin backend →

Magnetic hover

mousemove tracking, eased with a damping factor

The button nudges toward the cursor on every mousemove, damped to about a third of the raw offset so it feels like it's being pulled rather than following exactly — and snaps back the moment the cursor leaves. Skipped entirely on touch devices via @media (hover: hover), since there's no cursor to track.

Progress ring

conic-gradient + animatable @property
0% deployed

The same donut-ring pattern powering the countdown timers on Live Sessions and the auto-refresh dial on Analytics — a CSS custom property registered with @property so the browser can animate the gradient angle directly, no JS animation loop.

Segmented control

sliding thumb, width read from the DOM

The thumb's width and position are measured from the actual button it's tracking, not guessed as a percentage — so it stays pixel-accurate no matter how the label text reflows.

Toast stack

stacking notifications, self-dismissing

Each click stacks a new toast above the last and clears itself after a few seconds — click a few times fast to see them queue.

Rich tooltip

delayed reveal, back-out easing
Hover or focus me A short reveal delay stops it flashing on a quick mouse pass-over, and it's gated behind @media (hover: hover) so it never becomes a dead tap target on a phone.

Same tooltip language documented for the site's nav — fade + scale with a slight overshoot on entrance, never shown as a touch target.

Command palette

native <dialog>, keyboard-first

Built on the native <dialog> element — free focus trapping, Escape-to-close, and top-layer stacking without touching a z-index scale. Try the keyboard shortcut, arrow keys to move, Enter to jump.

Drag to reorder

pointer events, no library
  • 1 Harden session cookies
  • 2 Add IP-keyed throttling
  • 3 Ship the analytics dashboard
  • 4 Write the changelog

Raw pointerdown/pointermove tracking with touch-action: none — works identically with mouse, trackpad, and touch, no drag-and-drop library. Fully keyboard-operable too: Tab to an item, Space to pick up, arrow keys to move, Space or Escape to drop.

Copy to clipboard

Clipboard API + aria-live confirmation
curl -sI https://henrikbr.dk

Uses navigator.clipboard.writeText() directly, no fallback textarea-select hack. The button label and icon swap for a moment to confirm the copy, and a screen reader gets the same confirmation through a hidden aria-live region, not just the visual swap.

Swipe to dismiss

pointer events, threshold + snap-back
Swipe or drag
Dismiss me

Drag past a threshold and it flies off and resets a moment later so you can try again — let go early and it snaps back instead. Works identically with a mouse, trackpad, or a finger, and Enter dismisses it without touching the pointer at all.

Live relative time

Intl.RelativeTimeFormat, sped up 600×
Deployed  

Formats a timestamp the same way a browser's built-in locale data would — “2 minutes ago”, “an hour ago” — using Intl.RelativeTimeFormat instead of a hand-written lookup table. Sped up 600× here so you can actually watch it roll from seconds into minutes into hours, instead of waiting around in real time.

Before / after slider

clip-path masking, drag + keyboard
Before
After

Drag the handle, or focus it and use the arrow keys — Home/End jump straight to either end. The reveal is a plain clip-path: inset() on the top layer, no image swapping or canvas involved.

Confetti burst

canvas + hand-rolled particle physics

60 squares launched from the button with random velocity and spin, then gravity, drag, and fade take over every frame until they're gone. No library — a plain <canvas>, an array of particles, and a requestAnimationFrame loop.

Count-up stats

IntersectionObserver-triggered
0
year this shipped
0
years hands-on
0
frameworks used
0
% self-hosted

That's the lab.

Everything above is built the same way as the rest of this site — hand-written, no shortcuts. If you want to see where these techniques actually get used, or read more about the person behind them, here's where to go next.