# Author Card

> Person identity card — avatar, name, role, optional bio, optionally clickable.

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

- Page, with a live preview: https://shadcn.exchange/r/ilinxa/author-card
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ilinxa/author-card-light-24e8cd387ad013fd.png
- Registry item: https://ui.ilinxa.com/r/author-card.json
- Categories: marketing
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.ilinxa.com/r/author-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

### `src/registry/components/marketing/author-card/author-card.tsx`

```tsx
import { memo, useId } from "react";
import { User } from "lucide-react";
import { cn } from "@/lib/utils";
import { AuthorAvatar } from "./parts/avatar";
import { resolveTone } from "./parts/tone-resolver";
import { AUTHOR_CARD_DEFAULT_LABELS, type AuthorCardProps } from "./types";

function AuthorCardInner(props: AuthorCardProps) {
  const {
    name,
    role,
    bio,
    imageSrc,
    imageAlt,
    fallbackIcon = User,
    href,
    linkComponent,
    tone = "primary",
    headingAs: HeadingTag = "h3",
    labels,
    className,
    headingClassName,
    nameClassName,
    bioClassName,
  } = props;

  const nameId = useId();
  const toneClasses = resolveTone(tone);
  const resolvedLabels = { ...AUTHOR_CARD_DEFAULT_LABELS, ...labels };
  const resolvedAlt = imageAlt ?? name;

  const isClickable = Boolean(href);
  const RootEl = isClickable ? linkComponent ?? "a" : "div";

  const rootClass = cn(
    "block bg-card rounded-2xl p-6 border border-border/50 transition-colors",
    isClickable &&
      "hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
    className
  );

  const linkProps = isClickable
    ? { href, "aria-labelledby": nameId }
    : {};

  return (
    <RootEl className={rootClass} {...linkProps}>
      <HeadingTag
        className={cn(
          "text-lg font-serif font-bold text-foreground mb-4",
          headingClassName
        )}
      >
        {resolvedLabels.heading}
      </HeadingTag>

      <div className="flex items-center gap-4">
        <AuthorAvatar
          imageSrc={imageSrc}
          imageAlt={resolvedAlt}
          fallbackIcon={fallbackIcon}
          toneClasses={toneClasses}
        />
        <div>
          <p
            id={nameId}
            className={cn("font-semibold text-foreground", nameClassName)}
          >
            {name}
          </p>
          <p className="text-sm text-muted-foreground">{role}</p>
        </div>
      </div>

      {bio ? (
        <p className={cn("text-sm text-muted-foreground mt-4", bioClassName)}>
          {bio}
        </p>
      ) : null}
    </RootEl>
  );
}

export const AuthorCard = memo(AuthorCardInner);
AuthorCard.displayName = "AuthorCard";
```

### `src/registry/components/marketing/author-card/index.ts`

```ts
export { AuthorCard } from "./author-card";
export {
  AUTHOR_CARD_DEFAULT_LABELS,
  type AuthorCardLabels,
  type AuthorCardProps,
  type AuthorCardTone,
} from "./types";
```

### `src/registry/components/marketing/author-card/types.ts`

```ts
import type { ComponentType, ElementType } from "react";

export type AuthorCardTone = "primary" | "accent" | "muted";

export interface AuthorCardLabels {
  heading?: string;
}

export interface AuthorCardProps {
  name: string;
  role: string;
  bio?: string;
  imageSrc?: string;
  imageAlt?: string;
  fallbackIcon?: ComponentType<{ className?: string }>;
  href?: string;
  linkComponent?: ElementType;
  tone?: AuthorCardTone;
  headingAs?: "h2" | "h3" | "h4";
  labels?: AuthorCardLabels;
  className?: string;
  headingClassName?: string;
  nameClassName?: string;
  bioClassName?: string;
}

export const AUTHOR_CARD_DEFAULT_LABELS: Required<AuthorCardLabels> = {
  heading: "About the author",
};
```

### `src/registry/components/marketing/author-card/parts/avatar.tsx`

```tsx
import type { ComponentType } from "react";
import { cn } from "@/lib/utils";
import type { ToneClasses } from "./tone-resolver";

interface AuthorAvatarProps {
  imageSrc?: string;
  imageAlt: string;
  fallbackIcon: ComponentType<{ className?: string }>;
  toneClasses: ToneClasses;
}

export function AuthorAvatar({
  imageSrc,
  imageAlt,
  fallbackIcon: Icon,
  toneClasses,
}: AuthorAvatarProps) {
  if (imageSrc) {
    return (
      <img
        src={imageSrc}
        alt={imageAlt}
        loading="lazy"
        className="w-16 h-16 rounded-full object-cover shrink-0"
      />
    );
  }
  return (
    <div
      className={cn(
        "w-16 h-16 rounded-full flex items-center justify-center shrink-0",
        toneClasses.avatarBg
      )}
      aria-hidden="true"
    >
      <Icon className={cn("w-8 h-8", toneClasses.avatarIcon)} />
    </div>
  );
}
```

### `src/registry/components/marketing/author-card/parts/tone-resolver.ts`

```ts
import type { AuthorCardTone } from "../types";

export interface ToneClasses {
  avatarBg: string;
  avatarIcon: string;
}

const TONE_MAP: Record<AuthorCardTone, ToneClasses> = {
  primary: { avatarBg: "bg-primary/10", avatarIcon: "text-primary" },
  accent: { avatarBg: "bg-accent", avatarIcon: "text-accent-foreground" },
  muted: { avatarBg: "bg-muted", avatarIcon: "text-muted-foreground" },
};

export function resolveTone(tone: AuthorCardTone): ToneClasses {
  return TONE_MAP[tone];
}
```
