Freedom Sale
Independence Day Special — Unlock the AI Path 70% off our most popular AI course · Limited time offer
--Days
--Hrs
--Min
--Sec
Claim Your Discount
✦ Beginner ⏱ 30 min

🍳 Build a Recipe Finder Web App

🎯 What You'll Build

A recipe finder that searches TheMealDB by ingredient or name, shows recipe cards with photos, and opens a modal with full cooking instructions.

📋 What You'll Need

1

Fetch recipes from TheMealDB API

TheMealDB is completely free and requires no API key for the basic endpoints.

// Search by meal name
async function searchByName(query) {
  const res  = await fetch(`https://www.themealdb.com/api/json/v1/1/search.php?s=${encodeURIComponent(query)}`);
  const data = await res.json();
  return data.meals || [];
}

// Search by main ingredient
async function searchByIngredient(ingredient) {
  const res  = await fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?i=${encodeURIComponent(ingredient)}`);
  const data = await res.json();
  return data.meals || [];
}

// Get full recipe details by ID
async function getMealById(id) {
  const res  = await fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${id}`);
  const data = await res.json();
  return data.meals ? data.meals[0] : null;
}

// Test
searchByName('pasta').then(meals => {
  console.log(`Found ${meals.length} pasta recipes`);
  console.log(meals[0]?.strMeal, meals[0]?.strCategory);
});
2

Full HTML, CSS, and JavaScript app

