Actions

ReactionPicker

A row of emoji reactions with counts, one tap to react or take it back, and a menu to add another.

#button#social#comment#interactive

Demo

Preview

Tap one to react.

The second is single: picking one moves your reaction.

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

Example

The code of the preview above.

demos/reaction_picker.qwe
component ReactionPickerDemoLive() {    state last = null    <div.demo-stack>        <ReactionPicker id="react-demo" name="reaction" reactions={[            { emoji: "👍", label: "Thumbs up", count: 12, mine: true },            { emoji: "❤️", label: "Heart", count: 4 },            { emoji: "🎉", label: "Party", count: 1, mine: true },        ]} choices={[{ emoji: "👍", label: "Thumbs up" }, { emoji: "❤️", label: "Heart" }, { emoji: "🎉", label: "Party" },                    { emoji: "😄", label: "Laugh" }, { emoji: "🤔", label: "Thinking" }, { emoji: "🚀", label: "Rocket" }]}            on_change={(e, on) => { last = `${on ? "Added" : "Removed"} ${e}` }}/>        <p.demo-note>{last ?? "Tap one to react."}</p>    </div>}view ReactionPickerDemo() =    <div.demo-stack>        <ReactionPickerDemoLive/>        <ReactionPicker id="react-demo-one" single={true} size="sm" reactions={[            { emoji: "👍", label: "Agree", count: 8, mine: true }, { emoji: "👎", label: "Disagree", count: 2 }, { emoji: "🤷", label: "Not sure", count: 1 },        ]}/>        <p.demo-note>The second is single: picking one moves your reaction.</p>    </div>

Source

