Lantern
Code Block.
A code panel with a filename bar, language badge, copy button, optional line numbers and highlighted lines. No syntax highlighter required.
startup.lualua
import { CodeBlock } from "@/components/ui/code-block";
const code = `-- startup.lua: runs when the computer boots
local modem = peripheral.find("modem")
rednet.open(peripheral.getName(modem))
print("Lantern hub online")
while true do
local id, message = rednet.receive("lantern")
print(("[%d] %s"):format(id, message))
end`;
export default function CodeBlockDemo() {
return <CodeBlock title="startup.lua" language="lua" code={code} className="w-full max-w-xl" />;
}Installation#
With the CLI
$npx lanterncn add code-blockThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/code-block. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install lucide-reactCopy the source into your project
components/ui/code-block.tsx"use client"; import * as React from "react"; import { Check, Copy } from "lucide-react"; import { cn } from "@/lib/utils"; function CodeBlockCopyButton({ value, className, ...props }: Omit<React.ComponentProps<"button">, "value" | "onClick"> & { value: string | (() => string) }) { const [copied, setCopied] = React.useState(false); const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null); React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); async function copy() { try { await navigator.clipboard.writeText(typeof value === "function" ? value() : value); setCopied(true); if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => setCopied(false), 1500); } catch { // Clipboard access can be blocked; leave the button in its idle state. } } return ( <button type="button" data-slot="code-block-copy" data-copied={copied || undefined} aria-label={copied ? "Copied" : "Copy code"} onClick={copy} className={cn( "inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md border border-border bg-background/80 text-muted-foreground transition-colors outline-none hover:border-input hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-terminal [&_svg]:size-3.5", className, )} {...props} > {copied ? <Check className="text-success" /> : <Copy />} <span className="sr-only" aria-live="polite"> {copied ? "Copied to clipboard" : ""} </span> </button> ); } type CodeBlockProps = Omit<React.ComponentProps<"div">, "title"> & { /** Plain source text. Rendered line by line with the default Lantern text color. */ code?: string; /** Filename or label shown in the title bar. */ title?: React.ReactNode; /** Language badge in the title bar, e.g. "lua". */ language?: string; /** Show line numbers. Applies to `code`. */ showLineNumbers?: boolean; /** 1-based line numbers to mark with an orange bar and tint. Applies to `code`. */ highlightLines?: number[]; /** Max height of the scroll area, e.g. 320 or "20rem". */ maxHeight?: number | string; /** Text copied by the copy button. Defaults to `code`, or the rendered text of `children`. */ copyValue?: string; /** Hide the copy button. */ copyable?: boolean; }; /** * A code panel without a highlighter dependency. Pass `code` for plain text, or `children` * for content you highlighted yourself (for example shiki HTML). */ function CodeBlock({ code, title, language, showLineNumbers = false, highlightLines, maxHeight, copyValue, copyable = true, className, children, ...props }: CodeBlockProps) { const bodyRef = React.useRef<HTMLDivElement>(null); const hasHeader = title != null || language != null; const highlighted = React.useMemo(() => new Set(highlightLines ?? []), [highlightLines]); const lines = React.useMemo(() => (code === undefined ? null : code.replace(/\n$/, "").split("\n")), [code]); const getCopyValue = () => copyValue ?? code ?? bodyRef.current?.innerText ?? ""; const copyButton = copyable ? <CodeBlockCopyButton value={getCopyValue} /> : null; return ( <div data-slot="code-block" className={cn("relative min-w-0 overflow-hidden rounded-lg border bg-terminal text-terminal-foreground", className)} {...props} > {hasHeader && ( <div data-slot="code-block-header" className="flex min-h-11 items-center justify-between gap-3 border-b bg-[#16211c] py-2 pr-2 pl-4" > <span className="min-w-0 truncate font-mono text-[11px] text-[#a8b89b]">{title}</span> <span className="flex shrink-0 items-center gap-2"> {language && ( <span data-slot="code-block-language" className="rounded-sm border border-border px-1.5 py-1 font-mono text-[9px] leading-none tracking-[0.16em] text-muted-foreground uppercase" > {language} </span> )} {copyButton} </span> </div> )} {!hasHeader && copyButton && <div className="absolute top-2 right-2 z-10">{copyButton}</div>} <div ref={bodyRef} data-slot="code-block-body" role="region" aria-label={typeof title === "string" ? title : "Code"} tabIndex={0} style={maxHeight !== undefined ? { maxHeight } : undefined} className={cn( "overflow-auto font-mono text-[13px] leading-[1.7] outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-inset", "[scrollbar-color:var(--color-input)_transparent] [scrollbar-width:thin]", "[&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:font-mono", )} > {lines ? ( <pre className="py-4"> <code className="grid w-max min-w-full"> {lines.map((line, i) => { const n = i + 1; const isHighlighted = highlighted.has(n); return ( <span key={i} data-line={n} data-highlighted={isHighlighted || undefined} className={cn( "flex border-l-2 border-transparent pr-12 pl-[14px]", isHighlighted && "border-primary bg-primary/10", )} > {showLineNumbers && ( <span aria-hidden="true" className={cn( "mr-4 inline-block w-[3ch] shrink-0 text-right text-[#5f6f5a] select-none", isHighlighted && "text-primary", )} > {n} </span> )} <span className="whitespace-pre">{line || " "}</span> </span> ); })} </code> </pre> ) : ( <div className="[&_pre]:p-4">{children}</div> )} </div> </div> ); } export { CodeBlock, CodeBlockCopyButton };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { CodeBlock, CodeBlockCopyButton } from "@/components/ui/code-block";<CodeBlock title="startup.lua" language="lua" code={source} showLineNumbers highlightLines={[2, 3]} />Pass code as a plain string and it renders in the default terminal text color. To use your own highlighter, render its output as children instead, for example a div with shiki HTML, and set copyValue to the raw source.
Line numbers and highlighted lines apply to the code prop. Set maxHeight to cap the panel and scroll the rest.
Examples#
Line numbers and highlighted lines
empty.lualua
import { CodeBlock } from "@/components/ui/code-block";
const code = `local turtle = require("turtle")
for i = 1, 16 do
turtle.select(i)
turtle.dropDown()
end
print("Inventory emptied into the chest below")`;
export default function CodeBlockLineNumbers() {
return (
<CodeBlock
title="empty.lua"
language="lua"
code={code}
showLineNumbers
highlightLines={[3, 4, 5]}
className="w-full max-w-xl"
/>
);
}Long file with max height
miner.lualua
import { CodeBlock } from "@/components/ui/code-block";
const code = `-- miner.lua: dig a 3x3 tunnel and come home
local length = tonumber(...) or 32
local fuelNeeded = length * 2 + 10
local function refuel()
if turtle.getFuelLevel() >= fuelNeeded then return true end
for slot = 1, 16 do
turtle.select(slot)
if turtle.refuel(0) then
turtle.refuel()
if turtle.getFuelLevel() >= fuelNeeded then return true end
end
end
return false
end
local function digColumn()
turtle.digUp()
turtle.digDown()
end
local function step()
while not turtle.forward() do
turtle.dig()
turtle.attack()
end
end
if not refuel() then
print("Not enough fuel, need " .. fuelNeeded)
return
end
for i = 1, length do
turtle.dig()
step()
digColumn()
turtle.turnLeft()
turtle.dig()
turtle.turnRight()
turtle.turnRight()
turtle.dig()
turtle.turnLeft()
end
turtle.turnLeft()
turtle.turnLeft()
for i = 1, length do
step()
end
print("Tunnel finished, " .. length .. " blocks")`;
export default function CodeBlockMaxHeight() {
return <CodeBlock title="miner.lua" language="lua" code={code} showLineNumbers maxHeight={320} className="w-full max-w-xl" />;
}