Skip to content

Integrate Unsplash Library

Easily add Unsplash image search to your media library modal—just like in the demo! A code example is provided so you can copy-paste to get started.

Unsplash Integration Guide

This guide shows you how to add Unsplash image search to your custom media library modal in the Vue Website Page Builder. You can use the code below almost as-is—just add your Unsplash API key and connect it to your modal.

Custom Media Library Component

By default, the Page Builder does not include a built-in media library.

First You may extend the Page Builder by adding your own media library. Inject your media library component easily to tailor the builder to your application's needs.

vue
<template>
  <div>
    <PageBuilder :CustomMediaLibraryComponent="YourMediaLibraryComponent" />
  </div>
</template>

Example: Unsplash Media Library

You can copy following this code into your own media library modal and customize the UI as needed.
For a full working example, see the demo project or reach out for support!

The demo layout follows the same responsive split used by the product picker modal: a larger results column and a desktop-only details sidebar, with a dedicated stacked details panel on mobile.

Security Note:
For production use, it is recommended to fetch Unsplash images from your own backend server, not directly from the frontend.
If you use the Unsplash API key in frontend code, it will be visible in browser devtools and can be misused.
Instead, create a backend endpoint that proxies Unsplash requests and keeps your API key secure.
Your frontend can then fetch images from your backend and render them as shown in the example below.

vue
<script setup>
import { ref, onMounted } from 'vue'
const unsplashKey = import.meta.env.VITE_UNSPLASH_KEY

import { getPageBuilder, usePageBuilderModal } from '@myissue/vue-website-page-builder'

// Retrieve Page Builder service instance
const pageBuilderService = getPageBuilder()

const { closeMediaLibraryModal } = usePageBuilderModal()

const getIsLoading = ref(false)
const getSearchTerm = ref('')
const getCurrentPageNumber = ref(1)
const getOrientationValue = ref('')
const getCurrentImage = ref('')
const getCurrentUser = ref('')

const getUnsplashImages = ref([])

const fetchUnsplash = async function () {
  getIsLoading.value = true

  localStorage.setItem('unsplash-query', getSearchTerm.value)
  localStorage.setItem('unsplash-page', getCurrentPageNumber.value)

  if (
    getUnsplashImages.value &&
    Array.isArray(getUnsplashImages.value.results) &&
    getUnsplashImages.value.results.length === 0
  ) {
    getCurrentPageNumber.value = 1
  }

  const orientationParam = getOrientationValue.value
    ? `&orientation=${getOrientationValue.value}`
    : ''

  try {
    const response = await fetch(
      `https://api.unsplash.com/search/photos?page=${
        getCurrentPageNumber.value
      }&per_page=24&query=${getSearchTerm.value || 'kinfolk'}${orientationParam}`,
      {
        headers: {
          'Accept-Version': 'v1',
          Authorization: `Client-ID ${unsplashKey}`,
        },
      },
    )

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    const images = await response.json()
    getUnsplashImages.value = images
    getIsLoading.value = false
  } catch (error) {
    console.error('Error fetching Unsplash images:', error)
  } finally {
    getIsLoading.value = false
  }
}

const handleImageClick = async function (data) {
  getCurrentImage.value = data.url || ''
}

const searchByOrientation = function (orientationParameter) {
  if (getOrientationValue.value !== orientationParameter) {
    getOrientationValue.value = orientationParameter
    getCurrentPageNumber.value = 1
    fetchUnsplash()
  }
}

const previousPage = function () {
  localStorage.setItem('unsplash-page', getCurrentPageNumber.value)
  fetchUnsplash()
}

const nextPage = async function () {
  localStorage.setItem('unsplash-page', getCurrentPageNumber.value)
  fetchUnsplash()
}

const useImage = async function (imageURL) {
  await pageBuilderService.applySelectedImage({
    src: `${imageURL}`,
  })

  closeMediaLibraryModal()
}

onMounted(async () => {
  getSearchTerm.value = localStorage.getItem('unsplash-query') || 'kinfolk'
  getCurrentPageNumber.value = Number(localStorage.getItem('unsplash-page')) || 1

  await fetchUnsplash()
})
</script>

