# Server Card

> Fleet of host cards, each with a status pill, region, CPU / memory / disk specs and threshold-tinted usage meters. Ships composable parts.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/server-card).

- Page, with a live preview: https://shadcn.exchange/r/hirael/server-card
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/server-card-light-9f7c827c9aeccde6.png
- Registry item: https://hirael.com/r/server-card.json
- Categories: blocks, cloud
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/server-card.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`

## Files

### `registry/hirael/bases/radix/blocks/server-card/server-card.tsx`

```tsx
// Server Card from Hirael <https://hirael.com/blocks/cloud/server-card>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

import * as React from 'react';
import { Cpu, HardDrive, MemoryStick } from 'lucide-react';

import { cn } from '@/lib/utils';

export type ServerStatus = 'online' | 'degraded' | 'offline' | 'provisioning';

const statusTone: Record<ServerStatus, string> = {
  online: 'bg-success text-success',
  degraded: 'bg-warning text-warning',
  offline: 'bg-muted-foreground text-muted-foreground',
  provisioning: 'bg-info text-info',
};

type ServerCardProps = React.ComponentProps<'div'>;

const ServerCard = ({ className, ...props }: ServerCardProps) => {
  return (
    <div
      data-slot="server-card"
      className={cn('flex flex-col gap-4 rounded-lg border border-border bg-card p-5 text-card-foreground', className)}
      {...props}
    />
  );
};

type ServerCardHeaderProps = React.ComponentProps<'div'>;

const ServerCardHeader = ({ className, ...props }: ServerCardHeaderProps) => {
  return (
    <div
      data-slot="server-card-header"
      className={cn('flex items-start justify-between gap-3', className)}
      {...props}
    />
  );
};

interface ServerCardTitleProps extends React.ComponentProps<'div'> {
  region?: React.ReactNode;
}

const ServerCardTitle = ({ region, className, children, ...props }: ServerCardTitleProps) => {
  return (
    <div data-slot="server-card-title" className={cn('flex min-w-0 flex-col gap-1', className)} {...props}>
      <span className="truncate text-sm font-medium text-foreground">{children}</span>
      {region ? <span className="truncate text-xs text-muted-foreground">{region}</span> : null}
    </div>
  );
};

interface ServerCardStatusProps extends Omit<React.ComponentProps<'span'>, 'children'> {
  status: ServerStatus;
  children?: React.ReactNode;
}

const ServerCardStatus = ({ status, className, children, ...props }: ServerCardStatusProps) => {
  const live = status === 'provisioning';

  return (
    <span
      data-slot="server-card-status"
      data-status={status}
      className={cn(
        'inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-accent px-2 py-0.5 text-[11px] font-medium capitalize',
        statusTone[status].split(' ')[1],
        className,
      )}
      {...props}
    >
      <span className="relative flex size-1.5">
        {live ? (
          <span
            className={cn(
              'absolute inline-flex size-full animate-ping rounded-full opacity-75 motion-reduce:animate-none',
              statusTone[status].split(' ')[0],
            )}
            aria-hidden
          />
        ) : null}
        <span
          className={cn('relative inline-flex size-1.5 rounded-full', statusTone[status].split(' ')[0])}
          aria-hidden
        />
      </span>
      {children ?? status}
    </span>
  );
};

type ServerCardSpecsProps = React.ComponentProps<'dl'>;

const ServerCardSpecs = ({ className, ...props }: ServerCardSpecsProps) => {
  return <dl data-slot="server-card-specs" className={cn('grid grid-cols-3 gap-3', className)} {...props} />;
};

const specIcon = {
  cpu: Cpu,
  memory: MemoryStick,
  disk: HardDrive,
} as const;

interface ServerCardSpecProps extends React.ComponentProps<'div'> {
  label: React.ReactNode;
  icon?: keyof typeof specIcon;
}

const ServerCardSpec = ({ label, icon, className, children, ...props }: ServerCardSpecProps) => {
  const Icon = icon ? specIcon[icon] : null;

  return (
    <div data-slot="server-card-spec" className={cn('flex flex-col gap-1', className)} {...props}>
      <dt className="flex items-center gap-1 text-[10px] tracking-[0.1em] text-muted-foreground uppercase">
        {Icon ? <Icon className="size-3" aria-hidden /> : null}
        {label}
      </dt>
      <dd className="text-sm text-foreground">{children}</dd>
    </div>
  );
};

