Skip to content
All components

Coverage Card

Stable

CoverageBilling and coverage

Renders insurance coverage with payer, plan, member and group identifiers, and the active period. A status of active is not sufficient to say a coverage is usable: a record can carry an active status while its period has already ended, and acting on lapsed coverage produces a denied claim and a surprise bill for the patient. The effective state is therefore derived from status and period together.

pnpm dlx shadcn@latest add @oxygenui/coverage-card

First install? Add "@oxygenui": "https://oxygenui.design/r/{name}.json" to the registries block of your components.json first — or skip the config and pass https://oxygenui.design/r/coverage-card.json directly.

Preview

Every state, switchable.

Live · synthetic data

Meridian Health Plan

Meridian Choice PPO

Active
Member ID
882-401-556
Group
GRP-40218
Relationship
self
Effective
2026-01-012026-12-31

Status active and inside its period.

ActiveLapsedNot yet effectiveCancelledStatus unknownMasked identifiers

Usage

Props are the FHIR resource.

import { CoverageCard } from "@/components/oxygen/coverage-card";

<CoverageCard
  coverage={coverage}
  maskIdentifiers={isSharedWorkstation}
/>

Dependencies

  • @oxygenui-design/fhir@^0.1.0
  • lucide-react
  • clsx
  • tailwind-merge
Coverage Card props
PropTypeDefault
coverage

Coverage | undefined
asOf

Date used to evaluate the coverage period. Pass a fixed date for deterministic tests.

Date | undefinednew Date()
loading

boolean | undefinedfalse
maskIdentifiers

Mask all but the last four characters of member and group identifiers.

boolean | undefinedfalse

Guidance

When to use it, and when not to.

DECISION SURFACE / 6 RULES

Recommended context

Use it

03
  • On registration, check-in, and billing screens.
  • With maskIdentifiers on front-desk and shared workstations.
  • Rendering every coverage in order — secondary coverage matters for coordination of benefits.

Guardrails

Don't

03
  • As an eligibility check. A rendered card is not a real-time eligibility response from the payer.
  • Showing only the primary coverage. Dropping secondary coverage causes downstream billing errors.
  • Trusting status alone in your own code — use coverageState from @oxygenui-design/fhir.

Quality

What was tested, and what is still missing.

Lapsed is explicit
A lapsed coverage adds a written instruction to verify eligibility, not just a red border.
Masked identifiers
Screen readers receive the last four characters and a statement that the value is masked.
Absent fields
Every unpopulated field renders as explicitly not recorded rather than as an empty cell.

Open gaps

Known limitations

03
  • Renders payor[0] only; multi-payer coverage shows the first.
  • Cost-to-beneficiary (copay, deductible) is not rendered.
  • No eligibility or benefits verification — display only.

Source

Exactly what lands in your repository.

Read directly from the published registry, so this can never drift from what the CLI installs.

Show source181 lines
"use client";

/**
 * CoverageCard — insurance coverage from a FHIR Coverage resource.
 *
 * `status: "active"` is not sufficient to say a coverage is usable. A record
 * can carry an active status while its period has already ended, and acting on
 * lapsed coverage produces a denied claim and a surprise bill for the patient.
 * The effective state is therefore derived from status AND period together.
 *
 * Member identifiers are maskable for the same reason they are on the patient
 * banner: these screens are read at shared desks and over shoulders.
 */

import * as React from "react";
import { CalendarClock, CircleCheck, CircleSlash, ShieldX } from "lucide-react";
import {
  codeableText,
  coverageClass,
  coverageState,
  maskIdentifier,
  type Coverage,
  type CoverageState,
} from "@oxygenui-design/fhir";
import { StatusBadge, type StatusTone } from "@/components/oxygen/status-badge";
import { cn } from "@/lib/utils";

const STATE_PRESENTATION: Record<
  CoverageState,
  { label: string; tone: StatusTone; icon: React.ComponentType<{ className?: string }> }
> = {
  active: { label: "Active", tone: "normal", icon: CircleCheck },
  "not-yet-effective": { label: "Not yet effective", tone: "high", icon: CalendarClock },
  lapsed: { label: "Lapsed", tone: "critical", icon: ShieldX },
  cancelled: { label: "Cancelled", tone: "neutral", icon: CircleSlash },
  unknown: { label: "Status unknown", tone: "unknown", icon: CircleSlash },
};

export interface CoverageCardProps extends React.HTMLAttributes<HTMLDivElement> {
  coverage: Coverage | undefined;
  /** Date used to evaluate the coverage period. Pass a fixed date for deterministic tests. */
  asOf?: Date;
  /** Mask all but the last four characters of member and group identifiers. */
  maskIdentifiers?: boolean;
  loading?: boolean;
}

