Data

ReviewList

Reviews sorted by a select (newest, highest, most helpful), filtered by stars with counts, shown a page at a time.

#list#sort#filter#commerce#social#interactive

Demo

Preview

7 reviews

Exactly as described

Verified purchase
Ola N.

Arrived in two days, well packed.

3 people found this helpful

Good value

Verified purchase
Marta R.

Does the job. The lid is a bit loose.

12 people found this helpful

Smaller than it looks

Jun K.

The photos make it look twice the size.

9 people found this helpful

Second one I bought

Sam T.

Gave the first to my sister.

1 person found this helpful

3 reviews

Exactly as described

Verified purchase
Ola N.

Arrived in two days, well packed.

3 people found this helpful

Good value

Verified purchase
Marta R.

Does the job. The lid is a bit loose.

12 people found this helpful

Smaller than it looks

Jun K.

The photos make it look twice the size.

9 people found this helpful

No votes yet.

Get it

It needs theme.qwe, as every component does, and avatar.qwe and review_card.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$ curl -O https://quillolang.org/components/ui/review_list.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/review_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/review_list.qwe
let review_list_demo_reviews = [    { id: "a1", rating: 5, title: "Exactly as described", name: "Ola N.", date: "2026-10-02", verified: true, helpful: 3, text: "Arrived in two days, well packed." },    { id: "a2", rating: 2, title: "Smaller than it looks", name: "Jun K.", date: "2026-08-21", helpful: 9, text: "The photos make it look twice the size." },    { id: "a3", rating: 4, title: "Good value", name: "Marta R.", date: "2026-09-14", verified: true, helpful: 12, text: "Does the job. The lid is a bit loose." },    { id: "a4", rating: 5, title: "Second one I bought", name: "Sam T.", date: "2026-07-01", helpful: 1, text: "Gave the first to my sister." },    { id: "a5", rating: 3, name: "Anonymous", date: "2026-06-11", helpful: 0, text: "Fine." },    { id: "a6", rating: 4, title: "Quiet", name: "Lea B.", date: "2026-05-30", verified: true, helpful: 5, text: "Quieter than the old one." },    { id: "a7", rating: 1, title: "Broke in a week", name: "Ivo P.", date: "2026-04-18", helpful: 20, text: "The hinge snapped. Support replaced it, to be fair." },]component ReviewListDemoLive() {    state mine = []    state log = "No votes yet."    <div.demo-stack>        <ReviewList id="rl-live" reviews={take(review_list_demo_reviews, 3)} voted={mine}                    on_helpful={(rid) => {                        mine = contains(mine, rid) ? mine | filter((x) => x != rid) : push(mine, rid)                        log = `Voted: ${len(mine) == 0 ? "none" : join(mine, ", ")}.`                    }}/>        <p.demo-note>{log}</p>    </div>}view ReviewListDemo() =    <div.demo-stack>        <form method="post" action="/reviews/helpful">            <ReviewList id="rl-all" reviews={review_list_demo_reviews} limit={4}/>        </form>        <ReviewListDemoLive/>    </div>

Source

