Data

FeedItem

A social-feed item: avatar, name, handle, time, text, a picture or any media, and like, comment and share buttons with counts.

#social#card#user#comment#no-js

Demo

Preview
Ada Lovelace, verified@ada

The first program is written.\nThe machine is not even built yet.

Grace Hopper@grace

A nanosecond is 30 cm of wire. Keep one on your desk.

Alan Turing

Can machines think?

A quoted post, as children.

Nothing yet.

Get it

It needs theme.qwe, as every component does, and avatar.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/avatar.qwe$ curl -O https://quillolang.org/components/ui/feed_item.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/feed_item.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/feed_item.qwe
component FeedItemDemoLive() {    state liked = false    state likes = 128    state last = "Nothing yet."    <div.demo-stack>        <div style="border: 1px solid var(--ui-border); border-radius: var(--ui-radius); overflow: hidden">        <FeedItem id="feed-item-demo-live" name="Ada Lovelace" handle="@ada" time="2h" datetime="2026-10-28T09:00:00Z" href="#feed_item" verified={true}                  text="The first program is written.\nThe machine is not even built yet." media="https://picsum.photos/seed/engine/800/400"                  likes={likes} comments={12} shares={4} liked={liked}                  on_like={() => { liked = !liked; likes = liked ? likes + 1 : likes - 1; last = liked ? "Liked." : "Unliked." }}                  on_comment={() => { last = "Comment box opened." }}                  on_share={() => { last = "Share sheet opened." }}/>            <FeedItem id="feed-item-demo-plain" name="Grace Hopper" handle="@grace" src="https://i.pravatar.cc/80?img=47" time="Yesterday"                      text="A nanosecond is 30 cm of wire. Keep one on your desk." likes={64} comments={3}/>            <FeedItem name="Alan Turing" time="Oct 20" text="Can machines think?">                <div style="padding: 12px 14px; font-size: 14px; color: var(--ui-muted)">A quoted post, as children.</div>            </FeedItem>        </div>        <p.demo-note>{last}</p>    </div>}view FeedItemDemo() =    <div.demo-stack>        <FeedItemDemoLive/>    </div>

Source