export function CoverageCard({
  coverage,
  asOf = new Date(),
  maskIdentifiers: shouldMask = false,
  loading = false,
  className,
  ...props
}: CoverageCardProps) {
  if (loading) {
    return (
      <div
        role="status"
        aria-busy="true"
        aria-label="Loading coverage"
        className={cn(
          "rounded-[var(--ox-radius-lg)] border border-[var(--ox-border)] bg-[var(--ox-surface)] px-[var(--ox-density-pad-x)] py-[var(--ox-density-pad-y)]",
          className,
        )}
        {...props}
      >
        <div className="h-4 w-36 animate-pulse rounded bg-[var(--ox-bg-muted)]" />
        <div className="mt-3 h-3.5 w-48 animate-pulse rounded bg-[var(--ox-bg-muted)]" />
        <span className="sr-only">Loading coverage</span>
      </div>
    );
  }

  const state = coverageState(coverage, asOf);
  const presentation = STATE_PRESENTATION[state];
  const payer = coverage?.payor?.[0]?.display;
  const plan = coverageClass(coverage, "plan") ?? codeableText(coverage?.type);
  const group = coverageClass(coverage, "group");
  const memberId = shouldMask ? maskIdentifier(coverage?.subscriberId) : coverage?.subscriberId;
  const relationship = codeableText(coverage?.relationship);
  const { start, end } = coverage?.period ?? {};

  return (
    <div
      data-state={state}
      className={cn(
        "rounded-[var(--ox-radius-lg)] border bg-[var(--ox-surface)]",
        "px-[var(--ox-density-pad-x)] py-[var(--ox-density-pad-y)]",
        "text-[length:var(--ox-density-font)]",
        state === "lapsed"
          ? "border-[var(--ox-status-critical-border)] bg-[var(--ox-status-critical-bg)]"
          : "border-[var(--ox-border)]",
        className,
      )}
      {...props}
    >
      <div className="flex items-start justify-between gap-3">
        <div className="min-w-0">
          <h3 className="truncate font-semibold text-[var(--ox-text)]">
            {payer ?? (
              <span className="font-normal italic text-[var(--ox-text-muted)]">
                Payer not recorded
              </span>
            )}
          </h3>
          {plan && (
            <p className="mt-0.5 text-[length:var(--ox-text-sm)] text-[var(--ox-text-muted)]">
              {plan}
            </p>
          )}
        </div>

        <StatusBadge tone={presentation.tone} icon={presentation.icon} className="shrink-0">
          {presentation.label}
        </StatusBadge>
      </div>

      <dl className="mt-3 grid gap-x-4 gap-y-2 border-t border-[var(--ox-border)] pt-3 text-[length:var(--ox-text-xs)] sm:grid-cols-2">
        <Field label="Member ID">
          {memberId ? (
            <span className="font-[family-name:var(--ox-font-numeric)] tabular-nums">
              {memberId}
              {shouldMask && coverage?.subscriberId && (
                <span className="sr-only"> (masked, ending {coverage.subscriberId.slice(-4)})</span>
              )}
            </span>
          ) : (
            <NotRecorded />
          )}
        </Field>

        <Field label="Group">
          {group ? (
            <span className="font-[family-name:var(--ox-font-numeric)] tabular-nums">{group}</span>
          ) : (
            <NotRecorded />
          )}
        </Field>

        <Field label="Relationship">
          {relationship ? <span className="capitalize">{relationship}</span> : <NotRecorded />}
        </Field>

        <Field label="Effective">
          {start || end ? (
            <span className="font-[family-name:var(--ox-font-numeric)] tabular-nums">
              {start?.slice(0, 10) ?? "—"}
              {" → "}
              {end?.slice(0, 10) ?? "open"}
            </span>
          ) : (
            <NotRecorded />
          )}
        </Field>
      </dl>

      {state === "lapsed" && end && (
        <p className="mt-3 rounded-[var(--ox-radius)] border border-[var(--ox-status-critical-border)] bg-[var(--ox-surface)] px-3 py-2 text-[length:var(--ox-text-sm)] text-[var(--ox-status-critical)]">
          Coverage ended {end.slice(0, 10)}. Verify eligibility before billing.
        </p>
      )}
    </div>
  );
}

function Field({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex items-baseline gap-2">
      <dt className="shrink-0 font-medium uppercase tracking-wide text-[var(--ox-text-subtle)]">
        {label}
      </dt>
      <dd className="min-w-0 truncate text-[var(--ox-text)]">{children}</dd>
    </div>
  );
}

function NotRecorded() {
  return <span className="italic text-[var(--ox-text-subtle)]">Not recorded</span>;
}