Skip to content
Marketing block

Video Player Shell

The watch screen with no playback in it: a ratio-locked stage, a control bar drawn down to the buffered layer, and an up-next rail. Drop a video element in and wire your own player.

327 lineslucide-reactAdded 14 Sept 2026
  • video
  • player
  • streaming
  • watch
  • media

What's included

  • components/video-player-shell.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add video-player-shell

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Episode 3

4:18 / 12:04

Profiling, end to end

Where the time actually goes

48,219 views · Episode 3 of 7 · Published 4 March

We take a page that feels slow and find out what is actually slow about it — with a profiler open the whole time, and no guessing. The fix at the end is two lines; finding it is the other eleven minutes.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/video-player-shell.tsx
/**
 * <VideoPlayerShell> — the watch screen: stage, control bar, and what is next.
 *
 * It ships NO PLAYBACK. That is deliberate and it is the first thing to
 * understand about the block. Every streaming product wires its player
 * differently — HLS or DASH, a DRM licence round-trip, a heartbeat to the
 * analytics pipeline, an ad break that has to pause the timeline — and a
 * block that picked one of those would be wrong for everyone else and
 * would drag a 200KB dependency into a catalog whose whole claim is one
 * runtime dependency. What is genuinely reusable is the *furniture*: the
 * locked-ratio stage, the control bar with all of its states drawn, the
 * metadata row, and the up-next rail. Drop a `<video>` into the stage and
 * point your player's state at these props.
 *
 * THE ASPECT RATIO IS LOCKED ON THE CONTAINER, NOT THE MEDIA. `aspect-video`
 * sits on the stage, so the box exists at its final size before any media
 * arrives. A player that sizes itself to the video once metadata loads
 * shifts every word below it — and the thing below it is the title, so the
 * reader loses their place on the one line they were reading.
 *
 * THE SCRUBBER IS A PROGRESSBAR, NOT A SLIDER. A `role="slider"` that does
 * not move is a lie told to a screen reader: it promises arrow keys that
 * do nothing. As a shell this reports position and stops there. When you
 * wire real seeking, promote it to a slider *and* implement the keys —
 * both, or neither.
 *
 * TIMES ARE STRINGS. `12:04`, not seconds run through a formatter at
 * render. Same reason as everywhere else in this catalog: the server's
 * locale is not the browser's, and the mismatch throws the subtree away.
 *
 * THE BUFFERED BAR IS THE DETAIL EVERY MOCKUP LEAVES OUT, and it is the
 * one that makes a drawn player read as a real one — three layers, not
 * two: track, buffered, played.
 */

import * as React from 'react'
import {
  Play,
  SkipBack,
  SkipForward,
  Volume2,
  Captions,
  Settings,
  Maximize,
  Eye,
} from 'lucide-react'

function instanceId(...parts: (string | undefined)[]): string {
  const text = parts.filter(Boolean).join('|')
  let hash = 0
  for (let i = 0; i < text.length; i++) hash = (Math.imul(hash, 31) + text.charCodeAt(i)) | 0
  return (hash >>> 0).toString(36).slice(0, 6)
}

export interface UpNextItem {
  title: string
  /** "12:04". A string — see the header. */
  duration: string
  meta?: string
  /** Marks the row the viewer is on. */
  current?: boolean
  href?: string
}

export interface VideoPlayerShellProps {
  title?: string
  /** The series, channel or course this belongs to. */
  seriesLabel?: string
  /** Under the title — view count, date, chapter. Written by you. */
  metaLine?: string
  description?: string
  /** Elapsed and total, both pre-formatted. */
  elapsed?: string
  duration?: string
  /** Played and buffered as percentages, 0–100. */
  playedPercent?: number
  bufferedPercent?: number
  /** Shown on the stage before playback starts. */
  badge?: string
  upNextHeading?: string
  upNext?: UpNextItem[]
  className?: string
}

