Skip to content
RTL Support: Full
Accessibility: Full
Translations: Not Needed

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-icon events in the examples below are emitted by your wrapper component, not by HLIconPicker. Wire selection up inside your slot content.
  • Slots are yours to fill. The .hr-emoji-container, .hr-gif-container, and .hr-icon-container class names in the examples are styling hooks the component targets — keep them so the built-in layout applies.
  • One type = no tabs. When allowedTypes has 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.

vue
<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'].

Vue
vue
<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.

Vue
vue
<!-- 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.

Vue
vue
<!-- 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.

Vue
vue
<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

Imports

ts
import { HLIconPicker } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
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.

NameDescription
emojisContent for the Emojis tab.
gifsContent for the GIFs tab.
iconsContent for the Icons tab.