Guides / Tutorials
Compare two dog breeds side by side
By the Dog Image API team. Updated .
Two selects, one set of view buttons and two images in a row: that is a breed comparison. This guide gives you a single HTML file with plain JavaScript. Choose two breeds, choose an angle, and both dogs appear in that same view side by side.
Read this first: shape, not size
Each dog fills its own frame. Every image is a square canvas where the dog takes up at most 80 percent, centered, whatever the breed. A Great Dane and a Chihuahua are therefore drawn at about the same size on screen. The comparison shows shape and proportions: leg length against body, muzzle length, ear set, coat outline. It does not show relative size.
If you want to tell readers which breed is bigger, write that in text from a source you trust. Do not let the pictures imply it. The images are also AI-generated illustrations, not photos of real dogs.
The whole page
Save this as compare.html and open it in a browser:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Compare two dog breeds</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; background: #faf8f3; color: #222; }
main { max-width: 860px; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
.pickers, .views { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1rem 0; }
label { display: grid; gap: 0.25rem; font-size: 0.875rem; flex: 1 1 200px; }
select { padding: 0.5rem; font-size: 1rem; }
.views button { padding: 0.4rem 0.7rem; border: 1px solid #bbb; background: #fff; border-radius: 6px; cursor: pointer; }
.views button[aria-pressed="true"] { background: #222; color: #fff; border-color: #222; }
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
figure { margin: 0; text-align: center; }
.stage { aspect-ratio: 1; background: #efe9dc; border-radius: 10px; overflow: hidden; }
.stage img { width: 100%; height: 100%; object-fit: contain; display: block; }
figcaption { font-size: 0.875rem; margin-top: 0.4rem; }
.note { font-size: 0.875rem; color: #555; margin-top: 1rem; }
</style>
</head>
<body>
<main>
<h1>Compare two dog breeds</h1>
<div class="pickers">
<label>Left breed <select id="breed-a"></select></label>
<label>Right breed <select id="breed-b"></select></label>
</div>
<div class="views" id="views" role="group" aria-label="View"></div>
<p id="error" role="alert" hidden></p>
<div class="pair" aria-live="polite">
<figure>
<div class="stage"><img id="img-a" width="512" height="512" alt="" /></div>
<figcaption><a id="credit-a" href="#"></a></figcaption>
</figure>
<figure>
<div class="stage"><img id="img-b" width="512" height="512" alt="" /></div>
<figcaption><a id="credit-b" href="#"></a></figcaption>
</figure>
</div>
<p class="note">
Each dog fills its own frame, so this shows shape and proportions, not
relative size. Images are AI-generated illustrations.
</p>
</main>
<script>
const API = "https://dogimage.dev";
const state = { breeds: [], views: [], view: "front-left" };
const selA = document.getElementById("breed-a");
const selB = document.getElementById("breed-b");
const viewsEl = document.getElementById("views");
const errorEl = document.getElementById("error");
function fillSelect(select, breeds, value) {
select.replaceChildren(
...breeds.map((b) => new Option(b.name, b.slug, false, b.slug === value))
);
}
function show(side, select) {
const breed = state.breeds.find((b) => b.slug === select.value);
if (!breed) return;
const view = state.views.find((v) => v.id === state.view);
const img = document.getElementById("img-" + side);
img.src = `${API}/api/v1/images/${breed.slug}/default/${state.view}?size=512`;
img.alt = `${breed.name}, ${view ? view.label : state.view}, AI-generated illustration`;
const credit = document.getElementById("credit-" + side);
credit.href = `${API}/dogs/${breed.slug}`;
credit.textContent = `${breed.name} image: Dog Image API`;
}
function showBoth() {
show("a", selA);
show("b", selB);
}
function renderViews() {
viewsEl.replaceChildren(
...state.views.map((v) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = v.label;
button.setAttribute("aria-pressed", String(v.id === state.view));
button.addEventListener("click", () => {
state.view = v.id;
renderViews();
showBoth();
});
return button;
})
);
}
async function init() {
try {
const res = await fetch(`${API}/api/v1/breeds`);
if (!res.ok) throw new Error(`Breed list failed with status ${res.status}`);
const data = await res.json();
state.breeds = data.breeds;
state.views = data.views;
} catch (err) {
errorEl.textContent = err.message;
errorEl.hidden = false;
return;
}
const has = (slug) => state.breeds.some((b) => b.slug === slug);
const first = has("pug") ? "pug" : state.breeds[0].slug;
const second = has("samoyed") ? "samoyed" : state.breeds[state.breeds.length - 1].slug;
fillSelect(selA, state.breeds, first);
fillSelect(selB, state.breeds, second);
renderViews();
showBoth();
selA.addEventListener("change", () => show("a", selA));
selB.addEventListener("change", () => show("b", selB));
}
init();
</script>
</body>
</html>
How it works
Load the catalog once. GET /api/v1/breeds returns views (id and label) and breeds (slug, name and variants). It needs no key, and the API allows cross-origin reads, so a page opened from your disk or any other site can call it. The code stores both lists in one state object.
Build both selects and the view buttons from the data. Nothing is hardcoded except two starting breeds, and the code falls back to the first and last breed if those slugs are ever missing. The view buttons come from the views array, so their labels match the catalog: Front; Front three-quarter, left; Left side; and so on.
One URL pattern draws both dogs. For a slug and a view id:
https://dogimage.dev/api/v1/images/{slug}/default/{view}?size=512
The default word means the breed's main set. The address redirects to a transparent WebP, which an <img> follows. Changing the view just changes the last path segment on both images, so the two dogs always show the same angle.
The stage keeps frames equal. Both .stage boxes use aspect-ratio: 1 and object-fit: contain, so the two frames are identical squares. That is a feature for comparing outlines, and it is the reason the page cannot show size.
Using it well
The standing views share one ground line, so left and right are good for comparing back length, leg length and head carriage. front and rear show width and chest. head compares muzzle shape and ear set, and it is not a standing pose. The left side gallery shows every breed from one angle if you want to scan many at once.
Pairs with a similar outline, such as two spaniels, read clearly. Pairs that differ a lot in coat volume, such as a Samoyed and a Dachshund, compare mainly by silhouette.
Credit and next steps
Both captions link to the breed pages, which is the credit the free license asks for. It must be visible next to the image or on the same page. Read the license before you use the page in a product. The endpoints are in the docs.
For a version that lets people search a long list instead of scrolling a select, see the autocomplete guide.
Questions
Why do the two dogs look the same size?
Each dog is generated to fill its own square frame, up to 80 percent of it. The images do not carry real-world scale, so a comparison shows shape and proportions only.
Can I compare a breed's colors, not just two breeds?
Yes. Breeds with several colors list them as variants in the catalog. Swap the word default in the image URL for a variant slug such as fawn or black.
Do I need a server to run the page?
No. Save it as an HTML file and open it in a browser. The breed list and previews are keyless and allow cross-origin reads, so the page can fetch them directly.
Next steps
- API docs: endpoints, keys, limits and errors.
- All dog breeds, each with nine views.
- Pricing for full-size transparent PNG files.
- A dog breed card with Tailwind CSS
- A dog breed gallery in SvelteKit
- A dog breed picker in Angular
- A dog breed selector in Vue 3