Lantern
Server Card.
A Minecraft server status card with a copyable address, online state, a player bar, version badge, MOTD and an initials stack of who is on.
Lantern SMP
Onlineplay.example.net
Turtle races every Friday at spawn
Players7 / 20
1.21.4
THBMRRCK+3
import { ServerCard } from "@/components/ui/server-card";
export default function ServerCardDemo() {
return (
<ServerCard
className="w-full max-w-sm"
name="Lantern SMP"
address="play.example.net"
players={7}
maxPlayers={20}
version="1.21.4"
motd="Turtle races every Friday at spawn"
playerNames={["Thultz", "ByteMiner", "RedstoneRae", "copper_kid", "Ashling", "NightOwl", "Pim"]}
/>
);
}Installation#
With the CLI
$npx lanterncn add server-cardThis 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/server-card. 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/server-card.tsx"use client"; import * as React from "react"; import { ServerIcon, UsersIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { CopyButton } from "@/components/ui/copy-button"; import { StatusPill } from "@/components/ui/status-pill"; function initials(name: string) { const parts = name.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean); return (parts.length > 1 ? parts[0][0] + parts[1][0] : name.slice(0, 2)).toUpperCase(); } /** A Minecraft server status card: address, status, players, version and MOTD. */ function ServerCard({ className, name, address, online = true, players = 0, maxPlayers = 20, version, motd, playerNames, maxAvatars = 4, icon, ...props }: Omit<React.ComponentProps<"div">, "children"> & { name: string; address: string; online?: boolean; players?: number; maxPlayers?: number; version?: string; motd?: React.ReactNode; /** Names of players online, shown as an initials stack. */ playerNames?: string[]; maxAvatars?: number; icon?: React.ReactNode; }) { const count = online ? players : 0; const pct = maxPlayers > 0 ? Math.min(100, Math.round((count / maxPlayers) * 100)) : 0; const shown = online ? (playerNames ?? []).slice(0, maxAvatars) : []; const extra = online ? (playerNames?.length ?? 0) - shown.length : 0; return ( <div data-slot="server-card" data-online={online} className={cn("flex min-w-0 flex-col gap-4 rounded-lg border bg-card p-5 text-card-foreground", className)} {...props} > <div className="flex items-start gap-3"> <div aria-hidden="true" className={cn( "grid size-10 shrink-0 place-items-center rounded-md border bg-accent bg-grid text-[#b7ca9e] [&_svg]:size-5", !online && "opacity-60", )} > {icon ?? <ServerIcon />} </div> <div className="min-w-0 flex-1"> <div className="flex items-center gap-3"> <h3 className="min-w-0 truncate font-display text-lg leading-tight font-medium tracking-tight">{name}</h3> <StatusPill tone={online ? "online" : "offline"} className="ml-auto"> {online ? "Online" : "Offline"} </StatusPill> </div> <div className="-ml-1 flex min-w-0 items-center gap-0.5"> <span className="truncate px-1 font-mono text-[12px] text-muted-foreground">{address}</span> <CopyButton value={address} label={`Copy address ${address}`} className="size-7 [&_svg:not([class*='size-'])]:size-3.5" /> </div> </div> </div> {motd && ( <p data-slot="server-card-motd" className="line-clamp-2 border-l-2 border-input pl-3 font-mono text-[12px] text-[#d9b774]"> {motd} </p> )} <div className="grid gap-2"> <div className="flex items-center gap-2 font-mono text-[10px] tracking-[0.15em] text-muted-foreground uppercase"> <UsersIcon className="size-3.5 text-primary" aria-hidden="true" /> Players <span className="ml-auto text-[12px] tracking-normal text-foreground normal-case"> {count} <span className="text-muted-foreground"> / {maxPlayers}</span> </span> </div> <div role="meter" aria-label="Players online" aria-valuemin={0} aria-valuemax={maxPlayers} aria-valuenow={count} className="h-1 overflow-hidden rounded-[1px] bg-[#2b352f]" > <div className="h-full bg-success transition-[width] duration-300" style={{ width: `${pct}%` }} /> </div> </div> {(version || shown.length > 0) && ( <div className="flex min-h-7 items-center gap-3"> {version && <Badge variant="outline">{version}</Badge>} {shown.length > 0 && ( <AvatarGroup className="ml-auto -space-x-2" aria-label={`Players: ${playerNames?.join(", ")}`} role="img"> {shown.map((p) => ( <Avatar key={p} className="size-7" title={p}> <AvatarFallback className="text-[9px]">{initials(p)}</AvatarFallback> </Avatar> ))} {extra > 0 && ( <Avatar className="size-7"> <AvatarFallback className="bg-secondary text-[9px] text-muted-foreground">+{extra}</AvatarFallback> </Avatar> )} </AvatarGroup> )} </div> )} </div> ); } export { ServerCard };Update the import paths to match your project
The source imports
cnfrom@/lib/utilsand uses avatar, badge, copy-button, status-pill.
Usage#
import { ServerCard } from "@/components/ui/server-card";<ServerCard
name="Lantern SMP"
address="play.example.net"
players={7}
maxPlayers={20}
version="1.21.4"
/>Examples#
Offline
Creative Test
Offline10.0.4.12:25566
Down for a world backup
Players0 / 10
1.20.1
import { ServerCard } from "@/components/ui/server-card";
export default function ServerCardOffline() {
return (
<ServerCard
className="w-full max-w-sm"
name="Creative Test"
address="10.0.4.12:25566"
online={false}
maxPlayers={10}
version="1.20.1"
motd="Down for a world backup"
/>
);
}Server grid
Lantern SMP
Onlineplay.example.net
Players18 / 20
1.21.4
THBMAS
Turtle Lab
Onlinelab.example.net
Players2 / 8
1.20.1
CKPI
Skyblock
Offlinesky.example.net
Players0 / 40
1.19.2
import { ServerCard } from "@/components/ui/server-card";
const servers = [
{ name: "Lantern SMP", address: "play.example.net", players: 18, maxPlayers: 20, version: "1.21.4", playerNames: ["Thultz", "ByteMiner", "Ashling"] },
{ name: "Turtle Lab", address: "lab.example.net", players: 2, maxPlayers: 8, version: "1.20.1", playerNames: ["copper_kid", "Pim"] },
{ name: "Skyblock", address: "sky.example.net", online: false, maxPlayers: 40, version: "1.19.2" },
];
export default function ServerCardGrid() {
return (
<div className="grid w-full gap-4 sm:grid-cols-2 lg:grid-cols-3">
{servers.map((s) => (
<ServerCard key={s.name} {...s} />
))}
</div>
);
}