ui/review_list.qwe
// ReviewList: reviews of one thing, sorted by a select (newest, oldest, highest,// lowest, most helpful) and filtered by stars with a row of buttons that count// them, shown a page at a time with a "Show more" button.////   <ReviewList id="reviews" reviews={reviews} limit={5}/>//   <ReviewList reviews={reviews} voted={mine} on_helpful={(id) => { mine = push(mine, id) }}/>//// reviews: a list of maps as ReviewCard takes them ({ id, rating, title, text, name,// src?, date, verified?, helpful?, images?, response? }); each is drawn by ReviewCard// (review_card.qwe, which this file needs, with avatar.qwe).// sort: "newest" (default), "oldest", "highest", "lowest" or "helpful" to start with.// stars: 1 to 5 to start filtered to that many; null shows all. limit: how many to// show at first, the rest behind "Show N more" (0, the default, shows every one).// voted: the ids this reader found helpful (their buttons show pressed).// on_helpful(id): inside one of your components, called with the review's id when// Helpful is clicked; without it the button submits `helpful` = id to a form// around the list. label: the list's name for screen readers ("Reviews").// empty: what it says with no reviews to show. id: a prefix for its ids; each review's card is `review-<id>-<review id>`.import ui/review_card.qwe use ReviewCardimport ui/theme.qwe use ui_clslet review_list_css = css`.ui-review-list { display: grid; gap: 12px; min-width: 0; font: 14px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-review-list-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }.ui-review-list-filter { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; padding: 0; list-style: none; }.ui-review-list-star { display: inline-flex; align-items: center; gap: 4px; 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-list-star:hover { border-color: var(--ui-strong); }.ui-review-list-star:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-review-list-star[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-review-list-star:disabled { opacity: .5; cursor: not-allowed; }.ui-review-list-star svg { width: 12px; height: 12px; color: var(--ui-warning); }.ui-review-list-star small { color: var(--ui-muted); font-weight: 400; }.ui-review-list-star[aria-pressed="true"] small { color: inherit; }.ui-review-list-sort { display: inline-flex; align-items: center; gap: 8px; color: var(--ui-muted); font-size: 13px; }.ui-review-list-sort select {  height: 32px; padding: 0 28px 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface) no-repeat right 9px center / 10px  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6' fill='none' stroke='%23888' stroke-width='1.5'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");  color: var(--ui-fg); font: 13px/1 var(--ui-font); appearance: none;}.ui-review-list-sort select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-review-list-count { color: var(--ui-muted); font-size: 13px; }.ui-review-list-items { display: grid; }.ui-review-list-items > .ui-review-list-item:last-child { border-bottom: 0; }.ui-review-list-empty { padding: 24px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm); color: var(--ui-muted); text-align: center; }.ui-review-list-more { justify-self: center; height: 34px; padding: 0 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface); color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-review-list-more:hover { border-color: var(--ui-strong); }.ui-review-list-more:focus-visible { outline: none; box-shadow: var(--ui-focus); }`// The reviews in an order: newest, oldest, highest, lowest or helpful.fn review_list_sorted(reviews: list, by: str) -> list =    match by {        "oldest" => sort_by(reviews, (r) => str(r.date ?? ""))        "highest" => reverse(sort_by(reviews, (r) => r.rating ?? 0))        "lowest" => sort_by(reviews, (r) => r.rating ?? 0)        "helpful" => reverse(sort_by(reviews, (r) => r.helpful ?? 0))        _ => reverse(sort_by(reviews, (r) => str(r.date ?? "")))    }component ReviewList(reviews: list = [], sort: str = "newest", stars: int? = null, limit: int = 0, voted: list = [],                     on_helpful = null, label: str = "Reviews", empty: str = "No reviews yet.", id: str = "reviews",                     class: str? = null) {    style global review_list_css    state by = sort    state only = stars    state shown = limit    let counts = for s in [5, 4, 3, 2, 1] => { stars: s, n: count(reviews, (r) => floor(r.rating ?? 0) == s) }    let kept = review_list_sorted(only == null ? reviews : reviews | filter((r) => floor(r.rating ?? 0) == only), by)    let page = shown > 0 && len(kept) > shown ? take(kept, shown) : kept    let left = len(kept) - len(page)    <section class={ui_cls(["ui-review-list", class])} id={id} aria-label={label}>        <div.ui-review-list-bar>            <ul.ui-review-list-filter aria-label="Filter by stars">                <li><button.ui-review-list-star type="button" aria-pressed={only == null ? "true" : "false"} @click={() => { only = null }}>                    All <small>{len(reviews)}</small></button></li>                {for c in counts => <li key={c.stars}>                    <button.ui-review-list-star type="button" aria-pressed={only == c.stars ? "true" : "false"} disabled={c.n == 0}                            aria-label={`${c.stars} stars, ${c.n}`} @click={() => { only = only == c.stars ? null : c.stars }}>                        {c.stars}<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><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>                        <small>{c.n}</small>                    </button>                </li>}            </ul>            <label.ui-review-list-sort>Sort by                <select name="sort" @change={(e) => { by = e.value }}>                    <option value="newest" selected={by == "newest"}>Newest</option>                    <option value="oldest" selected={by == "oldest"}>Oldest</option>                    <option value="highest" selected={by == "highest"}>Highest rated</option>                    <option value="lowest" selected={by == "lowest"}>Lowest rated</option>                    <option value="helpful" selected={by == "helpful"}>Most helpful</option>                </select>            </label>        </div>        <p.ui-review-list-count role="status">            {only == null                ? `${len(reviews)} ${len(reviews) == 1 ? "review" : "reviews"}`                : `${len(kept)} of ${len(reviews)} reviews with ${only} ${only == 1 ? "star" : "stars"}`}        </p>        {len(kept) == 0            ? <div.ui-review-list-empty>{empty}</div>            : <div.ui-review-list-items>                  {for r in page => <ReviewCard key={r.id} {...r} id={`${id}-${r.id}`} voted={contains(voted, r.id)} class="ui-review-list-item"                                                on_helpful={on_helpful != null ? () => { on_helpful(r.id) } : null}/>}              </div>}        {left > 0 && <button.ui-review-list-more type="button" @click={() => { shown = shown + limit }}>            Show {left > limit ? limit : left} more        </button>}    </section>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.