Cards

Tweet Card

A social post card with avatar, handle, timestamp and like and repost counts that lifts on hover.

A social post card: avatar, display name, handle, optional timestamp, the post text, and like and repost counts. The whole card lifts and scales on hover, and it is driven entirely by props — there is nothing to wire up and no state to hold.

Every prop is optional, which means an empty card renders rather than throwing. If you want a post to read as a post, author, handle, content and avatar are the four that matter; the rest fill in.

Usage

Pass the post's fields as props. time only renders when it is a non-empty string, so an undated post simply has no timestamp rather than a blank one.

Devon Achebe
Devon Achebedevon2h

Spent the afternoon removing a backdrop-filter from a card. The layout shift I was chasing was never the blur — it was the repaint the blur forced on every scroll frame.

1842 316
<template>
  <div class="w-full max-w-md">
    <MTweetCard
      avatar="https://randomuser.me/api/portraits/men/11.jpg"
      author="Devon Achebe"
      handle="devon"
      time="2h"
      :likes="1842"
      :retweets="316"
      content="Spent the afternoon removing a backdrop-filter from a card. The layout shift I was chasing was never the blur — it was the repaint the blur forced on every scroll frame."
    />
  </div>
</template>

Customize

Devon Achebe
Devon Achebedevon2h

Spent the afternoon removing a backdrop-filter from a card. The layout shift I was chasing was never the blur — it was the repaint the blur forced on every scroll frame.

1842 316

Customize

AuthorDevon Achebe
Handledevon
ContentSpent the afternoon removing a backdrop-filter from a card. The layout shift I was chasing was never the blur — it was the repaint the blur forced on every scroll frame.
Avatarhttps://randomuser.me/api/portraits/men/11.jpg
Time2h
Likes1842
Retweets316

Props

Prop Type Default Description
authorstring''Display name, truncated to one line. Also the avatar's alt, which is what a screen reader reads for the person in the post.
handlestring''Secondary line next to the name, truncated to fit.
contentstring''The post body. Rendered as a paragraph with relaxed leading; it wraps rather than clamping, and line breaks in the string are preserved as ordinary whitespace.
avatarstring''Avatar source, rendered at md by UAvatar. With no source the avatar falls back to its initial or placeholder.
timestring''Timestamp, pushed to the end of the name row. The whole element is omitted when this is empty, so an undated post has no trailing gap.
likesnumber0Like count, rendered next to a heart icon. Rendered as written — a value over 1000 shows as 1000, not 1K.
retweetsnumber0Repost count, rendered next to a repeat icon. Formatted the same way as likes.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.tweetCard in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-will-change-transform
body-flex items-start gap-3
content-flex-1 min-w-0
header-flex items-center gap-2
author-font-semibold text-highlighted truncate
handle-text-sm text-muted truncate
time-text-xs text-muted ml-auto whitespace-nowrap
text-mt-2 text-sm leading-relaxed
meta-flex items-center gap-4 mt-3 text-sm text-muted
stat-inline-flex items-center gap-1
statIcon-w-4 h-4

Accessibility

The component adds no roles, no ARIA attributes and no keyboard behaviour. The counts are not interactive: the heart and repeat icons are decorative glyphs beside plain numbers, and there is no button behind either one. If likes should be tappable, the component does not offer a way in — build that row yourself.

The avatar is a UAvatar whose alt is the display name, so the author is announced once. The name and handle are truncated with CSS, which means a long name is present in the DOM and reachable by assistive technology even though it is visually cut off. The timestamp is text, not a <time> element, so it carries no machine-readable date — put one in if it matters.

The hover lift is pointer-only and there is no prefers-reduced-motion handling. It is a scale to 1.02 with a y: -4 lift on a spring, which is mild but not gated; if you need the guarantee, the component gives you no prop for it, so style around it or replace the root.

Performance

  • The lift is a single whileHover on the root motion.div — one transform on one element, with no per-frame Vue state. Nothing re-renders while hovering.
  • will-change-transform is set on the root, so the browser promotes it before the first hover rather than on the first frame of one.
  • The card has no slots, no reactive state, no timers, no listeners and no observers. It renders one UAvatar, one icon-bearing row per count and a paragraph.
  • The spring is fixed at stiffness: 300, damping: 20 with no duration, so it settles on physics rather than a clock — there is nothing to cancel or clean up on unmount.
  • The counts are interpolated into the template as text, so changing likes is a text patch on an existing node rather than a component re-render.
Copyright © 2026