Forms

EmojiPicker

Pick an emoji from tabs of categories or by searching their names; each is named for screen readers and a form gets the one picked.

#forms#picker#search#social#interactive

Demo

Preview
Reaction

Smileys

None picked

Search by name: party, coffee, heart.

Status

Travel

๐ŸŒดpalm tree holiday

Your status: ๐ŸŒด

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

Example

The code of the preview above.

demos/emoji_picker.qwe
component EmojiPickerDemoLive() {    state status = "๐ŸŒด"    <div.demo-stack>        <EmojiPicker id="emoji-picker-demo-live" label="Status" value={status} on_change={(e) => { status = e }}/>        <p.demo-note>Your status: {status}</p>    </div>}view EmojiPickerDemo() =    <div.demo-stack>        <EmojiPicker id="emoji-picker-demo-all" name="emoji" label="Reaction" hint="Search by name: party, coffee, heart."/>        <EmojiPickerDemoLive/>    </div>

Source

ui/emoji_picker.qwe
// EmojiPicker: pick an emoji from tabs of categories (smileys, people, animals,// food, travel, activities, objects, symbols) or by searching their names. The// emoji are radio buttons, so the arrow keys move between them, and each is named// for screen readers ("grinning face"); the one picked goes in a hidden input// named `name`, so a plain <form> sends it.////   <EmojiPicker id="reaction" name="emoji" label="Reaction"/>//   <EmojiPicker id="status" label="Status" value="๐ŸŒด" on_change={(e) => { status = e }}/>//// id: a prefix for its ids, unique on the page. value: the emoji picked to start// with. categories: the categories to show, in place of the eight built in// (`emoji_picker_sets`: a list of { label, emoji: [[emoji, name], ...] }).// search (true): the search box, which looks through every category. columns (8):// emoji per row. empty: what the grid says when nothing matches ("No emoji match").// label, hint, error and required as for every field. The tabs follow the ARIA// tabs pattern: the arrow keys, Home and End move between them.// on_change(emoji): inside one of your components, given the emoji picked.import ui/theme.qwe use ui_clslet emoji_picker_css = css`.ui-emoji-picker { display: grid; gap: 8px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-emoji-picker-legend { padding: 0; font-size: 13px; font-weight: 500; }.ui-emoji-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-emoji-picker-panel { display: grid; gap: 8px; width: 100%; max-width: 360px; padding: 10px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow); }.ui-emoji-picker-search {  box-sizing: border-box; width: 100%; height: 34px; margin: 0; padding: 0 10px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 13px var(--ui-font);}.ui-emoji-picker-search::placeholder { color: var(--ui-muted); opacity: 1; }.ui-emoji-picker-search:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-emoji-picker-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--ui-border); }.ui-emoji-picker-tab {  flex: 1; display: grid; place-items: center; height: 32px; padding: 0; border: 0; border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;  background: none; font-size: 17px; line-height: 1; cursor: pointer; opacity: .6; transition: opacity .15s, background .15s;}.ui-emoji-picker-tab:hover { opacity: 1; background: var(--ui-subtle); }.ui-emoji-picker-tab[aria-selected="true"] { opacity: 1; box-shadow: inset 0 -2px 0 var(--ui-accent); }.ui-emoji-picker-tab:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-emoji-picker-heading { margin: 0; font-size: 12px; font-weight: 600; color: var(--ui-muted); text-transform: uppercase; letter-spacing: .05em; }.ui-emoji-picker-grid { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 2px; max-height: 210px; overflow: auto; padding: 2px; }.ui-emoji-picker-opt { position: relative; display: grid; place-items: center; aspect-ratio: 1; border-radius: var(--ui-radius-sm); font-size: 22px; line-height: 1; cursor: pointer; }.ui-emoji-picker-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-emoji-picker-opt:hover { background: var(--ui-subtle); }.ui-emoji-picker-opt:has(input:checked) { background: var(--ui-accent-soft); box-shadow: inset 0 0 0 2px var(--ui-accent); }.ui-emoji-picker-opt:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-emoji-picker-empty { grid-column: 1 / -1; padding: 14px 4px; color: var(--ui-muted); font-size: 13px; text-align: center; }.ui-emoji-picker-status { display: flex; align-items: center; gap: 8px; min-height: 26px; font-size: 12px; color: var(--ui-muted); }.ui-emoji-picker-status b { font-size: 20px; font-weight: 400; line-height: 1; }.ui-emoji-picker-invalid .ui-emoji-picker-panel { border-color: var(--ui-danger); }.ui-emoji-picker-hint, .ui-emoji-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-emoji-picker-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-emoji-picker-tab { transition: none; } }`let emoji_picker_sets = [    { label: "Smileys", emoji: [["๐Ÿ˜€", "grinning face"], ["๐Ÿ˜ƒ", "smiling face with big eyes"], ["๐Ÿ˜„", "smiling face with smiling eyes"],        ["๐Ÿ˜", "beaming face"], ["๐Ÿ˜†", "laughing face"], ["๐Ÿ˜…", "sweat smile"], ["๐Ÿ˜‚", "tears of joy laugh"], ["๐Ÿ™‚", "slightly smiling face"],        ["๐Ÿ˜‰", "winking face"], ["๐Ÿ˜Š", "blush happy"], ["๐Ÿ˜", "heart eyes love"], ["๐Ÿ˜˜", "kiss"], ["๐Ÿ˜Ž", "cool sunglasses"],        ["๐Ÿค”", "thinking face"], ["๐Ÿ˜", "neutral face"], ["๐Ÿ˜ด", "sleeping face"], ["๐Ÿ˜ข", "crying face sad"], ["๐Ÿ˜ก", "angry face"],        ["๐Ÿคฏ", "mind blown"], ["๐Ÿฅณ", "party face celebrate"]] },    { label: "People", emoji: [["๐Ÿ‘", "thumbs up yes"], ["๐Ÿ‘Ž", "thumbs down no"], ["๐Ÿ‘", "clapping hands"], ["๐Ÿ™Œ", "raising hands hooray"],        ["๐Ÿ‘‹", "waving hand hello"], ["๐Ÿ™", "folded hands thanks please"], ["๐Ÿ’ช", "flexed biceps strong"], ["โœŒ๏ธ", "victory hand peace"],        ["๐Ÿค", "handshake deal"], ["๐Ÿ‘€", "eyes look"], ["๐Ÿง‘โ€๐Ÿ’ป", "technologist coder"], ["๐Ÿง‘โ€๐ŸŽจ", "artist"], ["๐Ÿง‘โ€๐Ÿณ", "cook chef"],        ["๐Ÿ•บ", "man dancing"], ["๐Ÿ’ƒ", "woman dancing"], ["๐Ÿคท", "shrug"]] },    { label: "Animals", emoji: [["๐Ÿถ", "dog face"], ["๐Ÿฑ", "cat face"], ["๐ŸฆŠ", "fox"], ["๐Ÿป", "bear"], ["๐Ÿผ", "panda"], ["๐Ÿจ", "koala"],        ["๐Ÿฏ", "tiger"], ["๐Ÿฆ", "lion"], ["๐Ÿฎ", "cow"], ["๐Ÿท", "pig"], ["๐Ÿธ", "frog"], ["๐Ÿต", "monkey"], ["๐Ÿ”", "chicken"],        ["๐Ÿง", "penguin"], ["๐Ÿฆ‰", "owl"], ["๐Ÿข", "turtle"], ["๐Ÿ™", "octopus"], ["๐Ÿฆ‹", "butterfly"], ["๐ŸŒธ", "cherry blossom flower"], ["๐ŸŒณ", "tree"]] },    { label: "Food", emoji: [["๐ŸŽ", "red apple"], ["๐ŸŒ", "banana"], ["๐Ÿ‡", "grapes"], ["๐Ÿ“", "strawberry"], ["๐Ÿฅ‘", "avocado"],        ["๐Ÿ•", "pizza"], ["๐Ÿ”", "hamburger burger"], ["๐ŸŒฎ", "taco"], ["๐Ÿฃ", "sushi"], ["๐Ÿœ", "noodles ramen"], ["๐Ÿฅ", "croissant"],        ["๐Ÿฐ", "cake"], ["๐Ÿฉ", "doughnut"], ["๐Ÿช", "cookie"], ["โ˜•", "hot beverage coffee tea"], ["๐Ÿท", "wine glass"], ["๐Ÿบ", "beer"]] },    { label: "Travel", emoji: [["๐Ÿš—", "car"], ["๐Ÿšฒ", "bicycle bike"], ["๐Ÿš†", "train"], ["โœˆ๏ธ", "airplane plane flight"], ["๐Ÿš€", "rocket launch"],        ["โ›ต", "sailboat"], ["๐Ÿ–๏ธ", "beach"], ["๐Ÿ”๏ธ", "snow mountain"], ["๐Ÿ•๏ธ", "camping tent"], ["๐ŸŒด", "palm tree holiday"],        ["๐Ÿ—ฝ", "statue of liberty"], ["๐Ÿ—ผ", "tower"], ["๐ŸŒ", "globe earth world"], ["๐ŸŒ™", "crescent moon night"], ["โ˜€๏ธ", "sun"], ["๐ŸŒˆ", "rainbow"]] },    { label: "Activities", emoji: [["โšฝ", "soccer football"], ["๐Ÿ€", "basketball"], ["๐ŸŽพ", "tennis"], ["๐Ÿ“", "ping pong"], ["๐ŸŽณ", "bowling"],        ["๐ŸŽฏ", "direct hit target"], ["๐ŸŽฎ", "video game"], ["๐ŸŽฒ", "game die dice"], ["๐Ÿงฉ", "puzzle piece"], ["๐ŸŽจ", "artist palette paint"],        ["๐ŸŽธ", "guitar music"], ["๐ŸŽง", "headphones"], ["๐ŸŽ‰", "party popper celebrate"], ["๐Ÿ†", "trophy win"], ["๐Ÿฅ‡", "first place medal"]] },    { label: "Objects", emoji: [["๐Ÿ’ก", "light bulb idea"], ["๐Ÿ’ป", "laptop computer"], ["๐Ÿ“ฑ", "mobile phone"], ["โŒš", "watch"], ["๐Ÿ“ท", "camera"],        ["๐Ÿ“š", "books"], ["โœ๏ธ", "pencil"], ["๐Ÿ“Ž", "paperclip"], ["๐Ÿ“Œ", "pushpin"], ["๐Ÿ”‘", "key"], ["๐Ÿ”’", "locked"], ["๐Ÿ””", "bell"],        ["๐Ÿ“ฆ", "package box"], ["๐ŸŽ", "wrapped gift present"], ["๐Ÿ’ฐ", "money bag"], ["๐Ÿ› ๏ธ", "hammer and wrench tools"]] },    { label: "Symbols", emoji: [["โค๏ธ", "red heart love"], ["๐Ÿ’”", "broken heart"], ["โญ", "star"], ["โœจ", "sparkles"], ["๐Ÿ”ฅ", "fire hot lit"],        ["๐Ÿ’ฏ", "hundred points"], ["โœ…", "check mark done"], ["โŒ", "cross mark no"], ["โš ๏ธ", "warning"], ["โ“", "question mark"],        ["โ—", "exclamation mark"], ["โž•", "plus"], ["โž–", "minus"], ["๐Ÿ”ด", "red circle"], ["๐ŸŸข", "green circle"], ["๐Ÿ”ต", "blue circle"]] },]component EmojiPicker(id: str, name: str? = null, label: str? = null, value: str? = null, categories: list? = null,                      search: bool = true, columns: int = 8, empty: str = "No emoji match", hint: str? = null,                      error: str? = null, required: bool = false, on_change = null, class: str? = null) {    style global emoji_picker_css    let sets = categories ?? emoji_picker_sets    state chosen = value    state query = ""    state at = max([0, index_of(sets | map((s) => any(s.emoji, (e) => e[0] == value)), true) ?? 0])    let needle = lower(trim(query))    let searching = needle != ""    let shown = searching ? flatten(sets | map((s) => s.emoji)) | filter((e) => contains(e[1], needle)) : sets[at].emoji    let chosen_name = chosen != null ? (find(flatten(sets | map((s) => s.emoji)), (e) => e[0] == chosen) ?? [chosen, chosen])[1] : null    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    let go = (e, i: int) => {        at = i        let tabs = e.target.closest(".ui-emoji-picker-tabs")        js("requestAnimationFrame", (t = null) => {            let el = tabs.children[i]            el && el.focus()        })    }    <fieldset class={ui_cls(["ui-emoji-picker", error != null && "ui-emoji-picker-invalid", class])} id={id}              aria-describedby={described != "" ? described : null}>        {label != null && <legend.ui-emoji-picker-legend>            {label}{required && <span.ui-emoji-picker-req aria-hidden="true">*</span>}        </legend>}        <div.ui-emoji-picker-panel>            {search && <input.ui-emoji-picker-search type="search" id={`${id}-search`} placeholder="Search emoji" value={query}                              autocomplete="off" spellcheck="false" aria-label="Search emoji" aria-controls={`${id}-grid`}                              form="ui-emoji-picker-picker-none" @input={(e) => { query = e.value }}/>}            {!searching && <div.ui-emoji-picker-tabs role="tablist" aria-label="Categories" @keydown={(e) => {                let n = len(sets)                let to = match e.key {                    "ArrowRight" => (at + 1) % n                    "ArrowLeft" => (at + n - 1) % n                    "Home" => 0                    "End" => n - 1                    _ => null                }                if to != null {                    e.preventDefault()                    go(e, to)                }            }}>                {for [i, s] in enumerate(sets) => <button.ui-emoji-picker-tab type="button" role="tab" id={`${id}-tab-${i}`}                        aria-selected={i == at ? "true" : "false"} aria-controls={`${id}-grid`} tabindex={i == at ? "0" : "-1"}                        aria-label={s.label} title={s.label} @click={() => { at = i }}>                    <span aria-hidden="true">{len(s.emoji) > 0 ? s.emoji[0][0] : "?"}</span>                </button>}            </div>}            <p.ui-emoji-picker-heading id={`${id}-heading`}>{searching ? `Results for โ€œ${trim(query)}โ€` : sets[at].label}</p>            <div.ui-emoji-picker-grid id={`${id}-grid`} role="radiogroup" aria-labelledby={`${id}-heading`}                 style={`--cols: ${columns}`}>                {for e in shown => <label.ui-emoji-picker-opt key={e[0]} title={e[1]}>                    <input type="radio" name={`${id}-radio`} value={e[0]} checked={e[0] == chosen} form="ui-emoji-picker-picker-none"                           aria-label={e[1]}                           @change={() => {                               chosen = e[0]                               on_change && on_change(e[0])                           }}/>                    <span aria-hidden="true">{e[0]}</span>                </label>}                {len(shown) == 0 && <p.ui-emoji-picker-empty role="status">{empty}</p>}            </div>            <div.ui-emoji-picker-status aria-live="polite">                {chosen != null ? [<b>{chosen}</b>, <span>{chosen_name}</span>] : <span>None picked</span>}            </div>        </div>        {name != null && <input type="hidden" name={name} value={chosen ?? ""} required={required}/>}        {hint != null && <p.ui-emoji-picker-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-emoji-picker-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
47Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.