Search bar, recipe grid, and a modal for full instructions.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Recipe Finder</title>
  <style>
    * { margin:0; padding:0; box-sizing:border-box; }
    body { font-family:'Segoe UI',sans-serif; background:#0f172a; color:#f8fafc; min-height:100vh; padding:28px 20px; }
    h1 { text-align:center; font-size:1.8rem; margin-bottom:8px; }
    .subtitle { text-align:center; color:#64748b; margin-bottom:24px; }
    .search-bar { display:flex; gap:10px; max-width:520px; margin:0 auto 32px; }
    .search-bar input { flex:1; padding:12px 18px; background:#1e293b; border:1px solid #334155; border-radius:10px; color:#fff; font-size:0.95rem; }
    .search-bar input:focus { outline:none; border-color:#3b82f6; }
    .search-bar button { padding:12px 20px; background:#3b82f6; border:none; border-radius:10px; color:#fff; font-weight:700; cursor:pointer; }
    .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:20px; max-width:1100px; margin:0 auto; }
    .card { background:#1e293b; border-radius:14px; overflow:hidden; cursor:pointer; transition:transform 0.2s,box-shadow 0.2s; }
    .card:hover { transform:translateY(-4px); box-shadow:0 8px 24px rgba(0,0,0,0.4); }
    .card img { width:100%; aspect-ratio:4/3; object-fit:cover; }
    .card-body { padding:14px; }
    .card-title { font-weight:700; font-size:0.9rem; margin-bottom:4px; }
    .card-cat { color:#64748b; font-size:0.78rem; }
    .modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:100; overflow-y:auto; padding:20px; }
    .modal-overlay.open { display:flex; align-items:flex-start; justify-content:center; }
    .modal { background:#1e293b; border-radius:16px; max-width:660px; width:100%; padding:28px; position:relative; }
    .modal-close { position:absolute; top:16px; right:16px; background:#334155; border:none; color:#fff; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:1.1rem; }
    .modal img { width:100%; border-radius:10px; margin-bottom:16px; }
    .modal h2 { font-size:1.3rem; margin-bottom:6px; }
    .modal .tags { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; }
    .tag { background:#0f172a; color:#94a3b8; font-size:0.75rem; padding:3px 10px; border-radius:20px; }
    .ingredients { display:grid; grid-template-columns:1fr 1fr; gap:4px; margin-bottom:16px; font-size:0.85rem; }
    .ingredient { background:#0f172a; padding:6px 10px; border-radius:8px; color:#cbd5e1; }
    .instructions { color:#cbd5e1; font-size:0.9rem; line-height:1.7; white-space:pre-line; }
    #status { text-align:center; color:#64748b; margin:40px 0; }
  </style>
</head>
<body>

<?php require_once __DIR__ . '/../promo_banner.php'; ?>

  <h1>🍳 Recipe Finder</h1>
  <p class="subtitle">Search by meal name or ingredient</p>
  <div class="search-bar">
    <input type="text" id="query" placeholder="e.g. chicken, pasta, sushi..." onkeydown="if(event.key==='Enter')search()">
    <button onclick="search()">Search</button>
  </div>
  <div id="status">Search for a recipe to get started.</div>
  <div class="grid" id="grid"></div>

  <div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
    <div class="modal">
      <button class="modal-close" onclick="closeModal()">×</button>
      <img id="m-img" src="" alt="">
      <h2 id="m-title"></h2>
      <div class="tags" id="m-tags"></div>
      <h3 style="margin-bottom:10px">Ingredients</h3>
      <div class="ingredients" id="m-ingredients"></div>
      <h3 style="margin:16px 0 10px">Instructions</h3>
      <div class="instructions" id="m-instructions"></div>
    </div>
  </div>

  <script src="app.js"></script>
</body>
</html>
3

JavaScript: search, render cards, and open modal

Fetch results, build cards, and load full details when a card is clicked.

// app.js
async function search() {
  const q    = document.getElementById('query').value.trim();
  if (!q) return;
  const grid = document.getElementById('grid');
  const status = document.getElementById('status');
  grid.innerHTML = '';
  status.textContent = 'Searching...';

  let meals = await searchByName(q);
  if (!meals.length) meals = await searchByIngredient(q);

  if (!meals.length) {
    status.textContent = 'No recipes found. Try a different search.'; return;
  }

  status.textContent = `Found ${meals.length} recipe(s)`;
  meals.forEach(meal => {
    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `<img src="${meal.strMealThumb}/preview" alt="${meal.strMeal}" loading="lazy">
      <div class="card-body"><div class="card-title">${meal.strMeal}</div>
      <div class="card-cat">${meal.strCategory || ''} ${meal.strArea ? '· ' + meal.strArea : ''}</div></div>`;
    card.onclick = () => openModal(meal.idMeal);
    grid.appendChild(card);
  });
}

async function openModal(id) {
  const meal = await getMealById(id);
  if (!meal) return;

  document.getElementById('m-img').src   = meal.strMealThumb;
  document.getElementById('m-title').textContent = meal.strMeal;
  document.getElementById('m-tags').innerHTML =
    [meal.strCategory, meal.strArea, meal.strTags].filter(Boolean)
    .flatMap(t => t.split(',')).map(t => `<span class="tag">${t.trim()}</span>`).join('');

  // Extract up to 20 ingredients
  const ingList = document.getElementById('m-ingredients');
  ingList.innerHTML = '';
  for (let i = 1; i <= 20; i++) {
    const ing = meal[`strIngredient${i}`]; const meas = meal[`strMeasure${i}`];
    if (ing && ing.trim()) {
      ingList.innerHTML += `<div class="ingredient"><b>${meas ? meas.trim() : ''}</b> ${ing.trim()}</div>`;
    }
  }

  document.getElementById('m-instructions').textContent = meal.strInstructions;
  document.getElementById('modal').classList.add('open');
}

function closeModal() {
  document.getElementById('modal').classList.remove('open');
}
💡 Tip: TheMealDB returns a thumbnail URL like https://www.themealdb.com/images/media/meals/abc.jpg — append /preview to get a smaller 240x240 version, which loads much faster in the grid view.

🎉 You Did It!

You built a full API-powered search app with a modal dialog. The same pattern — search -> render grid -> open detail view — is how every e-commerce site, app store, and content platform works.

Found something wrong?

Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.