Data

ReviewCard

One review: stars (halves too), title, text, reviewer and date, a Verified mark, images, the seller's reply and a Helpful vote.

#card#social#comment#commerce#no-js

Demo

Preview

Exactly as described

Verified purchase
Ola N.

Arrived in two days, well packed.\nWould buy again.

3 people found this helpful

Smaller than it looks

Jun K.

The photos make it look twice the size.

Was this helpful?
The shop replied · Aug 22, 2026

Sorry about that: the dimensions are now on the page. We have sent a return label.

Anonymous

Fine.

Great for a small team

Verified purchase
Marta R.

Set up in an afternoon. The only thing missing is a dark mode for the invoices.

12 people found this helpful

Not voted.

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/review_card.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/review_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/review_card.qwe
component ReviewCardDemoLive() {    state votes = 12    state voted = false    <div.demo-stack>        <ReviewCard id="rc-live" rating={4.5} title="Great for a small team" name="Marta R." date="2026-09-14" verified={true}                    text="Set up in an afternoon. The only thing missing is a dark mode for the invoices." helpful={votes} voted={voted}                    on_helpful={() => { votes = voted ? votes - 1 : votes + 1; voted = !voted }}/>        <p.demo-note>{voted ? "You found this helpful." : "Not voted."}</p>    </div>}view ReviewCardDemo() =    <div.demo-stack>        <form method="post" action="/reviews/helpful" id="review-card-demo-form">            <ReviewCard id="rc-1" rating={5} title="Exactly as described" name="Ola N." date="2026-10-02" verified={true} helpful={3}                        text="Arrived in two days, well packed.\nWould buy again."                        images={["https://picsum.photos/seed/box1/128", "https://picsum.photos/seed/box2/128"]}/>            <ReviewCard id="rc-2" rating={2} title="Smaller than it looks" name="Jun K." date="2026-08-21" helpful={0}                        text="The photos make it look twice the size."                        response={{ name: "The shop", date: "2026-08-22", text: "Sorry about that: the dimensions are now on the page. We have sent a return label." }}/>            <ReviewCard id="rc-3" rating={3.5} name="Anonymous" date="2026-07-30" text="Fine."/>        </form>        <ReviewCardDemoLive/>    </div>

Source

