Guides / Tutorials

A dog breed selector in Vue 3

By the Dog Image API team. Updated .

A Vue 3 breed selector is one single-file component: load the catalog once in onMounted, filter it with a computed, and bind the chosen slug to an <img>. The component below also emits the selected breed, so a parent form can use it with v-model.

The plan

You need Vue 3.3 or later for defineModel. The component fetches GET /api/v1/breeds, which needs no key, and shows the preview for the selected breed. If you start from scratch:

npm create vue@latest breed-selector
cd breed-selector
npm install

Pick the defaults. TypeScript is optional, and the sample uses plain JavaScript.

The component

Create src/components/BreedSelector.vue:

<script setup>
import { computed, onMounted, ref } from "vue";

const API = "https://dogimage.dev";

// v-model holds the selected breed slug, for example "beagle"
const model = defineModel({ type: String, default: "" });
const props = defineProps({
  view: { type: String, default: "front" },
});

const breeds = ref([]);
const filter = ref("");
const status = ref("loading");

onMounted(async () => {
  try {
    const res = await fetch(`${API}/api/v1/breeds`);
    if (!res.ok) throw new Error(String(res.status));
    breeds.value = (await res.json()).breeds;
    status.value = "ready";
  } catch (err) {
    console.error("Could not load breeds", err);
    status.value = "error";
  }
});

const visible = computed(() => {
  const q = filter.value.trim().toLowerCase();
  return q ? breeds.value.filter((b) => b.name.toLowerCase().includes(q)) : breeds.value;
});

const current = computed(() => breeds.value.find((b) => b.slug === model.value));

const previewSrc = computed(() =>
  model.value
    ? `${API}/api/v1/images/${model.value}/default/${props.view}?size=512`
    : ""
);
</script>

<template>
  <div class="selector">
    <p v-if="status === 'error'" role="alert">The breed list could not be loaded.</p>

    <label>
      Filter
      <input v-model="filter" type="search" placeholder="Start typing a breed" />
    </label>

    <label>
      Breed
      <select v-model="model" :disabled="status !== 'ready'">
        <option value="" disabled>Choose a breed</option>
        <option v-for="b in visible" :key="b.slug" :value="b.slug">
          {{ b.name }}
        </option>
      </select>
    </label>

    <figure v-if="current">
      <img
        :src="previewSrc"
        :alt="`${current.name}, AI-generated illustration`"
        width="256"
        height="256"
      />
      <figcaption>
        <a :href="`${API}/dogs/${current.slug}`">{{ current.name }} image: Dog Image API</a>
      </figcaption>
    </figure>
  </div>
</template>

<style scoped>
.selector { display: grid; gap: 0.75rem; max-width: 22rem; }
label { display: grid; gap: 0.25rem; font-size: 0.9rem; }
img { width: 100%; height: auto; background: #f4f1ea; border-radius: 8px; }
</style>

Use it from src/App.vue:

<script setup>
import { ref } from "vue";
import BreedSelector from "./components/BreedSelector.vue";

const breed = ref("samoyed");
</script>

<template>
  <h1>Your dog</h1>
  <BreedSelector v-model="breed" view="front-right" />
  <p>Selected slug: {{ breed }}</p>
</template>

Run npm run dev. The select starts on Samoyed, and the preview loads in the front three-quarter right view.

Why it is built this way

Filter and select together. A native <select> with about 300 options is awkward to scan, so the filter box narrows the options. When the filter hides the currently chosen breed, v-model still holds its slug, so the picture stays put. Reset the filter when you want a clean list.

defineModel keeps the parent simple. The parent owns the slug. The child reads and writes it through the model, with no extra emit code. On Vue 3.2 and older, use a modelValue prop and update:modelValue emit instead.

One request, one catalog. The response lists every breed with its variants. Do not request each breed separately for a picker. The single-breed endpoint, GET /api/v1/breeds/{breed}, is for pages that need the summary text or the full list of preview URLs.

Add a view switcher

The view prop takes any of the nine view ids: front, front-left, left, rear-left, rear, rear-right, right, front-right or head. A small <select> in the parent that sets a ref and passes it as :view is enough to let people rotate the dog. The catalog's views array carries the human labels if you want to build that list from data instead of typing it.

Colors and variants

Seven breeds ship in several colors, among them Poodle and Dachshund. Each entry in variants has a slug, a name and a kind of color, pattern or coat. To preview a variant, replace default in the URL with its slug, for example /api/v1/images/poodle/<variant>/front?size=512, where <variant> comes from the catalog. For the breeds that have only one set, default is the only value you need.

Licensing and limits

The previews are free, including in commercial products, if a visible link credits Dog Image API on the same page. The figcaption link covers that. The images are AI-generated illustrations, not photos, so label them that way if your users might think otherwise.

This component uses no key and should not get one. For full-size downloads, see the Next.js guide, which does them server side. The full list of endpoints is in the docs, and every breed page, such as Samoyed, shows the exact credit snippet.

Questions

Can I use this selector in a Nuxt app?

The component is plain Vue 3, so it works in Nuxt. Because it fetches in the browser, wrap it in a client-only boundary if you want to avoid fetching during server rendering.

How do I show a color variant such as a blue French Bulldog?

Each breed in the catalog lists its variants with a slug. Use the variant slug in the image URL in place of the word default, for example french-bulldog/blue/front.

Is there a Vue package for Dog Image API?

No. There is no SDK. The API is plain HTTP and JSON, so fetch is all you need.

Next steps