const DEFAULT_UP_NEXT: UpNextItem[] = [
  {
    title: 'Where the time actually goes',
    duration: '12:04',
    meta: 'Episode 3 · watching now',
    current: true,
  },
  { title: 'Reading a flame graph without guessing', duration: '18:41', meta: 'Episode 4', href: '#' },
  { title: 'The three allocations that mattered', duration: '09:57', meta: 'Episode 5', href: '#' },
  { title: 'Making the fix stick in CI', duration: '15:22', meta: 'Episode 6', href: '#' },
  { title: 'What we would do differently', duration: '07:15', meta: 'Episode 7', href: '#' },
]

/** One control-bar button. Icon-only, so the label is required. */
function ControlButton({
  label,
  children,
  large = false,
}: {
  label: string
  children: React.ReactNode
  large?: boolean
}) {
  return (
    <button
      type="button"
      aria-label={label}
      className={`inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 ${
        large ? 'h-9 w-9' : 'h-8 w-8'
      }`}
    >
      {children}
    </button>
  )
}

export function VideoPlayerShell({
  title = 'Where the time actually goes',
  seriesLabel = 'Profiling, end to end',
  metaLine = '48,219 views · Episode 3 of 7 · Published 4 March',
  description = 'We take a page that feels slow and find out what is actually slow about it — with a profiler open the whole time, and no guessing. The fix at the end is two lines; finding it is the other eleven minutes.',
  elapsed = '4:18',
  duration = '12:04',
  playedPercent = 36,
  bufferedPercent = 61,
  badge = 'Episode 3',
  upNextHeading = 'Up next in this series',
  upNext = DEFAULT_UP_NEXT,
  className = '',
}: VideoPlayerShellProps) {
  const headingId = `video-player-heading-${instanceId(title, seriesLabel)}`

  /* Clamped, because a percentage out of range draws a bar wider than its
     track and pushes the control row sideways at some breakpoints. */
  const played = Math.min(100, Math.max(0, playedPercent))
  const buffered = Math.min(100, Math.max(played, bufferedPercent))

  return (
    <section
      aria-labelledby={headingId}
      className={`mx-auto w-full max-w-7xl px-4 py-10 sm:px-6 lg:px-8 ${className}`}
    >
      <div className="flex flex-col gap-8 lg:flex-row">
        <div className="min-w-0 flex-1">
          {/* ── The stage ────────────────────────────────────────────
              Always dark, in both themes. A player is a cinema: the
              surround stays black so nothing competes with the picture,
              and a light-mode player with a white control bar is a thing
              no streaming product ships. */}
          <div className="relative aspect-video w-full overflow-hidden rounded-xl bg-neutral-950 ring-1 ring-inset ring-white/10">
            {/* The poster. A drawn gradient rather than a remote image —
                no request, no licence, no shift while it loads. */}
            <div
              aria-hidden
              className="absolute inset-0 bg-gradient-to-br from-primary/30 via-neutral-900 to-neutral-950"
            />

            {badge ? (
              <span className="absolute start-4 top-4 rounded-full bg-black/60 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider text-white/90 backdrop-blur">
                {badge}
              </span>
            ) : null}

            {/* The centre play affordance. A real button, because it is
                the one control a viewer reaches for before any other. */}
            <div className="absolute inset-0 flex items-center justify-center">
              <button
                type="button"
                aria-label={`Play — ${title}`}
                className="inline-flex h-16 w-16 items-center justify-center rounded-full bg-white/15 text-white ring-1 ring-inset ring-white/30 backdrop-blur transition-colors hover:bg-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
              >
                {/* `ps-1` because a triangle is optically off-centre in a
                    circle when it is geometrically centred. */}
                <Play aria-hidden className="h-7 w-7 ps-1" />
              </button>
            </div>

            {/* ── Control bar ───────────────────────────────────────── */}
            <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-3 pt-10">
              {/* Three layers: track, buffered, played. */}
              <div
                role="progressbar"
                aria-label="Playback position"
                aria-valuemin={0}
                aria-valuemax={100}
                aria-valuenow={played}
                aria-valuetext={`${elapsed} of ${duration}`}
                className="relative h-1 w-full overflow-hidden rounded-full bg-white/25 border border-transparent"
              >
                <span
                  aria-hidden
                  className="absolute inset-y-0 start-0 rounded-full bg-white/40 border border-transparent"
                  style={{ width: `${buffered}%` }}
                />
                <span
                  aria-hidden
                  className="absolute inset-y-0 start-0 rounded-full bg-primary border border-transparent"
                  style={{ width: `${played}%` }}
                />
              </div>

              <div className="mt-2 flex items-center gap-1">
                <ControlButton label="Play" large>
                  <Play aria-hidden className="h-4.5 w-4.5" />
                </ControlButton>
                <ControlButton label="Back ten seconds">
                  <SkipBack aria-hidden className="h-4 w-4" />
                </ControlButton>
                <ControlButton label="Forward ten seconds">
                  <SkipForward aria-hidden className="h-4 w-4" />
                </ControlButton>
                <ControlButton label="Mute">
                  <Volume2 aria-hidden className="h-4 w-4" />
                </ControlButton>

                <p className="ms-2 text-xs tabular-nums text-white/80">
                  {elapsed}
                  <span className="text-white/60"> / {duration}</span>
                </p>

                <span className="flex-1" />

                <ControlButton label="Subtitles">
                  <Captions aria-hidden className="h-4 w-4" />
                </ControlButton>
                <ControlButton label="Quality and speed">
                  <Settings aria-hidden className="h-4 w-4" />
                </ControlButton>
                <ControlButton label="Full screen">
                  <Maximize aria-hidden className="h-4 w-4" />
                </ControlButton>
              </div>
            </div>
          </div>

          {/* ── Below the stage ──────────────────────────────────────── */}
          <div className="mt-5">
            {seriesLabel ? (
              <p className="text-xs font-semibold uppercase tracking-wider text-primary">
                {seriesLabel}
              </p>
            ) : null}

            <h2 id={headingId} className="mt-1.5 text-2xl font-bold tracking-tight sm:text-3xl">
              {title}
            </h2>

            {metaLine ? (
              <p className="mt-2 inline-flex items-center gap-1.5 text-sm text-muted-foreground">
                <Eye aria-hidden className="h-3.5 w-3.5" />
                {metaLine}
              </p>
            ) : null}

            {description ? (
              <p className="mt-4 max-w-prose text-pretty text-sm leading-relaxed text-muted-foreground">
                {description}
              </p>
            ) : null}
          </div>
        </div>

        {/* ── Up next ────────────────────────────────────────────────── */}
        {upNext.length ? (
          <aside className="w-full shrink-0 lg:w-80">
            <h3 data-stress-ignore className="text-sm font-semibold tracking-tight">{upNextHeading}</h3>

            <ol className="mt-3 space-y-1.5">
              {upNext.map((item, i) => {
                const inner = (
                  <>
                    {/* The thumbnail slot, at the same ratio as the stage. */}
                    <span
                      aria-hidden
                      className="relative grid h-12 w-20 shrink-0 place-items-center rounded-md bg-gradient-to-br from-muted to-muted/40 text-[10px] font-semibold tabular-nums text-muted-foreground ring-1 ring-inset ring-border/60"
                    >
                      {item.duration}
                    </span>

                    <span className="min-w-0 flex-1">
                      <span className="block break-words text-sm font-medium text-card-foreground">
                        {item.title}
                      </span>
                      {item.meta ? (
                        <span className="mt-0.5 block break-words text-xs text-muted-foreground">
                          {item.meta}
                        </span>
                      ) : null}
                      {/* The duration is drawn on the decorative thumbnail
                          above, so it is announced here instead. */}
                      <span className="sr-only">Duration {item.duration}</span>
                    </span>
                  </>
                )

                return (
                  <li key={`${item.title}-${i}`}>
                    {item.current ? (
                      <p
                        aria-current="true"
                        className="flex items-center gap-3 rounded-lg border border-primary/40 bg-primary/5 p-2"
                      >
                        {inner}
                      </p>
                    ) : (
                      <a
                        href={item.href ?? '#'}
                        className="flex items-center gap-3 rounded-lg border border-transparent p-2 transition-colors hover:border-border/60 hover:bg-muted/50"
                      >
                        {inner}
                      </a>
                    )}
                  </li>
                )
              })}
            </ol>
          </aside>
        ) : null}
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/video-player-shell.tsx and import it where you need the section:

import { VideoPlayerShell } from '@/components/video-player-shell'

Customize

9 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
titlestring'Where the time actually goes'
seriesLabelThe series, channel or course this belongs to.string'Profiling, end to end'
metaLineUnder the title — view count, date, chapter. Written by you.string'48,219 views · Episode 3 of 7 · Published 4 …
descriptionstring—
elapsedElapsed and total, both pre-formatted.string'4:18'
durationstring'12:04'
playedPercentPlayed and buffered as percentages, 0–100.number36
bufferedPercentnumber61
badgeShown on the stage before playback starts.string'Episode 3'
upNextHeadingstring'Up next in this series'
upNextUpNextItem[]DEFAULT_UP_NEXT
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

video-player-shell.html
<!--
  Video Player Shell — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<section aria-labelledby="video-player-heading-nfqlc9" class="mx-auto w-full max-w-7xl px-4 py-10 sm:px-6 lg:px-8 ">
  <div class="flex flex-col gap-8 lg:flex-row">
    <div class="min-w-0 flex-1">
      <div class="relative aspect-video w-full overflow-hidden rounded-xl bg-neutral-950 ring-1 ring-inset ring-white/10">
        <div aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-primary/30 via-neutral-900 to-neutral-950"></div>
        <span class="absolute start-4 top-4 rounded-full bg-black/60 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider text-white/90 backdrop-blur">Episode 3</span>
        <div class="absolute inset-0 flex items-center justify-center">
          <button type="button" aria-label="Play — Where the time actually goes" class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-white/15 text-white ring-1 ring-inset ring-white/30 backdrop-blur transition-colors hover:bg-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-play h-7 w-7 ps-1" aria-hidden="true">
              <polygon points="6 3 20 12 6 21 6 3"></polygon>
            </svg>
          </button>
        </div>
        <div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-3 pt-10">
          <div role="progressbar" aria-label="Playback position" aria-valuemin="0" aria-valuemax="100" aria-valuenow="36" aria-valuetext="4:18 of 12:04" class="relative h-1 w-full overflow-hidden rounded-full bg-white/25 border border-transparent">
            <span aria-hidden="true" class="absolute inset-y-0 start-0 rounded-full bg-white/40 border border-transparent" style="width:61%"></span>
            <span aria-hidden="true" class="absolute inset-y-0 start-0 rounded-full bg-primary border border-transparent" style="width:36%"></span>
          </div>
          <div class="mt-2 flex items-center gap-1">
            <button type="button" aria-label="Play" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-9 w-9">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-play h-4.5 w-4.5" aria-hidden="true">
                <polygon points="6 3 20 12 6 21 6 3"></polygon>
              </svg>
            </button>
            <button type="button" aria-label="Back ten seconds" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-8 w-8">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-skip-back h-4 w-4" aria-hidden="true">
                <polygon points="19 20 9 12 19 4 19 20"></polygon>
                <line x1="5" x2="5" y1="19" y2="5"></line>
              </svg>
            </button>
            <button type="button" aria-label="Forward ten seconds" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-8 w-8">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-skip-forward h-4 w-4" aria-hidden="true">
                <polygon points="5 4 15 12 5 20 5 4"></polygon>
                <line x1="19" x2="19" y1="5" y2="19"></line>
              </svg>
            </button>
            <button type="button" aria-label="Mute" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-8 w-8">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-volume2 lucide-volume-2 h-4 w-4" aria-hidden="true">
                <path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"></path>
                <path d="M16 9a5 5 0 0 1 0 6"></path>
                <path d="M19.364 18.364a9 9 0 0 0 0-12.728"></path>
              </svg>
            </button>
            <p class="ms-2 text-xs tabular-nums text-white/80">4:18<span class="text-white/60"> / 12:04</span></p>
            <span class="flex-1"></span>
            <button type="button" aria-label="Subtitles" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-8 w-8">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-captions h-4 w-4" aria-hidden="true">
                <rect width="18" height="14" x="3" y="5" rx="2" ry="2"></rect>
                <path d="M7 15h4M15 15h2M7 11h2M13 11h4"></path>
              </svg>
            </button>
            <button type="button" aria-label="Quality and speed" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-8 w-8">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings h-4 w-4" aria-hidden="true">
                <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
                <circle cx="12" cy="12" r="3"></circle>
              </svg>
            </button>
            <button type="button" aria-label="Full screen" class="inline-flex shrink-0 items-center justify-center rounded-md text-white/90 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 h-8 w-8">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize h-4 w-4" aria-hidden="true">
                <path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
                <path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
                <path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
                <path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
              </svg>
            </button>
          </div>
        </div>
      </div>
      <div class="mt-5">
        <p class="text-xs font-semibold uppercase tracking-wider text-primary">Profiling, end to end</p>
        <h2 id="video-player-heading-nfqlc9" class="mt-1.5 text-2xl font-bold tracking-tight sm:text-3xl">Where the time actually goes</h2>
        <p class="mt-2 inline-flex items-center gap-1.5 text-sm text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye h-3.5 w-3.5" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"></path><circle cx="12" cy="12" r="3"></circle></svg>48,219 views · Episode 3 of 7 · Published 4 March</p>
        <p class="mt-4 max-w-prose text-pretty text-sm leading-relaxed text-muted-foreground">We take a page that feels slow and find out what is actually slow about it — with a profiler open the whole time, and no guessing. The fix at the end is two lines; finding it is the other eleven minutes.</p>
      </div>
    </div>
    <aside class="w-full shrink-0 lg:w-80">
      <h3 data-stress-ignore="true" class="text-sm font-semibold tracking-tight">Up next in this series</h3>
      <ol class="mt-3 space-y-1.5">
        <li>
          <p aria-current="true" class="flex items-center gap-3 rounded-lg border border-primary/40 bg-primary/5 p-2">
            <span aria-hidden="true" class="relative grid h-12 w-20 shrink-0 place-items-center rounded-md bg-gradient-to-br from-muted to-muted/40 text-[10px] font-semibold tabular-nums text-muted-foreground ring-1 ring-inset ring-border/60">12:04</span>
            <span class="min-w-0 flex-1">
              <span class="block break-words text-sm font-medium text-card-foreground">Where the time actually goes</span>
              <span class="mt-0.5 block break-words text-xs text-muted-foreground">Episode 3 · watching now</span>
              <span class="sr-only">Duration 12:04</span>
            </span>
          </p>
        </li>
        <li>
          <a href="#" class="flex items-center gap-3 rounded-lg border border-transparent p-2 transition-colors hover:border-border/60 hover:bg-muted/50">
            <span aria-hidden="true" class="relative grid h-12 w-20 shrink-0 place-items-center rounded-md bg-gradient-to-br from-muted to-muted/40 text-[10px] font-semibold tabular-nums text-muted-foreground ring-1 ring-inset ring-border/60">18:41</span>
            <span class="min-w-0 flex-1">
              <span class="block break-words text-sm font-medium text-card-foreground">Reading a flame graph without guessing</span>
              <span class="mt-0.5 block break-words text-xs text-muted-foreground">Episode 4</span>
              <span class="sr-only">Duration 18:41</span>
            </span>
          </a>
        </li>
        <li>
          <a href="#" class="flex items-center gap-3 rounded-lg border border-transparent p-2 transition-colors hover:border-border/60 hover:bg-muted/50">
            <span aria-hidden="true" class="relative grid h-12 w-20 shrink-0 place-items-center rounded-md bg-gradient-to-br from-muted to-muted/40 text-[10px] font-semibold tabular-nums text-muted-foreground ring-1 ring-inset ring-border/60">09:57</span>
            <span class="min-w-0 flex-1">
              <span class="block break-words text-sm font-medium text-card-foreground">The three allocations that mattered</span>
              <span class="mt-0.5 block break-words text-xs text-muted-foreground">Episode 5</span>
              <span class="sr-only">Duration 09:57</span>
            </span>
          </a>
        </li>
        <li>
          <a href="#" class="flex items-center gap-3 rounded-lg border border-transparent p-2 transition-colors hover:border-border/60 hover:bg-muted/50">
            <span aria-hidden="true" class="relative grid h-12 w-20 shrink-0 place-items-center rounded-md bg-gradient-to-br from-muted to-muted/40 text-[10px] font-semibold tabular-nums text-muted-foreground ring-1 ring-inset ring-border/60">15:22</span>
            <span class="min-w-0 flex-1">
              <span class="block break-words text-sm font-medium text-card-foreground">Making the fix stick in CI</span>
              <span class="mt-0.5 block break-words text-xs text-muted-foreground">Episode 6</span>
              <span class="sr-only">Duration 15:22</span>
            </span>
          </a>
        </li>
        <li>
          <a href="#" class="flex items-center gap-3 rounded-lg border border-transparent p-2 transition-colors hover:border-border/60 hover:bg-muted/50">
            <span aria-hidden="true" class="relative grid h-12 w-20 shrink-0 place-items-center rounded-md bg-gradient-to-br from-muted to-muted/40 text-[10px] font-semibold tabular-nums text-muted-foreground ring-1 ring-inset ring-border/60">07:15</span>
            <span class="min-w-0 flex-1">
              <span class="block break-words text-sm font-medium text-card-foreground">What we would do differently</span>
              <span class="mt-0.5 block break-words text-xs text-muted-foreground">Episode 7</span>
              <span class="sr-only">Duration 07:15</span>
            </span>
          </a>
        </li>
      </ol>
    </aside>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

11 more blocks in Content & Blog

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Content & Blog

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Content & Blog blocks

View category
Open the full page for this block

Release Timeline

A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.

Content & Blog145 linesNo deps
Open the full page for this block

Public Roadmap Columns

Shipped / in progress / planned in three columns, with status carried by icon as well as colour.

Content & Blog122 lines1 dep

Blog Index Grid

A featured post at full width above a card grid, with drawn covers built from tokens — a blog index with no image pipeline and no broken-thumbnail state.

Content & Blog188 lines1 dep

Case Study Card Grid

A grid of case-study cards that lead with the metric rather than the company name, each one linking out to the full write-up — the block a customer-stories section or a home page needed and only had a full case-study page for.

Content & Blog136 lines1 dep
Open the full page for this block

Article Header & Byline

Kicker, headline, standfirst, byline and share row, with the opening prose and a pull quote included — the type scale of a post, decided once instead of per-article.

Content & Blog171 lines1 dep
Open the full page for this block

Team Grid

An about-page team section on initials avatars rather than photos, so shipping it never waits on headshots and there is no broken-image state to design.

Content & Blog166 lines1 dep

Careers Openings Board

Openings grouped by department with location, type and salary on every row — candidates self-select by craft first, and a flat list makes them scan for it.

Content & Blog186 lines1 dep
Open the full page for this block

Three-Column Docs Frame

Sidebar, article and on-this-page rail — each column answers a different question, and they collapse on mobile in the order that keeps the article alive.

Content & Blog290 lines1 dep
Open the full page for this block

API Endpoint Reference

Method badge, path, parameter table and a request/response pair kept side by side — an example request whose response is a scroll away gets pasted blind.

Content & Blog237 lines1 dep
Open the full page for this block

Package Manager Code Tabs

One install command behind npm/pnpm/yarn/bun tabs with a working copy button — every reader uses exactly one package manager, so showing all four wastes three lines.

Content & Blog152 lines1 dep
Open the full page for this block

Résumé Document

A CV as a document rather than a timeline of cards — one column, dates on the end edge, and a print layer that puts it on paper without a separate PDF to keep in step.

Content & Blog347 lines1 dep