Icon, Emoji, and GIF Picker
A component for selecting icons, emojis, and GIFs with a modern interface. The component is fully customizable with slots for each type.
External dependency note
These examples integrate external packages — emoji-mart-vue-fast, @iconify/vue, and lodash-es — and the GIF tab calls the Tenor API with your own API key. The embedded playground can't resolve these, so copy the examples into a project with those packages installed (and supply a Tenor key) to run them.
How It Works
HLIconPicker is a lightweight tabbed container. It renders one tab per entry in allowedTypes and shows the matching slot's content. It does not ship an emoji grid, icon list, GIF search, or any selection logic of its own — you supply those inside the #emojis, #gifs, and #icons slots, and you own the search state and selection handling there.
- No emits. The component itself emits nothing. The
@select-emoji/@select-gif/@select-iconevents in the examples below are emitted by your wrapper component, not byHLIconPicker. Wire selection up inside your slot content. - Slots are yours to fill. The
.hr-emoji-container,.hr-gif-container, and.hr-icon-containerclass names in the examples are styling hooks the component targets — keep them so the built-in layout applies. - One type = no tabs. When
allowedTypeshas a single entry, the tab bar is hidden and that slot renders directly. See Single Type.
Basic Usage
Set allowed-types and fill the #emojis, #gifs, and #icons slots with your own pickers. The component only renders the tabs you allow.
<template>
<HLIconPicker :allowed-types="['emojis', 'gifs', 'icons']">
<template #emojis>
<div class="hr-emoji-container">
<Picker
:data="emojiIndex"
:emoji-size="21"
native
:per-line="9"
color="var(--primary-700)"
:show-preview="false"
:emoji-tooltip="false"
:i18n="i18n"
@select="showEmoji"
>
<template #searchTemplate="{ onSearch }">
<div class="emoji-search-container">
<HLInput
id="search"
v-model:model-value="emojiSearch"
:prefix-icon="SearchMdIcon"
size="md"
placeholder="Search for an emoji"
@update:model-value="onSearch"
/>
</div>
</template>
</Picker>
</div>
</template>
<template #gifs>
<div :key="top_10_gifs.length" class="hr-gif-container">
<div class="gif-search-container">
<HLInput
id="search"
v-model:model-value="gifSearch"
:prefix-icon="SearchMdIcon"
size="md"
placeholder="Search for GIFs"
@update:model-value="searchGif"
/>
</div>
<div class="gif-list">
<div v-for="(gif, index) in top_10_gifs" :key="index" class="gif-item" @click="selectGif(gif)">
<img :src="gif.media_formats.nanogif.url" alt="gif" />
</div>
</div>
</div>
</template>
<template #icons>
<div class="hr-icon-container">
<div class="hr-icon-search-container">
<HLInput
id="icon-search"
v-model:model-value="iconSearch"
:prefix-icon="SearchMdIcon"
size="md"
placeholder="Search for an icon"
@update:model-value="searchIcon"
/>
</div>
<div class="icons">
<div v-for="icon in icons" :key="icon" class="icon-item">
<Icon :icon="icon" @click="selectIcon(icon)" />
</div>
</div>
</div>
</template>
</HLIconPicker>
</template>
<script setup lang="ts">
import { iconToHTML, iconToSVG, replaceIDs } from '@iconify/utils'
import { Icon, getIcon } from '@iconify/vue'
import { HLIconPicker, HLInput } from '@platform-ui/highrise'
import 'emoji-mart-vue-fast/css/emoji-mart.css'
import data from 'emoji-mart-vue-fast/data/all.json'
import { EmojiIndex, Picker } from 'emoji-mart-vue-fast/src'
import { debounce } from 'lodash-es'
import { onMounted, ref } from 'vue'
import { SearchMdIcon } from '@gohighlevel/ghl-icons/24/outline'
const emit = defineEmits(['select-gif', 'select-emoji', 'select-icon'])
// start of emoji
// https://github.com/serebrov/emoji-mart-vue
let emojiIndex = new EmojiIndex(data)
function showEmoji(emoji: any) {
// console.log(emoji)
emit('select-emoji', emoji)
console.log(emoji)
}
const i18n = {
search: 'Search for an emoji',
notfound: 'No emoji found',
categories: {
search: 'Search Results',
recent: 'Recent',
smileys: 'Smileys',
people: 'People & Body',
nature: 'Animals & Nature',
foods: 'Food & Drink',
activity: 'Activity',
places: 'Travel & Places',
objects: 'Objects',
symbols: 'My Symbols',
flags: 'My Flags',
custom: 'My Custom',
},
}
// end of emoji
// start of gif
// url Async requesting function
async function httpGetAsync(theUrl): Promise<any> {
const response = await fetch(theUrl)
return response.json()
}
const top_10_gifs = ref([])
// function to call the trending and category endpoints
async function grab_data() {
// set the apikey and limit
var apikey = '**********'
var clientkey = 'my_test_app'
var lmt = 10
var featured_url = 'https://tenor.googleapis.com/v2/featured?key=' + apikey + '&client_key=' + clientkey + '&limit=' + lmt
const response = await httpGetAsync(featured_url)
top_10_gifs.value = response.results
// data will be loaded by each call's callback
return
}
// SUPPORT FUNCTIONS ABOVE
// MAIN BELOW
// start the flow
grab_data()
const emojiSearch = ref('')
const gifSearch = ref('')
async function searchGif() {
// test search term
if (!gifSearch.value) {
grab_data()
return
}
var search_term = gifSearch.value
var apikey = '**********'
var clientkey = 'my_test_app'
var lmt = 10
// using default locale of en_US
var search_url =
'https://tenor.googleapis.com/v2/search?q=' + search_term + '&key=' + apikey + '&client_key=' + clientkey + '&limit=' + lmt
const response = await httpGetAsync(search_url)
top_10_gifs.value = response.results
}
const selectGif = gif => {
emit('select-gif', gif)
console.log(gif)
}
// end of gif
// start of icon
const getIconsFromAPI = (apiEndpoint: string, searchStr?: string) => {
return new Promise((resolve, reject) => {
fetch(`${apiEndpoint}/search?query=${searchStr}&limit=100`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to retrieve icons. Please try again later.')
}
return response.json()
})
.then(response => {
return resolve(response.icons)
})
.catch(err => {
reject(err)
})
})
}
const iconifyUrl = 'https://api.iconify.design'
const icons = ref<any[]>([])
const iconSearch = ref('social')
const dfb = debounce(() => {
const searchTerm = iconSearch.value || 'social'
getIconsFromAPI(iconifyUrl, searchTerm).then(response => {
icons.value = response as any[]
})
}, 500)
const searchIcon = () => {
dfb()
}
onMounted(() => {
getIconsFromAPI(iconifyUrl, 'social').then(response => {
icons.value = response as any[]
})
console.log(icons.value)
})
// end of icon
const selectIcon = (icon: any) => {
const iconData: any = getIcon(icon)
const svgData = iconToSVG(iconData, {
height: '100%',
width: '100%',
})
const constructedSVG = iconToHTML(replaceIDs(svgData.body), svgData.attributes)
emit('select-icon', icon, constructedSVG)
console.log(icon, constructedSVG)
}
</script>Only Emojis and Icons
Don't pass the gifs slot to the component, and allowed-types should be ['emojis', 'icons'].
<HLIconPicker :allowed-types="['emojis', 'icons']">
<template #emojis>
<div class="hr-emoji-container">
<Picker
:data="emojiIndex"
:emoji-size="21"
native
:per-line="9"
color="var(--primary-700)"
:show-preview="false"
:emoji-tooltip="false"
:i18n="i18n"
@select="showEmoji"
>
<template #searchTemplate="{ onSearch }">
<div class="emoji-search-container">
<HLInput
id="search"
v-model:model-value="emojiSearch"
:prefix-icon="SearchMdIcon"
size="md"
placeholder="Search for an emoji"
@update:model-value="onSearch"
/>
</div>
</template>
</Picker>
</div>
</template>
<template #icons>
<div class="hr-icon-container">
<div class="hr-icon-search-container">
<HLInput
id="icon-search"
v-model:model-value="iconSearch"
:prefix-icon="SearchMdIcon"
size="md"
placeholder="Search for an icon"
@update:model-value="searchIcon"
/>
</div>
<div class="icons">
<div v-for="icon in icons" :key="icon" class="icon-item">
<Icon :icon="icon" @click="selectIcon(icon)" />
</div>
</div>
</div>
</template>
</HLIconPicker>Default Tab
By default the first allowed type is selected when the picker opens. Set default-tab to open on a specific tab instead. If the value isn't in allowed-types, the picker falls back to the first allowed type.
<!-- Opens on the GIFs tab instead of Emojis -->
<HLIconPicker :allowed-types="['emojis', 'gifs', 'icons']" default-tab="gifs">
<template #emojis> <!-- emoji picker --> </template>
<template #gifs> <!-- gif picker --> </template>
<template #icons> <!-- icon picker --> </template>
</HLIconPicker>Single Type
When allowed-types has a single entry, the tab bar is hidden and that slot renders directly — useful for an emoji-only or icon-only picker.
<!-- Emoji-only picker, no tabs -->
<HLIconPicker :allowed-types="['emojis']">
<template #emojis>
<div class="hr-emoji-container">
<Picker
:data="emojiIndex"
:emoji-size="21"
native
:per-line="9"
:show-preview="false"
:i18n="i18n"
@select="showEmoji"
/>
</div>
</template>
</HLIconPicker>Inside an Input
A common pattern is triggering the picker from an icon inside an input — for example an emoji button in a message or note field. Wrap the picker in an HLPopover and append the selected value to the input's model.
<template>
<HLInput v-model:model-value="message">
<template #suffix>
<HLPopover trigger="click">
<template #trigger>
<HLIcon size="20" class="cursor-pointer">
<FaceSmileIcon />
</HLIcon>
</template>
<HLIconPicker :allowed-types="['emojis']">
<template #emojis>
<div class="hr-emoji-container">
<Picker :data="emojiIndex" native :i18n="i18n" @select="handleSelectEmoji" />
</div>
</template>
</HLIconPicker>
</HLPopover>
</template>
</HLInput>
</template>
<script setup lang="ts">
import { HLIconPicker, HLInput, HLPopover, HLIcon } from '@platform-ui/highrise'
import { FaceSmileIcon } from '@gohighlevel/ghl-icons/24/outline'
import { EmojiIndex, Picker } from 'emoji-mart-vue-fast/src'
import data from 'emoji-mart-vue-fast/data/all.json'
import 'emoji-mart-vue-fast/css/emoji-mart.css'
import { ref } from 'vue'
const message = ref('')
const emojiIndex = new EmojiIndex(data)
const i18n = { search: 'Search for an emoji', notfound: 'No emoji found' }
// Append the picked emoji's native glyph to the input
const handleSelectEmoji = (emoji: { native: string }) => {
message.value += emoji.native
}
</script>Libraries
- Emojis: emoji-mart-vue-fast
- GIFs: tenor-js
- Icons: @iconify/vue
Imports
import { HLIconPicker } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
allowedTypes | ('emojis' | 'gifs' | 'icons')[] | ['emojis', 'gifs', 'icons'] | Which tabs to render, in order. A single entry hides the tab bar and renders that slot directly. |
defaultTab | 'emojis' | 'gifs' | 'icons' | 'emojis' | Tab selected on open. Falls back to the first entry in allowedTypes if it isn't an allowed type. |
Emits
HLIconPicker does not emit any events. Handle selection inside your slot content (see How It Works).
Slots
Fill each slot with your own picker UI. Wrap the content in the matching container class (hr-emoji-container, hr-gif-container, hr-icon-container) so the component's built-in layout and scrolling apply.
| Name | Description |
|---|---|
emojis | Content for the Emojis tab. |
gifs | Content for the GIFs tab. |
icons | Content for the Icons tab. |