tweetCard
A Core block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call tweetCard() with no arguments for a working demo, or edit the code below live.
Props
| Prop | Type | Description |
|---|---|---|
tweetId | string | Tweet id to resolve via fetchTweet. Ignored when tweet is provided. |
tweet | TweetData | Pre-fetched tweet data — renders synchronously with no loading skeleton (the server-rendered path). |
fetchTweet | TweetFetcher | Injectable data source, so the card is testable against static fixtures without a real network call. Defaults to a bundled mock fixture lookup. |
theme | "light" | "dark" | Forces the card's subtree to a specific theme instead of inheriting the ambient page theme. |
showMedia | boolean | — |
showQuotedTweet | boolean | — |
Implementation notes
Independently designed TweetData contract (author/avatar/verified, text, media, linkPreview, quotedTweet capped at 1 nesting level, createdAt) with an injectable fetchTweet for testability (defaults to an in-memory fixture mock — no real network call). Header (avatar/name/verified-badge/handle/platform-glyph), body with @mention/#hashtag/URL entity styling via a regex tokenizer, media grid (1-4 images), external link-preview card, nested quoted-tweet card, footer timestamp, pulsing loading skeleton, and an 'unavailable' fallback on fetch rejection. Supports both the server-rendered path (tweet prop, renders synchronously with zero flicker) and the client-fetch path (tweetId + async phases). theme prop maps to Domphy's own dataTheme attribute override. One real bug found and fixed during testing: the phase-switching reactive root needed distinct _keys per phase (skeleton/error/body) — without them the reconciler positionally patched the old skeleton DOM in place instead of replacing it, leaking stale attributes (e.g. the skeleton's aria-label) onto the loaded body; fixed and covered by a test. Verified-badge and platform-logo glyphs are deliberately original generic icons, not reproductions of any platform's trademarked logo.
Status: ported · Reference: Magic UI original
// magicui "Tweet Card" — clean-room reimplementation from the public
// behavior/visual spec only (no upstream source viewed or copied). A card
// that replicates a single social-post embed: avatar/name/verified-badge
// header, body text with mention/hashtag/link styling, optional media grid
// or link-preview card, an optional nested quoted post, and a footer
// timestamp. Shows a pulsing skeleton while data loads and a graceful
// fallback on fetch failure. The tweet-data contract (author, text, media,
// createdAt, optional quoted tweet) and the injectable `fetchTweet` are an
// independently designed shape to build against, not lifted from any
// existing fetching library.
import type {
BehaviorInstance,
DomphyElement,
ElementNode,
Listener,
State,
StyleObject,
} from "@domphy/core";
import { behavior, toState } from "@domphy/core";
import { themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import {
avatar,
empty,
icon,
paragraph,
skeleton,
small,
strong,
} from "@domphy/ui";
import { fixed } from "../../shared/typography.js";
export interface TweetAuthor {
name: string;
handle: string;
avatarUrl?: string;
verified?: boolean;
/** Link to the author's profile (upstream `tweet.user.url`). When set, the
* avatar, name and handle become clickable anchors to it. */
profileUrl?: string;
}
export interface TweetMedia {
url: string;
alt?: string;
}
export interface TweetLinkPreview {
url: string;
title: string;
description?: string;
thumbnailUrl?: string;
}
export interface TweetData {
id: string;
author: TweetAuthor;
text: string;
createdAt: string | number | Date;
/** Permalink to the post itself (upstream `tweet.url`). When set, the header's
* platform icon becomes a link to it carrying an sr-only "Link to tweet" label. */
url?: string;
media?: TweetMedia[];
linkPreview?: TweetLinkPreview;
quotedTweet?: TweetData;
}
export type TweetFetcher = (tweetId: string) => Promise<TweetData>;
export interface TweetCardProps {
/** Tweet id to resolve via `fetchTweet`. Ignored when `tweet` is provided. */
tweetId?: string;
/** Pre-fetched tweet data — renders synchronously with no loading skeleton (the server-rendered path). */
tweet?: TweetData;
/** Injectable data source, so the card is testable against static fixtures without a real network call. Defaults to a bundled mock fixture lookup. */
fetchTweet?: TweetFetcher;
/** Forces the card's subtree to a specific theme instead of inheriting the ambient page theme. */
theme?: "light" | "dark";
showMedia?: boolean;
showQuotedTweet?: boolean;
}
type TweetPhase = "loading" | "loaded" | "error";
const TWEET_CARD_BEHAVIOR_KEY = "magicui-tweet-card";
interface TweetCardBehaviorProps {
phase: State<TweetPhase>;
tweetState: State<TweetData | null>;
tweet?: TweetData;
tweetId?: string;
fetchTweet: TweetFetcher;
}
interface TweetCardBehavior extends BehaviorInstance<TweetCardBehaviorProps> {
phase: State<TweetPhase>;
tweetState: State<TweetData | null>;
}
function attachTweetCard(
_node: ElementNode,
initialProps: TweetCardBehaviorProps,
): TweetCardBehavior {
// Persist generation 1's phase/tweetState so a fetch that resolves after
// an ancestor re-render still writes the State the live children read.
const phase = initialProps.phase;
const tweetState = initialProps.tweetState;
let cancelled = false;
let latestId = initialProps.tweetId;
function startFetch(props: TweetCardBehaviorProps): void {
if (props.tweet || !props.tweetId) return;
cancelled = false;
latestId = props.tweetId;
props
.fetchTweet(props.tweetId)
.then((data) => {
if (cancelled || latestId !== props.tweetId) return;
tweetState.set(data);
phase.set("loaded");
})
.catch(() => {
if (cancelled || latestId !== props.tweetId) return;
phase.set("error");
});
}
startFetch(initialProps);
return {
phase,
tweetState,
update(next) {
if (next.tweet) {
tweetState.set(next.tweet);
phase.set("loaded");
return;
}
if (next.tweetId && next.tweetId !== latestId) {
cancelled = true;
phase.set("loading");
startFetch(next);
}
},
destroy() {
cancelled = true;
},
};
}
function elementNodeOf(listener: Listener): ElementNode | null {
const fromListener = (listener as { elementNode?: ElementNode }).elementNode;
if (fromListener && typeof fromListener.getBehavior === "function") {
return fromListener;
}
if (typeof (listener as unknown as ElementNode).getBehavior === "function") {
return listener as unknown as ElementNode;
}
return null;
}
function tweetCardState(
listener: Listener,
fallbackPhase: State<TweetPhase>,
fallbackTweet: State<TweetData | null>,
): { phase: State<TweetPhase>; tweetState: State<TweetData | null> } {
const instance = elementNodeOf(listener)?.getBehavior<TweetCardBehavior>(
TWEET_CARD_BEHAVIOR_KEY,
);
if (instance?.phase && instance.tweetState) {
return { phase: instance.phase, tweetState: instance.tweetState };
}
return { phase: fallbackPhase, tweetState: fallbackTweet };
}
const DEFAULT_TWEET: TweetData = {
id: "domphy-demo-1",
author: {
name: "Ada Byte",
handle: "adabyte",
verified: true,
profileUrl: "https://domphy.com/@adabyte",
},
text: "Shipping a whole design system as plain objects keyed by HTML tag. No JSX, no virtual DOM. @domphy #buildinpublic https://domphy.com",
createdAt: "2026-06-18T15:32:00Z",
url: "https://domphy.com/@adabyte/posts/domphy-demo-1",
linkPreview: {
url: "https://domphy.com",
title: "Domphy — the AI-friendly UI framework",
description:
"Patch-based UI for native elements, with a runtime built for reactivity and theming.",
},
};
const MOCK_TWEET_FIXTURES: Record<string, TweetData> = {
[DEFAULT_TWEET.id]: DEFAULT_TWEET,
};
/**
* Default injectable fetcher: resolves from an in-memory fixture table
* (falling back to a synthesized placeholder tweet for unknown ids) after a
* short simulated network delay. No real network request is made — callers
* that need live data should pass their own `fetchTweet`.
*/
const defaultFetchTweet: TweetFetcher = (tweetId) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(
MOCK_TWEET_FIXTURES[tweetId] ?? {
id: tweetId,
author: { name: "Unknown Author", handle: "unknown" },
text: "This is placeholder content for a tweet id with no bundled fixture.",
createdAt: Date.now(),
},
);
}, 400);
});
function formatTweetDate(createdAt: TweetData["createdAt"]): string {
const date = createdAt instanceof Date ? createdAt : new Date(createdAt);
if (Number.isNaN(date.getTime())) return "";
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
/** Caps a string at `length`, replacing the tail with an ellipsis — mirrors
* upstream's `truncate` used on the display name (20) and handle (16). */
function truncate(value: string, length: number): string {
if (value.length <= length) return value;
return `${value.slice(0, length - 3)}...`;
}
/** Visually-hidden ("sr-only") style, same recipe as this package's other
* sr-only usages (auroraText, kineticText). Carries the accessible "Link to
* tweet" label behind the decorative, aria-hidden platform icon. */
const SR_ONLY_STYLE: StyleObject = {
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
border: "0",
};
/** Body-entity (@mention / #hashtag / URL) styling. Upstream renders these as
* muted, de-emphasized text below the body copy (`text-muted-foreground`), no
* underline, brightening to the foreground color on hover (`hover:text-foreground
* transition-colors`). Two deliberate deviations for WCAG: these are interactive
* links (essential content), so they rest at the `text` tone (shift-9) — muted
* (shift-8) measures ~4.06:1, below AA for normal text — and they carry an
* underline, since inside a paragraph of full-strength text a color-only
* distinction fails axe `link-in-text-block`. */
const ENTITY_STYLE: StyleObject = {
color: (listener: Listener) => themeColor(listener, "shift-9", "neutral"),
textDecoration: fixed("underline"),
fontWeight: fixed(400),
transition: "color 150ms ease",
"&:hover": {
color: (listener: Listener) => themeColor(listener, "shift-10", "neutral"),
},
};
/** Small outline checkmark badge shown next to a verified author's name. */
function verifiedBadgeIcon(): DomphyElement<"span"> {
return {
span: [
{
svg: [
{ circle: null, cx: "12", cy: "12", r: "9" },
{ polyline: null, points: "8,12.5 11,15.5 16,9" },
],
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
role: "img",
ariaLabel: "Verified account",
style: { width: "100%", height: "100%" },
} as DomphyElement<"svg">,
],
$: [icon({ color: "info" })],
style: { width: themeSpacing(4), height: themeSpacing(4), flexShrink: "0" },
};
}
/** Small generic chat-bubble mark standing in for a "platform" logo — a
* deliberately original, generic glyph, not a reproduction of any specific
* platform's trademarked logo. */
function platformLogoIcon(): DomphyElement<"span"> {
return {
span: [
{
svg: [
{
path: null,
d: "M4 5h16a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9l-4 3v-3H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z",
},
],
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.75",
strokeLinecap: "round",
strokeLinejoin: "round",
role: "img",
ariaHidden: "true",
style: { width: "100%", height: "100%" },
} as DomphyElement<"svg">,
],
ariaHidden: "true",
$: [icon({ color: "neutral" })],
style: {
width: themeSpacing(4),
height: themeSpacing(4),
flexShrink: "0",
// Upstream: `hover:text-foreground hover:scale-105 transition-all`.
transition: "color 150ms ease, transform 150ms ease",
"&:hover": {
color: (listener: Listener) =>
themeColor(listener, "shift-10", "neutral"),
transform: "scale(1.05)",
},
},
};
}
function tweetHeader(data: TweetData): DomphyElement<"div"> {
const author = data.author;
const initials =
author.name
.split(/\s+/)
.map((word) => word[0])
.slice(0, 2)
.join("")
.toUpperCase() || "?";
const profileUrl = author.profileUrl;
// Avatar — a profile anchor when a profileUrl exists (upstream wraps the
// `<img>` in `<a href={tweet.user.url}>`), otherwise the bare avatar span.
const avatarSpan: DomphyElement<"span"> = {
span: author.avatarUrl
? [
{
img: null,
src: author.avatarUrl,
alt: author.name,
loading: "lazy" as const,
},
]
: initials,
$: [avatar({ color: "primary" })],
};
const avatarNode: DomphyElement = profileUrl
? {
a: [avatarSpan],
href: profileUrl,
target: "_blank",
rel: "noreferrer",
style: {
display: "inline-flex",
flexShrink: "0",
textDecoration: fixed("none"),
},
}
: avatarSpan;
// Name row (name + verified badge) — truncated to 20 chars like upstream.
const nameRowChildren: DomphyElement[] = [
{ strong: truncate(author.name, 20), $: [strong()] },
];
if (author.verified) nameRowChildren.push(verifiedBadgeIcon());
const nameNode: DomphyElement = profileUrl
? {
a: nameRowChildren,
href: profileUrl,
target: "_blank",
rel: "noreferrer",
style: {
display: "flex",
alignItems: "center",
gap: themeSpacing(1),
whiteSpace: "nowrap",
textDecoration: fixed("none"),
color: "inherit",
transition: "opacity 150ms ease",
"&:hover": { opacity: "0.8" },
},
}
: {
div: nameRowChildren,
style: { display: "flex", alignItems: "center", gap: themeSpacing(1) },
};
// Handle — truncated to 16 chars; a muted profile anchor (upstream
// `text-muted-foreground hover:text-foreground text-sm transition-colors`)
// or the plain `small` when no profileUrl.
const handleText = `@${truncate(author.handle, 16)}`;
const handleNode: DomphyElement = profileUrl
? {
a: handleText,
href: profileUrl,
target: "_blank",
rel: "noreferrer",
dataSize: "decrease-1",
style: {
color: (listener: Listener) =>
themeColor(listener, "shift-9", "neutral"),
textDecoration: fixed("none"),
transition: "color 150ms ease",
"&:hover": {
color: (listener: Listener) =>
themeColor(listener, "shift-10", "neutral"),
},
},
}
: { small: handleText, $: [small()] };
// Platform icon — a link to the tweet (upstream `<a href={tweet.url}>` with an
// sr-only "Link to tweet" label) when a url exists, otherwise the inert icon.
const platformIcon = platformLogoIcon();
const platformNode: DomphyElement = data.url
? {
a: [{ span: "Link to tweet", style: SR_ONLY_STYLE }, platformIcon],
href: data.url,
target: "_blank",
rel: "noreferrer",
style: {
display: "inline-flex",
alignItems: "flex-start",
flexShrink: "0",
marginInlineStart: "auto",
textDecoration: fixed("none"),
color: "inherit",
},
}
: {
...platformIcon,
style: { ...platformIcon.style, marginInlineStart: "auto" },
};
return {
div: [
avatarNode,
{
div: [nameNode, handleNode],
style: {
display: "flex",
flexDirection: "column",
minWidth: "0",
overflow: "hidden",
},
},
platformNode,
],
style: { display: "flex", alignItems: "flex-start", gap: themeSpacing(3) },
};
}
/** Splits the tweet body into plain text runs and clickable @mention / #hashtag / URL entities. */
function tweetTextBody(text: string): DomphyElement<"p"> {
const tokens = text.split(/(\s+)/);
const children: (string | DomphyElement<"a">)[] = tokens.map(
(token, index) => {
if (/^https?:\/\/\S+/.test(token)) {
return {
a: token,
href: token,
target: "_blank",
rel: "noopener noreferrer",
_key: `entity-${index}`,
style: ENTITY_STYLE,
};
}
if (/^[@#]\w+/.test(token)) {
return {
a: token,
href: "#",
_key: `entity-${index}`,
style: ENTITY_STYLE,
};
}
return token;
},
);
return { p: children as DomphyElement<"p">["p"], $: [paragraph()] };
}
function mediaGrid(media: TweetMedia[]): DomphyElement<"div"> {
const shown = media.slice(0, 4);
const columns = shown.length === 1 ? 1 : 2;
return {
div: shown.map((item, index) => ({
img: null,
src: item.url,
alt: item.alt ?? "",
loading: "lazy" as const,
_key: `media-${index}`,
style: {
width: "100%",
height: "100%",
aspectRatio: shown.length === 1 ? "16 / 9" : "1 / 1",
objectFit: "cover",
display: "block",
},
})),
style: {
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: themeSpacing(0.5),
borderRadius: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 3),
overflow: "hidden",
color: (listener: Listener) => themeColor(listener, "shift-9"),
outline: (listener: Listener) =>
`1px solid ${themeColor(listener, "shift-3")}`,
outlineOffset: "-1px",
},
};
}
function linkPreviewCard(preview: TweetLinkPreview): DomphyElement<"a"> {
let hostname = preview.url;
try {
hostname = new URL(preview.url).hostname;
} catch {
// Malformed preview URL — fall back to showing the raw string.
}
const detailChildren: DomphyElement[] = [
{ strong: preview.title, $: [strong()] },
];
if (preview.description)
detailChildren.push({ small: preview.description, $: [small()] });
detailChildren.push({ small: hostname, $: [small()] });
const cardChildren: DomphyElement[] = [];
if (preview.thumbnailUrl) {
cardChildren.push({
img: null,
src: preview.thumbnailUrl,
alt: "",
loading: "lazy",
style: {
width: "100%",
display: "block",
objectFit: "cover",
aspectRatio: "2 / 1",
},
});
}
cardChildren.push({
div: detailChildren,
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(1),
padding: (listener: Listener) => themeSpacing(themeDensity(listener) * 3),
},
});
return {
a: cardChildren,
href: preview.url,
target: "_blank",
rel: "noopener noreferrer",
style: {
display: "block",
textDecoration: () => "none",
borderRadius: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 3),
overflow: "hidden",
outline: (listener: Listener) =>
`1px solid ${themeColor(listener, "shift-3")}`,
outlineOffset: "-1px",
color: (listener: Listener) => themeColor(listener, "shift-9"),
backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
"&:hover": {
backgroundColor: (listener: Listener) =>
themeColor(listener, "increase-1"),
},
},
};
}
function tweetFooter(createdAt: TweetData["createdAt"]): DomphyElement<"div"> {
return {
div: [{ small: formatTweetDate(createdAt), $: [small()] }],
style: {
display: "flex",
color: (listener: Listener) => themeColor(listener, "shift-9"),
borderTop: (listener: Listener) =>
`1px solid ${themeColor(listener, "shift-3")}`,
paddingTop: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 2),
},
};
}
interface TweetBodyOptions {
showMedia: boolean;
showQuotedTweet: boolean;
/** Caps quoted-tweet nesting at one level, so a quote never renders its own quote. */
depth: number;
}
function tweetBody(
data: TweetData,
options: TweetBodyOptions,
): DomphyElement<"div"> {
const children: DomphyElement[] = [
tweetHeader(data),
tweetTextBody(data.text),
];
if (options.showMedia && data.media?.length)
children.push(mediaGrid(data.media));
if (data.linkPreview) children.push(linkPreviewCard(data.linkPreview));
if (options.showQuotedTweet && data.quotedTweet && options.depth < 1) {
children.push({
div: [
tweetBody(data.quotedTweet, {
showMedia: options.showMedia,
showQuotedTweet: false,
depth: options.depth + 1,
}),
],
dataTone: "shift-1",
style: {
borderRadius: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 3),
padding: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 3),
backgroundColor: (listener: Listener) =>
themeColor(listener, "inherit"),
color: (listener: Listener) => themeColor(listener, "shift-10"),
outline: (listener: Listener) =>
`1px solid ${themeColor(listener, "shift-3")}`,
outlineOffset: "-1px",
},
});
}
children.push(tweetFooter(data.createdAt));
return {
div: children,
style: {
display: "flex",
flexDirection: "column",
gap: (listener: Listener) => themeSpacing(themeDensity(listener) * 3),
},
};
}
function tweetSkeleton(): DomphyElement<"div"> {
return {
div: [
{
div: [
{
span: null,
$: [skeleton()],
style: {
width: themeSpacing(9),
height: themeSpacing(9),
borderRadius: "50%",
},
},
{
div: [
{ span: null, $: [skeleton()], style: { width: "40%" } },
{ span: null, $: [skeleton()], style: { width: "25%" } },
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(1),
flex: "1 1 auto",
},
},
],
style: { display: "flex", gap: themeSpacing(3), alignItems: "center" },
},
{ span: null, $: [skeleton()], style: { width: "100%" } },
{ span: null, $: [skeleton()], style: { width: "80%" } },
],
ariaLabel: "Loading tweet",
style: { display: "flex", flexDirection: "column", gap: themeSpacing(3) },
};
}
function tweetFallback(): DomphyElement<"div"> {
return {
div: [{ span: "🚫" }, { p: "This post is unavailable.", $: [paragraph()] }],
$: [empty()],
};
}
/**
* A card replicating a single social-post embed, with header/body/media/
* link-preview/quoted-post/footer regions, a loading skeleton, and a
* fallback state on fetch failure. Call with no arguments for a working
* demo tweet (rendered synchronously — no loading flicker). Pass `tweetId`
* (with an optional injectable `fetchTweet`) to exercise the async
* loading/error states instead.
*/
function tweetCard(props: TweetCardProps = {}): DomphyElement<"div"> {
const fetchTweet = props.fetchTweet ?? defaultFetchTweet;
const showMedia = props.showMedia ?? true;
const showQuotedTweet = props.showQuotedTweet ?? true;
const initialTweet = props.tweet ?? (props.tweetId ? null : DEFAULT_TWEET);
const phase = toState<TweetPhase>(initialTweet ? "loaded" : "loading");
const tweetState = toState<TweetData | null>(initialTweet);
return {
div: (listener: Listener) => {
const live = tweetCardState(listener, phase, tweetState);
const currentPhase = live.phase.get(listener);
// Each phase gets a distinct `_key` so the reconciler replaces the
// single child outright on a phase transition instead of positionally
// patching the old (structurally different) DOM subtree in place —
// without this, stale attributes/nodes from the previous phase can
// leak through (e.g. the skeleton's `aria-label` surviving onto the
// loaded body).
if (currentPhase === "loading")
return [{ ...tweetSkeleton(), _key: "skeleton" }];
const data = live.tweetState.get(listener);
if (currentPhase === "error" || !data)
return [{ ...tweetFallback(), _key: "error" }];
return [
{
...tweetBody(data, { showMedia, showQuotedTweet, depth: 0 }),
_key: `body-${data.id}`,
},
];
},
role: "article",
dataTheme: props.theme,
dataTone: "shift-0",
style: {
display: "block",
width: "100%",
maxWidth: themeSpacing(120),
borderRadius: (listenerValue: Listener) =>
themeSpacing(themeDensity(listenerValue) * 3),
padding: (listenerValue: Listener) =>
themeSpacing(themeDensity(listenerValue) * 4),
backgroundColor: (listenerValue: Listener) =>
themeColor(listenerValue, "inherit"),
color: (listenerValue: Listener) => themeColor(listenerValue, "shift-10"),
outline: (listenerValue: Listener) =>
`1px solid ${themeColor(listenerValue, "shift-3")}`,
outlineOffset: "-1px",
},
...behavior<TweetCardBehaviorProps>(
TWEET_CARD_BEHAVIOR_KEY,
attachTweetCard,
{
phase,
tweetState,
tweet: props.tweet,
tweetId: props.tweetId,
fetchTweet,
},
),
};
}
export { tweetCard, defaultFetchTweet };