Installation
npm install react-tweet
Installation React Server Component (Next.js 13+):
Copy and paste the following code into your project.
import { cn } from "@/lib/utils";
import { Suspense } from "react";
import {
enrichTweet,
type EnrichedTweet,
type TweetProps,
type TwitterComponents,
} from "react-tweet";
import { getTweet, type Tweet } from "react-tweet/api";
interface TwitterIconProps {
className?: string;
[key: string]: any;
}
const Twitter = ({ className, ...props }: TwitterIconProps) => (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...props}
>
<g>
<path fill="none" d="M0 0h24v24H0z"></path>
<path d="M22.162 5.656a8.384 8.384 0 0 1-2.402.658A4.196 4.196 0 0 0 21.6 4c-.82.488-1.719.83-2.656 1.015a4.182 4.182 0 0 0-7.126 3.814 11.874 11.874 0 0 1-8.62-4.37 4.168 4.168 0 0 0-.566 2.103c0 1.45.738 2.731 1.86 3.481a4.168 4.168 0 0 1-1.894-.523v.052a4.185 4.185 0 0 0 3.355 4.101 4.21 4.21 0 0 1-1.89.072A4.185 4.185 0 0 0 7.97 16.65a8.394 8.394 0 0 1-6.191 1.732 11.83 11.83 0 0 0 6.41 1.88c7.693 0 11.9-6.373 11.9-11.9 0-.18-.005-.362-.013-.54a8.496 8.496 0 0 0 2.087-2.165z"></path>
</g>
</svg>
);
const Verified = ({ className, ...props }: TwitterIconProps) => (
<svg
aria-label="Verified Account"
viewBox="0 0 24 24"
className={className}
{...props}
>
<g fill="currentColor">
<path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z" />
</g>
</svg>
);
export const truncate = (str: string | null, length: number) => {
if (!str || str.length <= length) return str;
return `${str.slice(0, length - 3)}...`;
};
const Skeleton = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
);
};
export const TweetSkeleton = ({
className,
...props
}: {
className?: string;
[key: string]: any;
}) => (
<div
className={cn(
"flex h-full max-h-max w-full min-w-[18rem] flex-col gap-2 rounded-lg border p-4",
className,
)}
{...props}
>
<div className="flex flex-row gap-2">
<Skeleton className="h-10 w-10 shrink-0 rounded-full" />
<Skeleton className="h-10 w-full" />
</div>
<Skeleton className="h-20 w-full" />
</div>
);
export const TweetNotFound = ({
className,
...props
}: {
className?: string;
[key: string]: any;
}) => (
<div
className={cn(
"flex h-full w-full flex-col items-center justify-center gap-2 rounded-lg border p-4",
className,
)}
{...props}
>
<h3>Tweet not found</h3>
</div>
);
export const TweetHeader = ({ tweet }: { tweet: EnrichedTweet }) => (
<div className="flex flex-row justify-between tracking-tight">
<div className="flex items-center space-x-2">
<a href={tweet.user.url} target="_blank" rel="noreferrer">
<img
title={`Profile picture of ${tweet.user.name}`}
alt={tweet.user.screen_name}
height={48}
width={48}
src={tweet.user.profile_image_url_https}
className="overflow-hidden rounded-full border border-transparent"
/>
</a>
<div>
<a
href={tweet.user.url}
target="_blank"
rel="noreferrer"
className="flex items-center whitespace-nowrap font-semibold"
>
{truncate(tweet.user.name, 20)}
{tweet.user.verified ||
(tweet.user.is_blue_verified && (
<Verified className="ml-1 inline h-4 w-4 text-blue-500" />
))}
</a>
<div className="flex items-center space-x-1">
<a
href={tweet.user.url}
target="_blank"
rel="noreferrer"
className="text-sm text-gray-500 transition-all duration-75"
>
@{truncate(tweet.user.screen_name, 16)}
</a>
</div>
</div>
</div>
<a href={tweet.url} target="_blank" rel="noreferrer">
<span className="sr-only">Link to tweet</span>
<Twitter className="h-5 w-5 items-start text-[#3BA9EE] transition-all ease-in-out hover:scale-105" />
</a>
</div>
);
export const TweetBody = ({ tweet }: { tweet: EnrichedTweet }) => (
<div className="break-words leading-normal tracking-tighter">
{tweet.entities.map((entity, idx) => {
switch (entity.type) {
case "url":
case "symbol":
case "hashtag":
case "mention":
return (
<a
key={idx}
href={entity.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-normal text-gray-500"
>
<span>{entity.text}</span>
</a>
);
case "text":
return (
<span
key={idx}
className="text-sm font-normal"
dangerouslySetInnerHTML={{ __html: entity.text }}
/>
);
}
})}
</div>
);
export const TweetMedia = ({ tweet }: { tweet: EnrichedTweet }) => (
<div className="flex flex-1 items-center justify-center">
{tweet.video && (
<video
poster={tweet.video.poster}
autoPlay
loop
muted
playsInline
className="rounded-xl border shadow-sm"
>
<source src={tweet.video.variants[0].src} type="video/mp4" />
Your browser does not support the video tag.
</video>
)}
{tweet.photos && (
<div className="relative flex transform-gpu snap-x snap-mandatory gap-4 overflow-x-auto">
<div className="shrink-0 snap-center sm:w-2" />
{tweet.photos.map((photo) => (
<img
key={photo.url}
src={photo.url}
title={"Photo by " + tweet.user.name}
alt={tweet.text}
className="h-64 w-5/6 shrink-0 snap-center snap-always rounded-xl border object-cover shadow-sm"
/>
))}
<div className="shrink-0 snap-center sm:w-2" />
</div>
)}
{!tweet.video &&
!tweet.photos &&
// @ts-ignore
tweet?.card?.binding_values?.thumbnail_image_large?.image_value.url && (
<img
// @ts-ignore
src={tweet.card.binding_values.thumbnail_image_large.image_value.url}
className="h-64 rounded-xl border object-cover shadow-sm"
/>
)}
</div>
);
export const MagicTweet = ({
tweet,
components,
className,
...props
}: {
tweet: Tweet;
components?: TwitterComponents;
className?: string;
}) => {
const enrichedTweet = enrichTweet(tweet);
return (
<div
className={cn(
"relative flex h-full w-full max-w-[32rem] flex-col gap-2 overflow-hidden rounded-lg border p-4 backdrop-blur-md",
className,
)}
{...props}
>
<TweetHeader tweet={enrichedTweet} />
<TweetBody tweet={enrichedTweet} />
<TweetMedia tweet={enrichedTweet} />
</div>
);
};
/**
* TweetCard (Server Side Only)
*/
export const TweetCard = async ({
id,
components,
fallback = <TweetSkeleton />,
onError,
...props
}: TweetProps & {
className?: string;
}) => {
const tweet = id
? await getTweet(id).catch((err) => {
if (onError) {
onError(err);
} else {
console.error(err);
}
})
: undefined;
if (!tweet) {
const NotFound = components?.TweetNotFound || TweetNotFound;
return <NotFound {...props} />;
}
return (
<Suspense fallback={fallback}>
<MagicTweet tweet={tweet} {...props} />
</Suspense>
);
};
export default TweetCard;
Installation Client Side
Copy and paste the following code into your project.
"use client";
import {
MagicTweet,
TweetNotFound,
TweetSkeleton,
} from "@/components/magicui/tweet-card";
import { TweetProps, useTweet } from "react-tweet";
const ClientTweetCard = ({
id,
apiUrl,
fallback = <TweetSkeleton />,
components,
fetchOptions,
onError,
...props
}: TweetProps & { className?: string }) => {
const { data, error, isLoading } = useTweet(id, apiUrl, fetchOptions);
if (isLoading) return fallback;
if (error || !data) {
const NotFound = components?.TweetNotFound || TweetNotFound;
return <NotFound error={onError ? onError(error) : error} />;
}
return <MagicTweet tweet={data} components={components} {...props} />;
};
export default ClientTweetCard;
Usage
To render on server side using RSC (Next.js 13):
import { TweetCard } from "@/components/magicui/tweet-card.tsx";
export default async function App() {
return <TweetCard id="1441032681968212480" />;
}
To render on client side:
"use client";
import { ClientTweetCard } from "@/components/magicui/client-tweet-card.tsx";
export default function App() {
return <ClientTweetCard id="1441032681968212480" />;
}
Examples
Tweet Card With Image Carousel
Tweet Card With Meta URL Preview
Props
ClientTweetCard
Prop | Type | Description |
---|---|---|
id | id | The id of the tweet to display. |
TweetCard
Prop | Type | Description |
---|---|---|
id | id | The id of the tweet to display. |
Credits
This component is built on top of React Tweet.