Reactions
Emoji reaction chips with counts, yours marked, and an add button that opens a row of emoji to pick from.
Demo
Press a chip, or add one.
Get it
It needs theme.qwe, as every component does; 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/reactions.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/reactions.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/reactions.qwe
component ReactionsDemoLive() { state last = null <div.demo-stack> <Reactions id="reactions-demo-live" name="reactions" reactions={[{ emoji: "👍", count: 12, mine: true, label: "thumbs up" }, { emoji: "❤️", count: 3, label: "heart" }, { emoji: "🎉", count: 1, label: "party" }]} on_change={(emoji, on) => { last = `${emoji} ${on ? "on" : "off"}` }}/> <p.demo-note>{last == null ? "Press a chip, or add one." : `Last: ${last}.`}</p> </div>}view ReactionsDemo() = <div.demo-stack> <ReactionsDemoLive/> <div.demo-row> <Reactions id="reactions-demo-sm" size="sm" reactions={[{ emoji: "😂", count: 5 }, { emoji: "😮", count: 2 }]}/> <Reactions id="reactions-demo-empty" reactions={[]} choices={["🚀", "👀", "💯"]} add_label="React"/> </div> </div>Source
ui/reactions.qwe
// Reactions: the reactions under a message or a post, a chip each with its emoji and// count, and a button that opens a row of emoji to add one. Pressing a chip adds your// reaction, or takes it back if it was yours. The emoji you reacted with go in a hidden// input named `name`, so a plain <form> sends them.//// <Reactions reactions={[{ emoji: "👍", count: 12, mine: true }, { emoji: "❤️", count: 3 }]}/>// <Reactions reactions={post.reactions} on_change={(emoji, on) => { react(post.id, emoji, on) }}/>//// reactions: a list of { emoji, count, mine? (true when one of them is yours), label? (what// screen readers call it, "thumbs up") }. choices: the emoji the add button offers// (["👍", "❤️", "😂", "🎉", "😮", "😢"]). name: what a <form> gets your emoji as,// comma-separated. label: what screen readers call the group ("Reactions"). size: "sm" or// "md". add_label: the add button's name ("Add reaction"). id: a prefix for the ids.// on_change(emoji, on): inside one of your components, the emoji and whether you now// react with it (true) or took it back (false).import ui/theme.qwe use ui_clslet reactions_css = css`.ui-reactions { position: relative; display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; font: 13px/1 var(--ui-font); color: var(--ui-fg); }.ui-reactions-chip { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 9px 0 7px; 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; transition: border-color .15s, background .15s;}.ui-reactions-chip:hover { border-color: var(--ui-strong); }.ui-reactions-chip:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reactions-chip[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-reactions-emoji { font-size: 15px; line-height: 1; }.ui-reactions-count { font-variant-numeric: tabular-nums; }.ui-reactions-add { display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px dashed var(--ui-strong); border-radius: 999px; background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-reactions-add svg { width: 15px; height: 15px; }.ui-reactions-add:hover, .ui-reactions-add[aria-expanded="true"] { border-style: solid; border-color: var(--ui-accent); color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-reactions-add:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reactions-menu { position: absolute; z-index: 20; top: 100%; left: 0; display: flex; gap: 2px; margin-top: 6px; padding: 4px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); box-shadow: var(--ui-shadow);}.ui-reactions-menu[hidden] { display: none; }.ui-reactions-pick { display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 50%; background: transparent; font-size: 18px; line-height: 1; cursor: pointer; transition: transform .1s;}.ui-reactions-pick:hover { background: var(--ui-subtle); transform: scale(1.2); }.ui-reactions-pick:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reactions-pick[aria-pressed="true"] { background: var(--ui-accent-soft); }.ui-reactions-sm .ui-reactions-chip { height: 24px; padding: 0 7px 0 6px; font-size: 12px; }.ui-reactions-sm .ui-reactions-emoji { font-size: 13px; }.ui-reactions-sm .ui-reactions-add { width: 24px; height: 24px; }@media (prefers-reduced-motion: reduce) { .ui-reactions-chip, .ui-reactions-pick { transition: none; } .ui-reactions-pick:hover { transform: none; } }`component Reactions(reactions: list = [], choices: list = ["👍", "❤️", "😂", "🎉", "😮", "😢"], name: str? = null, label: str = "Reactions", add_label: str = "Add reaction", size: str = "md", id: str = "ui-reactions", on_change = null, class: str? = null) { style global reactions_css state items = reactions state open = false let mine = items | filter((r) => r.mine == true) | map((r) => r.emoji) let toggle = (emoji: str) => { let have = find(items, (r) => r.emoji == emoji) let on = have == null || have.mine != true items = have == null ? items | push({ emoji, count: 1, mine: true }) : items | map((r) => r.emoji == emoji ? { ...r, mine: on, count: r.count + (on ? 1 : -1) } : r) | filter((r) => r.count > 0) open = false on_change && on_change(emoji, on) } <div class={ui_cls(["ui-reactions", size == "sm" && "ui-reactions-sm", class])} role="group" aria-label={label} @keydown={(e) => { if e.key == "Escape" && open { e.preventDefault(); open = false } }} @focusout={(e) => { if open && (e.relatedTarget == null || !e.currentTarget.contains(e.relatedTarget)) { open = false } }}> {for r in items => <button.ui-reactions-chip key={r.emoji} type="button" aria-pressed={r.mine == true ? "true" : "false"} aria-label={`${r.label ?? r.emoji}, ${r.count}${r.mine == true ? ", you reacted" : ""}`} @click={() => { toggle(r.emoji) }}> <span.ui-reactions-emoji aria-hidden="true">{r.emoji}</span> <span.ui-reactions-count aria-hidden="true">{r.count}</span> </button>} <button.ui-reactions-add type="button" id={`${id}-add`} aria-label={add_label} aria-haspopup="true" aria-expanded={open ? "true" : "false"} aria-controls={`${id}-menu`} @click={() => { open = !open }}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 14.5s1.2 1.5 3.5 1.5 3.5-1.5 3.5-1.5"/><path d="M9 9.5h.01M15 9.5h.01"/><path d="M19 3v4M17 5h4"/></svg> </button> <div.ui-reactions-menu id={`${id}-menu`} role="group" aria-label={add_label} hidden={!open}> {for c in choices => <button.ui-reactions-pick key={c} type="button" aria-label={c} aria-pressed={contains(mine, c) ? "true" : "false"} @click={() => { toggle(c) }}>{c}</button>} </div> {name != null && <input type="hidden" name={name} value={join(mine, ",")}/>} </div>}Related
BookmarkButtonA save-for-later toggle whose bookmark fills when pressed, with or without its label.8 views · 8 downloadsFollowButtonFollow becomes Following once pressed, and says Unfollow on hover, as the social sites do.5 views · 7 downloadsLikeButtonA heart that toggles with a count that follows it, a real toggle for screen readers and forms.5 views · 5 downloadsShareButtonShares a link through the system's share sheet, or copies it where the browser has none, and says which.6 views · 6 downloadsVoteButtonsUp and down votes around a score that follows them, as on a forum, in a column or a row.5 views · 6 downloadsPollVote for one option with a tap and see the results as bars with each one's share; the vote also goes in a hidden input.6 views · 6 downloads
Thread
Thread 0 posts
On Reactions: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.