Avatar
A postage-stamp or postmark avatar.
ALSOALGMEP
Install
$ npx shadcn@latest add https://ephemera.kerrshift.com/r/avatar.jsonOr copy the source below into components/ui/avatar.tsx. Core components need the Paper Theme and Radix where noted.
Usage
example.tsx
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<Avatar>
<AvatarImage src="/ada.jpg" alt="Ada" />
<AvatarFallback>AL</AvatarFallback>
</Avatar>
<Avatar shape="postmark" size="lg">…</Avatar>Built on the Radix Avatar, so keyboard navigation and screen reader support come for free.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | "xl" | "md" | 32, 44, 64 or 88px. |
| shape | "stamp" | "postmark" | "stamp" | Perforated stamp, or a round postmark ring. |
| AvatarFallback | ReactNode | — | Initials shown while the image loads or if it fails. |
Any other props are passed to the root element.
Fonts
Components read optional CSS variables and fall back to system fonts: --font-display, --font-receipt, --font-type, --font-hand and --font-stamp. This site uses Fraunces, IBM Plex Mono, Courier Prime, Caveat and Anton.
Source
components/ui/avatar.tsx
"use client";
import * as React from "react";
import { Avatar as AvatarPrimitive } from "radix-ui";
const SIZES = { sm: 32, md: 44, lg: 64, xl: 88 } as const;
export interface AvatarProps extends React.ComponentProps<typeof AvatarPrimitive.Root> {
size?: keyof typeof SIZES;
/** stamp: perforated postage stamp · postmark: round with an inked ring */
shape?: "stamp" | "postmark";
}
const perf = (r: number) =>
`radial-gradient(${r}px, #0000 98%, #000) round ${-1.5 * r}px ${-1.5 * r}px / ${3 * r}px ${3 * r}px, linear-gradient(#000 0 0) no-repeat 50% / calc(100% - ${3 * r}px) calc(100% - ${3 * r}px)`;
/** A postage-stamp avatar. Pair with AvatarImage and AvatarFallback. */
export function Avatar({ size = "md", shape = "stamp", className = "", style, children, ...props }: AvatarProps) {
const px = SIZES[size];
if (shape === "postmark") {
return (
<AvatarPrimitive.Root
className={`relative inline-flex shrink-0 overflow-hidden rounded-full p-[3px] ${className}`}
style={{ width: px, height: px, fontSize: px * 0.4, boxShadow: "0 0 0 1.5px var(--ink-muted), inset 0 0 0 1px var(--edge)", ...style }}
{...props}
>
<span className="relative flex size-full overflow-hidden rounded-full">{children}</span>
</AvatarPrimitive.Root>
);
}
const r = Math.max(2, Math.round(px / 16));
return (
<span className={`inline-flex shrink-0 ${className}`} style={{ filter: "drop-shadow(0 1px 2px rgb(0 0 0 / .25))" }}>
<AvatarPrimitive.Root
className="relative inline-flex bg-(--paper-raised)"
style={{ width: px, height: px * 1.12, fontSize: px * 0.4, padding: 2 * r, mask: perf(r), WebkitMask: perf(r), ...style }}
{...props}
>
<span className="relative flex size-full overflow-hidden">{children}</span>
</AvatarPrimitive.Root>
</span>
);
}
export function AvatarImage({ className = "", ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return <AvatarPrimitive.Image className={`size-full object-cover ${className}`} {...props} />;
}
export function AvatarFallback({ className = "", style, ...props }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
className={`flex size-full items-center justify-center bg-(--pen) text-[0.9em] font-semibold italic text-(--pen-ink) ${className}`}
style={{ fontFamily: "var(--font-display, Georgia, serif)", ...style }}
{...props}
/>
);
}