ui/reaction_picker.qwe
// ReactionPicker: a row of emoji reactions with how many gave each, one tap to add// yours or take it back; and, with `choices`, a button that offers more emojis to// react with. Each chip is a toggle (aria-pressed), named by the emoji's label.////   <ReactionPicker reactions={[{ emoji: "👍", label: "Thumbs up", count: 12, mine: true }, { emoji: "🎉", label: "Party", count: 3 }]}/>//   <ReactionPicker id="r7" reactions={rs} choices={["👍", "❤️", "🎉", "😄"]} on_change={(emoji, on) => { react(post, emoji, on) }}/>//// reactions: a list of { emoji, label?, count, mine?: true }: label is what screen// readers call it (the emoji itself when left out). choices: emojis the add button// offers (strings, or { emoji, label }); those already in the row are left out.// single: only one reaction at a time, so picking another moves yours.// id: the add button's menu id, unique on the page (needed with choices).// name: in a <form>, each emoji of yours is sent as name. size: "sm" or "md".// on_change(emoji, on): inside one of your components, given the emoji and whether// you now react with it.import ui/theme.qwe use ui_clslet reaction_picker_css = css`.ui-reaction-picker { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; vertical-align: middle; font: 500 13px/1 var(--ui-font); }.ui-reaction-picker-chip {  display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px 0 8px; border: 1px solid var(--ui-border);  border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: inherit; font-variant-numeric: tabular-nums;  cursor: pointer; transition: background .15s, border-color .15s, color .15s, transform .15s;}.ui-reaction-picker-chip:hover { background: var(--ui-subtle); border-color: var(--ui-strong); }.ui-reaction-picker-chip:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reaction-picker-chip[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)); color: var(--ui-accent); }.ui-reaction-picker-chip:active { transform: scale(.96); }.ui-reaction-picker-emoji { font-size: 16px; line-height: 1; }.ui-reaction-picker-add { padding: 0 9px; color: var(--ui-muted); }.ui-reaction-picker-add svg { width: 16px; height: 16px; }.ui-reaction-picker-menu {  display: flex; flex-wrap: wrap; gap: 2px; width: max-content; max-width: min(280px, calc(100vw - 16px)); margin: auto; padding: 6px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow-lg);}.ui-reaction-picker-choice {  display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--ui-radius-sm);  background: transparent; font-size: 20px; line-height: 1; cursor: pointer;}.ui-reaction-picker-choice:hover { background: var(--ui-subtle); }.ui-reaction-picker-choice:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reaction-picker-menu:popover-open { animation: ui-reaction-picker-in .12s ease-out; }@keyframes ui-reaction-picker-in { from { opacity: 0; transform: translateY(-4px); } }@supports (position-area: bottom) {  .ui-reaction-picker-menu { inset: auto; margin: 4px 0; position-area: bottom span-right; position-try-fallbacks: flip-block, flip-inline; }}@supports not (position-area: bottom) { .ui-reaction-picker-menu::backdrop { background: var(--ui-overlay); } }.ui-reaction-picker-sm .ui-reaction-picker-chip { height: 26px; padding: 0 8px 0 7px; font-size: 12px; }.ui-reaction-picker-sm .ui-reaction-picker-emoji { font-size: 14px; }@media (prefers-reduced-motion: reduce) { .ui-reaction-picker-chip { transition: none; } .ui-reaction-picker-menu:popover-open { animation: none; } }`fn reaction_picker_choices(choices: list) -> list =    choices | map((c) => type_of(c) == "map" ? { emoji: c.emoji, label: get(c, "label") ?? c.emoji } : { emoji: c, label: c })component ReactionPicker(reactions: list = [], choices: list = [], single: bool = false, id: str = "ui-reaction-picker",                         name: str? = null, size: str = "md", on_change = null, class: str? = null) {    style global reaction_picker_css    state rows = reactions | map((r) => { emoji: r.emoji, label: get(r, "label") ?? r.emoji, count: get(r, "count") ?? 0, mine: get(r, "mine") == true })    let toggle = (emoji: str) => {        let was = find(rows, (r) => r.emoji == emoji)        let on = was == null || !was.mine        rows = rows            | map((r) => r.emoji == emoji ? { ...r, mine: on, count: r.count + (on ? 1 : -1) }                        : single && on && r.mine ? { ...r, mine: false, count: r.count - 1 } : r)            | filter((r) => r.count > 0)        if was == null {            let c = find(reaction_picker_choices(choices), (c) => c.emoji == emoji)            rows = rows | push({ emoji, label: c != null ? c.label : emoji, count: 1, mine: true })        }        on_change && on_change(emoji, on)    }    let mine = rows | filter((r) => r.mine) | map((r) => r.emoji)    let offered = reaction_picker_choices(choices) | filter((c) => !any(rows, (r) => r.emoji == c.emoji))    <div class={ui_cls(["ui-reaction-picker", size == "sm" && "ui-reaction-picker-sm", class])} role="group" aria-label="Reactions">        {for r in rows => <button.ui-reaction-picker-chip key={r.emoji} type="button" aria-pressed={r.mine ? "true" : "false"}                aria-label={`${r.label}, ${r.count}`} title={r.label} @click={() => { toggle(r.emoji) }}>            <span.ui-reaction-picker-emoji aria-hidden="true">{r.emoji}</span>            <span aria-hidden="true">{r.count}</span>        </button>}        {len(offered) > 0 && <span style={`anchor-name: --${id}`}>            <button.ui-reaction-picker-chip.ui-reaction-picker-add type="button" popovertarget={`${id}-menu`} aria-label="Add a reaction" title="Add a reaction">                <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.5a4.5 4.5 0 0 0 7 0M9 10h.01M15 10h.01"/></svg>                <span aria-hidden="true">+</span>            </button>            <div.ui-reaction-picker-menu id={`${id}-menu`} popover="auto" role="group" aria-label="Reactions to add" style={`position-anchor: --${id}`}>                {for c in offered => <button.ui-reaction-picker-choice key={c.emoji} type="button" aria-label={c.label} title={c.label}                        popovertarget={`${id}-menu`} popovertargetaction="hide" @click={() => { toggle(c.emoji) }}>{c.emoji}</button>}            </div>        </span>}        {for e in (name != null ? mine : []) => <input key={e} type="hidden" name={name} value={e}/>}    </div>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.