interface ServerCardMeterProps extends React.ComponentProps<'div'> {
  label: React.ReactNode;
  value: number;
  /** Fraction (0 to 1) where the bar switches to warning, then destructive. */
  thresholds?: [warn: number, crit: number];
}

const ServerCardMeter = ({ label, value, thresholds = [0.75, 0.9], className, ...props }: ServerCardMeterProps) => {
  const pct = Math.max(0, Math.min(100, value));
  const frac = pct / 100;
  const [warn, crit] = thresholds;
  const tone = frac >= crit ? 'bg-destructive' : frac >= warn ? 'bg-warning' : 'bg-foreground/60';

  return (
    <div data-slot="server-card-meter" className={cn('flex flex-col gap-1.5', className)} {...props}>
      <div className="flex items-center justify-between text-xs">
        <span className="text-muted-foreground">{label}</span>
        <span className="text-foreground">{pct}%</span>
      </div>
      <div className="h-1.5 overflow-hidden rounded-full bg-muted">
        <div
          className={cn('h-full rounded-full transition-[inline-size] duration-300', tone)}
          style={{ inlineSize: `${pct}%` }}
        />
      </div>
    </div>
  );
};

export {
  ServerCard,
  ServerCardHeader,
  ServerCardTitle,
  ServerCardStatus,
  ServerCardSpecs,
  ServerCardSpec,
  ServerCardMeter,
};

const ENTER =
  'animate-in fade-in slide-in-from-bottom-2 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';

const SERVERS: {
  name: string;
  region: string;
  provider: string;
  status: ServerStatus;
  statusLabel: string;
  specs: { cpu: string; memory: string; disk: string };
  meters: { label: string; value: number }[];
}[] = [
  {
    name: 'web-prod-01',
    region: 'us-east-1',
    provider: 'AWS',
    status: 'online',
    statusLabel: 'Online',
    specs: { cpu: '8', memory: '32 GB', disk: '512 GB' },
    meters: [
      { label: 'CPU', value: 62 },
      { label: 'Memory', value: 81 },
    ],
  },
  {
    name: 'db-replica-03',
    region: 'eu-west-2',
    provider: 'AWS',
    status: 'degraded',
    statusLabel: 'Degraded',
    specs: { cpu: '16', memory: '64 GB', disk: '2 TB' },
    meters: [
      { label: 'CPU', value: 94 },
      { label: 'Disk', value: 88 },
    ],
  },
  {
    name: 'cache-02',
    region: 'ap-south-1',
    provider: 'AWS',
    status: 'provisioning',
    statusLabel: 'Provisioning',
    specs: { cpu: '4', memory: '16 GB', disk: '128 GB' },
    meters: [
      { label: 'CPU', value: 7 },
      { label: 'Memory', value: 12 },
    ],
  },
];

const ServerCardBlock = () => {
  return (
    <section data-slot="server-card-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <div className="grid w-full max-w-3xl gap-4 sm:grid-cols-2">
        {SERVERS.map((server, index) => (
          <ServerCard key={server.name} style={{ animationDelay: `${index * 60}ms` }} className={ENTER}>
            <ServerCardHeader>
              <ServerCardTitle
                region={
                  <>
                    <span>{server.region}</span>
                    <span aria-hidden className="mx-1.5 text-border">
                      |
                    </span>
                    <span>{server.provider}</span>
                  </>
                }
              >
                {server.name}
              </ServerCardTitle>
              <ServerCardStatus status={server.status}>{server.statusLabel}</ServerCardStatus>
            </ServerCardHeader>
            <ServerCardSpecs>
              <ServerCardSpec icon="cpu" label="vCPU">
                {server.specs.cpu}
              </ServerCardSpec>
              <ServerCardSpec icon="memory" label="Memory">
                {server.specs.memory}
              </ServerCardSpec>
              <ServerCardSpec icon="disk" label="Disk">
                {server.specs.disk}
              </ServerCardSpec>
            </ServerCardSpecs>
            <div className="grid gap-3">
              {server.meters.map((meter) => (
                <ServerCardMeter key={meter.label} label={meter.label} value={meter.value} />
              ))}
            </div>
          </ServerCard>
        ))}
      </div>
    </section>
  );
};

export default ServerCardBlock;
```
