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.
Demo
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.qwemain.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>}Related
IconPickerPick an icon from a grid of 51, narrowed by a search over names and meanings; the arrow keys move and a form gets the name.5 views ยท 5 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views ยท 7 downloadsBirthdayInputDay, month and year drop-downs that give one date in a hidden input, the days following the month and the years the age allowed.5 views ยท 7 downloadsColorPickerA colour as hex, or picked from a square, a hue slider and swatches; it says when hex is wrong.4 views ยท 5 downloadsColorSwatchesPick one colour from a set of round swatches: real radio buttons, so they post with a form and work with the keyboard.5 views ยท 5 downloadsComboboxA text box that filters a list of options as you type, or asks for them (a server, a map service), with the arrow keys, Enter and Escape.5 views ยท 6 downloads
Thread
Thread 0 posts
On EmojiPicker: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.