ui/review_card.qwe
// ReviewCard: one review of a product or a place: the stars, a title, the text, who// wrote it and when, a Verified mark, and how many found it helpful (with a button// to say so). ReviewList (review_list.qwe) shows many of them sorted and filtered.////   <ReviewCard id="r1" rating={4} title="Solid, a little heavy" text="..." name="Marta R."//               date="2026-09-14" verified={true} helpful={12}/>//   <ReviewCard {...r} voted={contains(mine, r.id)} on_helpful={() => { vote(r.id) }}/>//// id: the review's id (what Helpful sends). rating: 0 to 5, halves allowed, read to// screen readers as "4 out of 5 stars". title, text: what they wrote. name, src: who,// with their picture (initials without one). date: an ISO date, shown as "Sep 14,// 2026". verified: a "Verified purchase" mark. helpful: how many found it helpful,// with a Helpful button (left out, no button); voted: this reader did, which shows// the button pressed. images: a few image URLs they attached, as thumbnails.// response: { name, text, date? }, the seller's answer under it.// The Helpful button calls on_helpful() inside one of your components; otherwise it// is a submit button named `helpful` with the review's id, for a form around the// reviews. Needs avatar.qwe.import ui/avatar.qwe use Avatarimport ui/theme.qwe use ui_clslet review_card_css = css`.ui-review-card { display: grid; gap: 8px; min-width: 0; padding: 16px 0; border-bottom: 1px solid var(--ui-border); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); }.ui-review-card:first-child { padding-top: 0; }.ui-review-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }.ui-review-card-stars { display: inline-flex; gap: 1px; color: var(--ui-warning); }.ui-review-card-stars svg { width: 15px; height: 15px; }.ui-review-card-star-off { color: var(--ui-border); }.ui-review-card-star-half { clip-path: inset(0 50% 0 0); }.ui-review-card-title { margin: 0; font-size: 15px; font-weight: 600; }.ui-review-card-verified { display: inline-flex; align-items: center; gap: 3px; color: var(--ui-success); font-size: 12px; font-weight: 500; }.ui-review-card-verified svg { width: 13px; height: 13px; }.ui-review-card-who { display: flex; align-items: center; gap: 8px; color: var(--ui-muted); font-size: 13px; }.ui-review-card-name { color: var(--ui-fg); font-weight: 500; }.ui-review-card-text { margin: 0; white-space: pre-line; }.ui-review-card-images { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-review-card-images img { display: block; width: 64px; height: 64px; border-radius: var(--ui-radius-sm); object-fit: cover; }.ui-review-card-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; color: var(--ui-muted); font-size: 13px; }.ui-review-card-vote { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border: 1px solid var(--ui-border);  border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-review-card-vote:hover { border-color: var(--ui-strong); }.ui-review-card-vote:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-review-card-vote[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-review-card-vote svg { width: 14px; height: 14px; }.ui-review-card-reply { display: grid; gap: 4px; margin-top: 4px; padding: 10px 12px; border-left: 3px solid var(--ui-border); background: var(--ui-subtle);  border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0; }.ui-review-card-reply-who { font-size: 13px; color: var(--ui-muted); }.ui-review-card-reply-who b { color: var(--ui-fg); font-weight: 600; }.ui-review-card-reply p { margin: 0; }`let review_card_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date as "Sep 14, 2026"; anything else as it is.fn review_card_date(d) -> str {    if d == null { return "" }    let p = split(slice(str(d), 0, 10), "-")    len(p) == 3 && int(p[1]) != null && int(p[2]) != null ? `${review_card_months[int(p[1]) - 1]} ${int(p[2])}, ${p[0]}` : str(d)}// The stars of a rating out of 5: full, half and empty ones, as one picture.fn review_card_stars(rating: num, label: str = "") =    <span.ui-review-card-stars role="img" aria-label={`${label != "" ? label + ": " : ""}${rating} out of 5 stars`}>        {for i in range(0, 5) => {            let fill = rating >= i + 1 ? "full" : rating >= i + 0.5 ? "half" : "off"            <svg viewBox="0 0 24 24" class={fill == "off" ? "ui-review-card-star-off" : null} fill="currentColor" aria-hidden="true">                {fill == "half"                    ? [<path class="ui-review-card-star-off" d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>,                       <path.ui-review-card-star-half d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>]                    : <path d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>}            </svg>        }}    </span>view ReviewCard(id = null, rating: num = 0, title: str? = null, text: str = "", name: str = "Anonymous", src: str? = null,                date = null, verified: bool = false, helpful: int? = null, voted: bool = false, images: list = [],                response = null, on_helpful = null, class: str? = null) {    style global review_card_css    <article class={ui_cls(["ui-review-card", class])} id={id != null ? `review-${id}` : null}>        <div.ui-review-card-head>            {review_card_stars(rating)}            {title != null && <h4.ui-review-card-title>{title}</h4>}            {verified && <span.ui-review-card-verified>                <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>                Verified purchase            </span>}        </div>        <div.ui-review-card-who>            <Avatar name={name} src={src} size="xs"/>            <span.ui-review-card-name>{name}</span>            {date != null && <time datetime={str(date)}>{review_card_date(date)}</time>}        </div>        {text != "" && <p.ui-review-card-text>{text}</p>}        {len(images) > 0 && <ul.ui-review-card-images>{for u in images => <li key={u}><img src={u} alt="" loading="lazy"/></li>}</ul>}        {helpful != null && <div.ui-review-card-foot>            <span>{helpful == 0 ? "Was this helpful?" : `${helpful} ${helpful == 1 ? "person" : "people"} found this helpful`}</span>            {on_helpful != null                ? <button.ui-review-card-vote type="button" aria-pressed={voted ? "true" : "false"} @click={() => { on_helpful() }}>                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10h4Zm0 0 4-8a3 3 0 0 1 3 3v4h5a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 17.6 21H7"/></svg>                      Helpful                  </button>                : <button.ui-review-card-vote type="submit" name="helpful" value={str(id ?? "")} aria-pressed={voted ? "true" : "false"}>                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10h4Zm0 0 4-8a3 3 0 0 1 3 3v4h5a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 17.6 21H7"/></svg>                      Helpful                  </button>}        </div>}        {response != null && <div.ui-review-card-reply>            <div.ui-review-card-reply-who><b>{response.name}</b> replied{response.date != null && ` · ${review_card_date(response.date)}`}</div>            <p>{response.text}</p>        </div>}    </article>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.