Menu

  1. Docs
  2. What To Build
  3. Tarot Reading App

Build a Tarot Reading App

In this tutorial you will build a tarot reading app with three modes: daily card, three-card spread (past/present/future), and a yes/no oracle. One HTML file, no build tools, no frameworks.

What you will build

  • Three tabs for different reading types
  • Card imagery with names, keywords, and interpretations
  • Reversed card handling (cards can appear upside-down, changing their meaning)

The complete code

Create a file called tarot.html and paste this entire block:

<!DOCTYPE html>
<html>
<head>
  <title>Tarot Reading</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px; background: #0f0f1a; color: #e0e0e0; }
    h1 { text-align: center; margin-bottom: 24px; color: #c9a0dc; }
    .tabs { display: flex; gap: 8px; margin-bottom: 24px; justify-content: center; }
    .tabs button {
      padding: 10px 20px; border: 1px solid #333; border-radius: 8px;
      background: #1a1a2e; color: #aaa; cursor: pointer; font-size: 14px;
    }
    .tabs button.active { background: #7c3aed; color: white; border-color: #7c3aed; }
    .cards { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-bottom: 24px; }
    .card {
      background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 16px;
      width: 200px; text-align: center;
    }
    .card img { width: 100%; border-radius: 8px; margin-bottom: 12px; }
    .card img.reversed { transform: rotate(180deg); }
    .card .position { font-size: 12px; text-transform: uppercase; color: #7c3aed; font-weight: 600; margin-bottom: 8px; }
    .card .name { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
    .card .reversed-label { font-size: 11px; color: #e57373; margin-bottom: 4px; }
    .card .keywords { font-size: 12px; color: #999; margin-bottom: 8px; }
    .card .meaning { font-size: 13px; line-height: 1.5; color: #ccc; }
    .answer-box { text-align: center; background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 24px; }
    .answer-box .answer { font-size: 48px; font-weight: 700; color: #c9a0dc; margin-bottom: 8px; }
    .message { text-align: center; font-size: 15px; line-height: 1.6; color: #ccc; padding: 0 20px; margin-bottom: 24px; }
    .draw-btn {
      display: block; width: 100%; max-width: 300px; margin: 0 auto 24px; padding: 14px;
      background: #7c3aed; color: white; border: none; border-radius: 12px;
      font-size: 16px; font-weight: 600; cursor: pointer;
    }
    .draw-btn:hover { background: #6d28d9; }
    .draw-btn:disabled { background: #444; cursor: not-allowed; }
    .loading { text-align: center; padding: 40px; color: #888; }
  </style>
</head>
<body>
  <h1>Tarot Reading</h1>

  <div class="tabs">
    <button class="active" onclick="switchTab('daily')">Daily Card</button>
    <button onclick="switchTab('three-card')">Past / Present / Future</button>
    <button onclick="switchTab('yes-no')">Yes / No</button>
  </div>

  <button class="draw-btn" id="draw-btn" onclick="draw()">Draw Cards</button>
  <div id="result"></div>

  <script>
    const API_KEY = 'YOUR_API_KEY'; // Replace with your key from roxyapi.com/pricing
    let currentTab = 'daily';

    function switchTab(tab) {
      currentTab = tab;
      document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
      event.currentTarget.classList.add('active');
      document.getElementById('result').innerHTML = '';
      document.getElementById('draw-btn').textContent =
        tab === 'daily' ? 'Draw Card' :
        tab === 'three-card' ? 'Draw Three Cards' : 'Ask the Oracle';
    }

    function renderCard(card, position) {
      return `
        <div class="card">
          ${position ? '<div class="position">' + position + '</div>' : ''}
          ${card.imageUrl ? '<img src="' + card.imageUrl + '" alt="' + card.name + '"' + (card.reversed ? ' class="reversed"' : '') + ' />' : ''}
          <div class="name">${card.name}</div>
          ${card.reversed ? '<div class="reversed-label">Reversed</div>' : ''}
          <div class="keywords">${(card.keywords || []).join(' · ')}</div>
          <div class="meaning">${card.meaning || ''}</div>
        </div>
      `;
    }

    async function draw() {
      const btn = document.getElementById('draw-btn');
      btn.disabled = true;
      const el = document.getElementById('result');
      el.innerHTML = '<div class="loading">Shuffling the deck...</div>';

      const endpoints = {
        'daily': '/api/v2/tarot/daily',
        'three-card': '/api/v2/tarot/spreads/three-card',
        'yes-no': '/api/v2/tarot/yes-no'
      };

      const response = await fetch('https://roxyapi.com' + endpoints[currentTab], {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
        body: JSON.stringify({})
      });
      const data = await response.json();
      btn.disabled = false;

      if (currentTab === 'daily') {
        el.innerHTML =
          '<div class="cards">' + renderCard(data.card) + '</div>' +
          '<div class="message">' + data.dailyMessage + '</div>';
      } else if (currentTab === 'three-card') {
        el.innerHTML = '<div class="cards">' +
          data.positions.map(p => renderCard(p.card, p.name)).join('') +
          '</div>';
      } else {
        el.innerHTML =
          '<div class="answer-box"><div class="answer">' + data.answer + '</div></div>' +
          '<div class="cards">' + renderCard(data.card) + '</div>';
      }
    }
  </script>
</body>
</html>

Replace YOUR_API_KEY with your key and double-click the file to open it. Click "Draw Cards" to pull your first reading.

How it works

The app has three modes, each calling a different Roxy endpoint:

Daily CardPOST /api/v2/tarot/daily returns one card with an interpretation. The card object has name, meaning, keywords, imageUrl, and reversed (true/false). The dailyMessage is a ready-made summary.

Three-Card SpreadPOST /api/v2/tarot/spreads/three-card returns three cards in a positions array. Each position has a name (Past, Present, Future), an interpretation, and a card object. The renderCard() function handles displaying each card with its position label.

Yes/No OraclePOST /api/v2/tarot/yes-no returns an answer (Yes, No, or Maybe) along with the card that determined it. The answer box displays the result in large text above the card.

Reversed cards: When card.reversed is true, the card's image is rotated 180 degrees via CSS (transform: rotate(180deg)) and a "Reversed" label appears. Reversed cards have different meanings — the API automatically adjusts the meaning text.

Make it yours

  • Add card flip animations — start with cards face-down and animate them flipping over with a CSS 3D transform on click.
  • Add the Celtic Cross spread — call POST /api/v2/tarot/spreads/celtic-cross for a full 10-card reading. The response has the same positions structure with more cards.
  • Save reading history — store readings in localStorage and let users review past readings.
  • Add a question input — for the Yes/No oracle, let users type their question and display it alongside the answer.

Next steps