ui/feed_item.qwe
// FeedItem: one item of a social feed: who posted (picture, name, handle), when,// what they wrote, a picture or any media, and the like, comment and share counts// as buttons.////   <FeedItem name="Ada Lovelace" handle="@ada" time="2h" datetime="2026-10-28T09:00:00Z"//             text="The first program, and the machine is not even built yet." media="/img/engine.jpg"//             likes={128} comments={12} shares={4}/>//   <FeedItem {...post} liked={post.liked} on_like={() => { like(post.id) }}/>//// name, handle, src: who, with their picture (initials without one). time: when,// as it should read ("2h"); datetime: its ISO form; href: a link to the item on// the time. text: what they wrote (line breaks kept). media: an image URL, or// children for anything else (a video, a quoted post). likes, comments, shares:// the counts; a count left out hides its button. liked: this reader's like, the// heart filled. on_like(), on_comment(), on_share(): inside one of your components,// what each button does. verified: a tick after the name. id: the item's id.// Needs avatar.qwe.import ui/avatar.qwe use Avatarimport ui/theme.qwe use ui_clslet feed_item_css = css`.ui-feed-item { display: grid; grid-template-columns: auto 1fr; gap: 12px; min-width: 0; padding: 14px 16px; border-bottom: 1px solid var(--ui-border);  background: var(--ui-surface); color: var(--ui-fg); font: 15px/1.45 var(--ui-font); }.ui-feed-item-body { display: grid; gap: 6px; min-width: 0; }.ui-feed-item-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; font-size: 14px; }.ui-feed-item-name { font-weight: 650; }.ui-feed-item-tick { width: 15px; height: 15px; color: var(--ui-info); }.ui-feed-item-handle, .ui-feed-item-head time, .ui-feed-item-head a { color: var(--ui-muted); text-decoration: none; }.ui-feed-item-head a:hover { text-decoration: underline; }.ui-feed-item-head a:focus-visible { outline: none; border-radius: 4px; box-shadow: var(--ui-focus); }.ui-feed-item-head time::before { content: "·"; margin-right: 6px; }.ui-feed-item-text { margin: 0; white-space: pre-line; overflow-wrap: anywhere; }.ui-feed-item-media { margin-top: 4px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); overflow: hidden; }.ui-feed-item-slot:empty { display: none; }.ui-feed-item-media img { display: block; width: 100%; max-height: 420px; object-fit: cover; }.ui-feed-item-acts { display: flex; gap: 4px; margin: 4px 0 0 -8px; }.ui-feed-item-act { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border: 0; border-radius: 999px;  background: transparent; color: var(--ui-muted); font: 500 13px/1 var(--ui-font); font-variant-numeric: tabular-nums; cursor: pointer; }.ui-feed-item-act:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-feed-item-act:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-feed-item-act svg { width: 17px; height: 17px; }.ui-feed-item-like:hover { color: var(--ui-danger); background: var(--ui-danger-soft); }.ui-feed-item-like[aria-pressed="true"] { color: var(--ui-danger); }.ui-feed-item-like[aria-pressed="true"] svg { fill: currentColor; }.ui-feed-item-comment:hover { color: var(--ui-info); background: var(--ui-info-soft); }.ui-feed-item-share:hover { color: var(--ui-success); background: var(--ui-success-soft); }`view FeedItem(name: str, handle: str? = null, src: str? = null, time: str? = null, datetime: str? = null, href: str? = null,              text: str = "", media: str? = null, likes: int? = null, comments: int? = null, shares: int? = null,              liked: bool = false, verified: bool = false, on_like = null, on_comment = null, on_share = null,              id: str? = null, class: str? = null, children) {    style global feed_item_css    let when = time != null ? <time datetime={datetime}>{time}</time> : null    <article class={ui_cls(["ui-feed-item", class])} id={id}>        <Avatar name={name} src={src}/>        <div.ui-feed-item-body>            <div.ui-feed-item-head>                <span.ui-feed-item-name>{name}</span>                {verified && <svg.ui-feed-item-tick viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1.5l2.6 2.2 3.4-.4 1 3.3 3 1.7-1.3 3.2 1.3 3.2-3 1.7-1 3.3-3.4-.4L12 22.5l-2.6-2.2-3.4.4-1-3.3-3-1.7 1.3-3.2L2 9.3l3-1.7 1-3.3 3.4.4Zm-1.4 13.2 5.6-5.6-1.4-1.4-4.2 4.2-2.1-2.1-1.4 1.4Z"/></svg>}                {verified && <span.ui-sr>, verified</span>}                {handle != null && <span.ui-feed-item-handle>{handle}</span>}                {when != null && (href != null ? <a href={href}>{when}</a> : when)}            </div>            {text != "" && <p.ui-feed-item-text>{text}</p>}            {media != null && <div.ui-feed-item-media><img src={media} alt="" loading="lazy"/></div>}            <div.ui-feed-item-media.ui-feed-item-slot>{children}</div>            {(likes != null || comments != null || shares != null) && <div.ui-feed-item-acts>                {likes != null && <button.ui-feed-item-act.ui-feed-item-like type="button" aria-pressed={liked ? "true" : "false"}                        aria-label={`Like, ${likes}`} @click={() => { on_like && on_like() }}>                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20.5s-8-4.8-8-11A4.5 4.5 0 0 1 12 7a4.5 4.5 0 0 1 8 2.5c0 6.2-8 11-8 11Z"/></svg>                    <span aria-hidden="true">{likes}</span>                </button>}                {comments != null && <button.ui-feed-item-act.ui-feed-item-comment type="button" aria-label={`Comments, ${comments}`}                        @click={() => { on_comment && on_comment() }}>                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.5-4.6A8 8 0 1 1 21 12Z"/></svg>                    <span aria-hidden="true">{comments}</span>                </button>}                {shares != null && <button.ui-feed-item-act.ui-feed-item-share type="button" aria-label={`Share, ${shares}`}                        @click={() => { on_share && on_share() }}>                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3l4 4-4 4M21 7H10a5 5 0 0 0-5 5v1M7 21l-4-4 4-4M3 17h11a5 5 0 0 0 5-5v-1"/></svg>                    <span aria-hidden="true">{shares}</span>                </button>}            </div>}        </div>    </article>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On FeedItem: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.