← ui

email-address

ui

an email address in a row to copy or write to, or the new message it opens

npx shadcn add @ag/email-address
Already installed? Preview an update
npx shadcn add @ag/email-address --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-10-15
  • New: an email address as the page shows one, beside social-profile's shapes. compact, one row to copy it or write to it, for a hover card or a list; window, the new message a mail app opens to it, drawn, for a story of where an address leads. Takes a mailto: link or the bare address.
Updated 2026-10-15 · release notes

also called email, mailto

shelf social & links

uses button, copy, material, panel

the demo above, verbatim

email-address.demo.tsx
"use client"

import { Sample } from "@/app/samples"
import { EmailAddress } from "@/registry/base-nova/ui/email-address"
import { MaterialProvider } from "@/registry/base-nova/ui/material"

export default function Demo() {
  return (
    <div className="space-y-16">
      <Sample
        name="compact"
        glance
        title="compact"
        description="one row, social-profile's compact shape: the address, copy it, write to it"
      >
        <div className="w-full max-w-sm">
          <EmailAddress address="mailto:hello@example.com" />
        </div>
      </Sample>

      <Sample
        name="glass"
        title="compact, in glass"
        description="the row's panel in glass, over a picture"
      >
        <MaterialProvider material="glass" scheme="dark">
          <div className="rounded-xl bg-[url(/lightbox/river.jpg)] bg-cover bg-center p-6">
            <div className="max-w-sm">
              <EmailAddress address="hello@example.com" />
            </div>
          </div>
        </MaterialProvider>
      </Sample>

      <Sample
        name="window"
        title="window"
        description="the new message it opens, drawn: a mail app has no page to show"
      >
        <EmailAddress address="hello@example.com" form="window" />
      </Sample>
    </div>
  )
}

the source, every file an install writes

email-address.tsx
"use client"

// AN EMAIL ADDRESS, as the page shows one. An address is no profile (no face, no
// handle, nothing counted), so it is not social-profile's, but it stands beside one
// in the same shapes:
//
//   compact   social-profile's compact row: an envelope where a face stands, the
//             address, and what is done with it, copy it and write to it. For a
//             hover card on a link's mark, a list of where someone is.
//   window    the new message a mail app opens to it, drawn: what pressing it
//             leads to, for a story that shows where an address goes (a mail app
//             has no page to capture). A window, so plain whatever is round it.
//
// `address` takes a `mailto:` link or the bare address.

import { Mail, PenLine } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/base-nova/ui/button"
import { Copy } from "@/registry/base-nova/ui/copy"
import {
  type Material,
  type Scheme,
  useMaterial,
} from "@/registry/base-nova/ui/material"
import { Panel } from "@/registry/base-nova/ui/panel"

export type EmailAddressForm = "compact" | "window"

export interface EmailAddressProps {
  /** `mailto:someone@example.com`, or the address. */
  address: string
  form?: EmailAddressForm
  /** The compact row's surface (a panel). Default drawn; inside a frame already
   *  drawn, the inset one (Panel). */
  frame?: boolean
  /** The compact row's material: default the nearest provider's. */
  material?: Material
  scheme?: Scheme
  className?: string
}

/** The address alone, whatever form it came in. */
export function bareAddress(address: string): string {
  return address.replace(/^mailto:/i, "").trim()
}

export function EmailAddress({
  address,
  form = "compact",
  frame,
  material,
  scheme,
  className,
}: EmailAddressProps) {
  const m = useMaterial(material)
  const bare = bareAddress(address)
  if (!bare.includes("@"))
    throw new Error(`EmailAddress: ${address} is no email address`)

  if (form === "window")
    return (
      <Panel
        data-slot="email-address"
        data-form="window"
        material="plain"
        radius="1rem"
        className={cn("w-full max-w-3xl", className)}
        bodyClassName="block p-0 text-xl"
        head={<span className="font-semibold text-xl">new message</span>}
      >
        <div className="flex items-center gap-3 border-current/10 border-b px-7 py-4">
          <span className="text-current/45">to:</span>
          {/* The recipient as a mail app chips it, in its own blue. */}
          <span className="rounded-full bg-[#0a84ff]/20 px-3 py-0.5 text-[#0a84ff]">
            {bare}
          </span>
        </div>
        <div className="flex gap-3 border-current/10 border-b px-7 py-4">
          <span className="text-current/45">subject:</span>
        </div>
        {/* The body, empty, the caret waiting in it. */}
        <div className="h-72 px-7 py-5">
          <span className="inline-block h-6 w-0.5 bg-[#0a84ff]" />
        </div>
      </Panel>
    )

  const framed = frame ?? true
  return (
    <Panel
      data-slot="email-address"
      data-form="compact"
      material={m}
      scheme={scheme}
      frame={framed}
      bodyClassName={cn(
        "flex-row items-center gap-3",
        framed ? "px-3 py-2.5" : "p-0",
      )}
      className={cn("font-sans text-sm", className)}
    >
      <span className="grid size-10 shrink-0 place-items-center rounded-full bg-current/[0.06] [&_svg]:size-5 [&_svg]:stroke-[1.5]">
        <Mail aria-hidden />
      </span>
      <div className="min-w-0 flex-1">
        <div className="truncate font-semibold leading-tight">{bare}</div>
        <div className="text-[13px] text-current/55">email</div>
      </div>
      <Copy value={bare} />
      <Button
        render={<a href={`mailto:${bare}`} />}
        variant="ghost"
        size="icon"
        aria-label={`write to ${bare}`}
      >
        <PenLine aria-hidden />
      </Button>
    </Panel>
  )
}