<template>
  <div>
    <div>
      <form
        @submit.prevent="
          () => {
            getCurrentPageNumber = 1
            fetchUnsplash()
          }
        "
      >
        <label
          for="default-search"
          class="mb-2 text-sm font-normal text-gray-900 sr-only dark:text-gray-300"
          >Search</label
        >

        <div class="flex items-center gap-2 border border-gray-300 rounded-lg overflow-hidden">
          <div class="relative w-full">
            <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
              <span class="material-symbols-outlined"> search </span>
            </div>
            <input
              type="search"
              id="search_query"
              v-model="getSearchTerm"
              class="w-full text-sm py-4 border-none outline-none pl-10 bg-transparent focus:ring-0 focus:outline-none"
              autocomplete="off"
              placeholder="Search..."
            />
          </div>

          <button
            @click="
              () => {
                getCurrentPageNumber = 1
                fetchUnsplash()
              }
            "
            type="submit"
            class="px-4 py-2 text-sm font-medium rounded-full border border-gray-300 hover:bg-gray-50 cursor-pointer break-keep mr-4"
          >
            Search
          </button>
        </div>
      </form>
      <div class="mt-2">
        <div
          v-if="getUnsplashImages && getUnsplashImages.results"
          class="flex lg:justify-between justify-end items-center gap-2 py-2 px-2 mb-1 rounded-full border border-gray-200 shadow-sm"
        >
          <div class="lg:flex hidden justify-left items-center gap-2">
            <button
              @click="searchByOrientation('landscape')"
              type="button"
              class="px-3 py-1.5 text-sm font-medium rounded-full border border-gray-300 cursor-pointer"
              :class="{
                'bg-blue-600 text-white': getOrientationValue === 'landscape',
                'hover:bg-gray-50': getOrientationValue !== 'landscape',
              }"
            >
              Landscape
            </button>
            <button
              @click="searchByOrientation('portrait')"
              type="button"
              class="px-3 py-1.5 text-sm font-medium rounded-full border border-gray-300 cursor-pointer"
              :class="{
                'bg-blue-600 text-white': getOrientationValue === 'portrait',
                'hover:bg-gray-50': getOrientationValue !== 'portrait',
              }"
            >
              Portrait
            </button>
            <button
              @click="searchByOrientation('squarish')"
              type="button"
              class="px-3 py-1.5 text-sm font-medium rounded-full border border-gray-300 cursor-pointer"
              :class="{
                'bg-blue-600 text-white': getOrientationValue === 'squarish',
                'hover:bg-gray-50': getOrientationValue !== 'squarish',
              }"
            >
              Squarish
            </button>

            <svg
              @click="searchByOrientation(null)"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="2"
              stroke="currentColor"
              class="w-4 h-4 cursor-pointer"
            >
              <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
            </svg>
          </div>

          <nav class="flex items-center gap-2 justify-start" aria-label="Pagination">
            <p class="text-sm text-gray-700 text-xs italic">
              Total pages {{ getUnsplashImages.total_pages }}
            </p>
            <p class="text-sm text-gray-700 text-xs italic">Images {{ getUnsplashImages.total }}</p>
            <div class="flex flex-1 justify-between sm:justify-end items-center gap-2">
              <span
                v-if="Number(getCurrentPageNumber) !== 1"
                class="text-sm text-gray-700 text-xs italic pr-2 pl-1 cursor-pointer underline"
                @click="nextPage(Number((getCurrentPageNumber = 1)))"
              >
                First page
              </span>
            </div>
            <button
              v-if="Number(getCurrentPageNumber) > 1"
              :disabled="Number(getCurrentPageNumber) < 1"
              class="px-3 py-1.5 text-sm font-medium rounded-full border border-gray-300 hover:bg-gray-50 cursor-pointer disabled:opacity-50"
              @click="previousPage(Number(getCurrentPageNumber--))"
            >
              {{
                `Prev ${
                  Number(getCurrentPageNumber) > 0
                    ? Number(getCurrentPageNumber) - 1
                    : Number(getCurrentPageNumber) - 1
                }`
              }}
            </button>

            <span class="px-4 py-2.5 text-sm font-medium rounded-full border border-gray-300">
              {{ Number(getCurrentPageNumber) }}
            </span>
            <button
              :disabled="Number(getCurrentPageNumber) >= getUnsplashImages.total_pages"
              class="px-3 py-1.5 text-sm font-medium rounded-full border border-gray-300 hover:bg-gray-50 cursor-pointer disabled:opacity-50"
              :class="{
                'opacity-50': Number(getCurrentPageNumber) >= getUnsplashImages.total_pages,
              }"
              @click="nextPage(Number(getCurrentPageNumber++))"
            >
              {{
                `Next ${
                  Number(getCurrentPageNumber) > 0
                    ? Number(getCurrentPageNumber) + 1
                    : Number(getCurrentPageNumber) + 1
                }`
              }}
            </button>
          </nav>
        </div>

        <div class="min-h-dvh flex gap-6">
          <div class="w-9/12 pr-1 rounded-lg overflow-y-auto">
            <div v-if="getIsLoading">
              <div class="flex items-center justify-center mt-4">
                <div
                  class="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"
                >
                  <span
                    class="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
                    >Loading...</span
                  >
                </div>
              </div>
            </div>
            <div v-if="getUnsplashImages && getUnsplashImages.results">
              <div
                v-if="!getIsLoading"
                class="grid md:grid-cols-4 sm:grid-cols-4 grid-cols-2 gap-2"
              >
                <div
                  v-for="image in getUnsplashImages.results"
                  :key="image.id"
                  @click="
                    handleImageClick({
                      url: image.urls.regular,
                      user: image.user.name,
                    })
                  "
                  class="border border-gray-200 my-2 px-2 p-2 cursor-pointer"
                >
                  <img
                    :alt="image.user.name"
                    class="group block w-full overflow-hidden cursor-pointer"
                    :src="image.urls.thumb"
                  />
                  <p class="text-sm text-gray-700 text-xs font-normal mt-2">
                    By: {{ image.user.name }}
                  </p>
                </div>
              </div>
            </div>

            <div
              v-if="
                getUnsplashImages &&
                getUnsplashImages.results &&
                getUnsplashImages.results.length < 1
              "
            >
              <p class="text-sm text-gray-700 py-4 px-4">
                <span v-if="getCurrentPageNumber === 1">
                  We did not find any images. Make a new search.
                </span>
                <span
                  v-if="getCurrentPageNumber > 1"
                  @click="nextPage(1)"
                  class="text-blue-600 underline cursor-pointer"
                >
                  No results on current page. Navigate to First Page.
                </span>
              </p>
            </div>
          </div>
          <!-- Sidebar # start -->
          <aside class="w-3/12 overflow-y-auto">
            <template v-if="getCurrentImage">
              <img
                class="mx-auto block w-full object-cover object-center cursor-pointer"
                :src="`${getCurrentImage}`"
                alt="file"
              />
              <div class="md:px-3 px-2">
                <div>
                  <p class="text-sm text-gray-700 font-normal text-gray-900 pt-4">Information</p>
                  <dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
                    <div class="py-3 flex justify-between text-sm font-normal items-center">
                      <dt class="text-gray-500">From:</dt>
                      <dd class="text-gray-900">Unsplash</dd>
                    </div>
                    <div class="py-3 flex justify-between text-sm font-normal items-center">
                      <dt class="text-gray-500">By:</dt>
                      <dd class="text-gray-900">
                        {{ getCurrentUser }}
                      </dd>
                    </div>
                  </dl>
                </div>
              </div>
            </template>
          </aside>
        </div>
        <!-- Sidebar # end -->

        <!-- Actions footer # start -->
        <div class="bg-slate-50 px-2 py-4 flex sm:justify-end justify-center gap-3">
          <button
            @click="
              () => {
                closeMediaLibraryModal()
                localStorage.setItem('unsplash-page', getCurrentPageNumber)
              }
            "
            class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 cursor-pointer"
            type="button"
          >
            Close
          </button>
          <button
            v-if="getCurrentImage && typeof getCurrentImage === 'string'"
            @click="useImage(getCurrentImage)"
            class="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 cursor-pointer"
            type="button"
          >
            Select image
          </button>
        </div>
        <!-- Actions footer # end -->
      </div>
    </div>

    <div>
      <button class="sr-only">Focusable fallback</button>
    </div>
  </div>
</template>

Tip:

  • Make sure to set your Unsplash API key in your environment variables.

Released under the MIT License.