shadimage
afterglow
componentboot-logsource: ok · hit

Boot Log

A self test that prints itself line by line, at its final height from the first frame.

npx shadcn add "https://afterglow.thebuilder.dk/r/boot-log.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light

Registry dependencies

@afterglow/caret

Files1 · source cached 10/9/2026, 5:25:32 PM

registry/terminal/components/boot-log.tsx
"use client";

import { useEffect, useRef, useState } from "react";

import { cn } from "@/lib/utils";
import { Caret } from "@/registry/terminal/components/caret";

type BootTone = "default" | "dim" | "ok" | "warn" | "error";

type BootLine = string | { text: string; tone?: BootTone };

const TONE_CLASS: Record<BootTone, string> = {
  default: "text-phosphor-bright",
  dim: "text-phosphor-dim",
  error: "text-signal",
  ok: "text-phosphor",
  warn: "text-warning",
};

function toLine(line: BootLine): { text: string; tone: BootTone } {
  return typeof line === "string"
    ? { text: line, tone: "default" }
    : { text: line.text, tone: line.tone ?? "default" };
}

function BootLog({
  className,
  lines,
  interval = 240,
  prefix = ">",
  onComplete,
  ...props
}: Omit<React.ComponentProps<"ol">, "children"> & {
  lines: BootLine[];
  interval?: number;
  prefix?: string;
  onComplete?: () => void;
}) {
  const [printed, setPrinted] = useState(0);
  const total = lines.length;
  const done = printed >= total;
  const finished = useRef(onComplete);

  useEffect(() => {
    finished.current = onComplete;
  }, [onComplete]);

  useEffect(() => {
    if (done) {
      return;
    }

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");
    if (reduced.matches) {
      setPrinted(total);
      return;
    }

    const timer = window.setTimeout(() => setPrinted(printed + 1), interval);
    return () => window.clearTimeout(timer);
  }, [printed, total, interval, done]);

  useEffect(() => {
    if (done) {
      finished.current?.();
    }
  }, [done]);

  return (
    <ol
      className={cn(
        "m-0 grid list-none gap-1 p-0 font-mono text-xs",
        className
      )}
      data-slot="boot-log"
      {...props}
    >
      {lines.map(toLine).map((line, index) => {
        const shown = index < printed;

        return (
          <li
            aria-hidden={shown ? undefined : "true"}
            className={cn(
              "flex gap-2 leading-relaxed",
              TONE_CLASS[line.tone],
              shown ? "animate-type" : "invisible"
            )}
            key={line.text}
          >
            <span className="shrink-0 text-phosphor-dim">{prefix}</span>
            <span className="min-w-0">{line.text}</span>
            {shown && index === printed - 1 && printed < total && (
              <Caret className="self-center" />
            )}
          </li>
        );
      })}
    </ol>
  );
}

export type { BootLine, BootTone };
export { BootLog };

More from afterglow

Browse all components in this library →