Social

Reactions

Emoji reaction chips with counts, yours marked, and an add button that opens a row of emoji to pick from.

#social#button#forms#interactive

Demo

Preview

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.qwe
main.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>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.