Tutorial: Build Your First BiModal Site

A hands-on guide to building a product page that works for humans and AI agents

In this tutorial, you'll build a complete product page from scratch using BiModal Design principles. We'll start with semantic HTML, add accessibility features, include structured data, and progressively enhance with CSS and JavaScript. By the end, you'll have a page that works perfectly for both human visitors and AI agents.

What We're Building

A product page for "Premium Wireless Headphones" with:

For Humans

  • ✓ Beautiful, responsive design
  • ✓ Interactive image gallery
  • ✓ Add to cart functionality
  • ✓ Customer reviews
  • ✓ Full keyboard navigation
  • ✓ Screen reader accessible

For Agents

  • ✓ Semantic HTML structure
  • ✓ Schema.org Product markup
  • ✓ Clear pricing and availability
  • ✓ Extractable specifications
  • ✓ Purchase action metadata
  • ✓ Works without JavaScript

Prerequisites

  • • Basic HTML, CSS, and JavaScript knowledge
  • • A text editor (VS Code, Sublime, etc.)
  • • A modern web browser
  • • 30-45 minutes of time
1

HTML Foundation

Let's start with the HTML structure. This is the most important step—everything builds on this foundation.

product.htmlCopy this code
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Premium Wireless Headphones - AudioTech</title>
  <meta name="description" content="Premium noise-canceling wireless headphones with 30-hour battery life">
</head>
<body>
  <!-- Skip to main content link -->
  <a href="#main" class="skip-link">Skip to main content</a>

  <!-- Header with navigation -->
  <header>
    <nav aria-label="Main navigation">
      <a href="/">
        <img src="/logo.svg" alt="AudioTech Home" width="120" height="40">
      </a>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/products" aria-current="page">Products</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <!-- Breadcrumb navigation -->
  <nav aria-label="Breadcrumb">
    <ol>
      <li><a href="/">Home</a></li>
      <li><a href="/products">Products</a></li>
      <li><a href="/products/headphones">Headphones</a></li>
      <li aria-current="page">Premium Wireless Headphones</li>
    </ol>
  </nav>

  <!-- Main content -->
  <main id="main">
    <article>
      <!-- Product heading -->
      <h1>Premium Wireless Headphones</h1>

      <!-- Product images -->
      <section aria-label="Product images">
        <img
          src="/headphones-front.jpg"
          alt="Premium wireless headphones, front view showing cushioned ear cups"
          width="600"
          height="600"
        >
        <img
          src="/headphones-side.jpg"
          alt="Side view showing adjustable headband"
          width="600"
          height="600"
        >
      </section>

      <!-- Product details -->
      <section aria-labelledby="details-heading">
        <h2 id="details-heading">Product Details</h2>

        <p>
          Experience studio-quality audio with our Premium Wireless Headphones.
          Featuring active noise cancellation, 30-hour battery life, and
          premium comfort for all-day wear.
        </p>

        <!-- Pricing -->
        <div>
          <p>
            <strong>Price:</strong>
            <span>$299.99</span>
          </p>
          <p>
            <strong>Availability:</strong>
            <span>In Stock</span>
          </p>
        </div>

        <!-- Specifications -->
        <h3>Specifications</h3>
        <dl>
          <dt>Battery Life</dt>
          <dd>30 hours</dd>

          <dt>Connectivity</dt>
          <dd>Bluetooth 5.0</dd>

          <dt>Weight</dt>
          <dd>250g</dd>

          <dt>Noise Cancellation</dt>
          <dd>Active (ANC)</dd>
        </dl>

        <!-- Add to cart form -->
        <form action="/cart/add" method="POST">
          <input type="hidden" name="product-id" value="headphones-001">

          <label for="quantity">Quantity</label>
          <input
            type="number"
            id="quantity"
            name="quantity"
            min="1"
            max="10"
            value="1"
            required
          >

          <button type="submit">Add to Cart</button>
        </form>
      </section>

      <!-- Customer reviews -->
      <section aria-labelledby="reviews-heading">
        <h2 id="reviews-heading">Customer Reviews</h2>

        <p>
          <strong>Average Rating:</strong> 4.5 out of 5
          (based on 142 reviews)
        </p>

        <article>
          <h3>Excellent sound quality</h3>
          <p>
            <strong>Rating:</strong> 5 out of 5
          </p>
          <p>
            <strong>By:</strong> Sarah J.
          </p>
          <p>
            <strong>Date:</strong> <time datetime="2024-12-15">December 15, 2024</time>
          </p>
          <p>
            The noise cancellation is incredible and the battery lasts
            forever. Best headphones I've ever owned!
          </p>
        </article>

        <article>
          <h3>Very comfortable</h3>
          <p>
            <strong>Rating:</strong> 4 out of 5
          </p>
          <p>
            <strong>By:</strong> Mike T.
          </p>
          <p>
            <strong>Date:</strong> <time datetime="2024-12-10">December 10, 2024</time>
          </p>
          <p>
            I wear these all day for work. Super comfortable and great sound.
          </p>
        </article>
      </section>
    </article>
  </main>

  <!-- Footer -->
  <footer>
    <p>&copy; 2025 AudioTech. All rights reserved.</p>
    <nav aria-label="Footer navigation">
      <ul>
        <li><a href="/privacy">Privacy Policy</a></li>
        <li><a href="/terms">Terms of Service</a></li>
      </ul>
    </nav>
  </footer>
</body>
</html>

✓ What We Accomplished

  • • Semantic HTML structure (header, nav, main, article, footer)
  • • Proper heading hierarchy (h1 → h2 → h3)
  • • Skip link for keyboard users
  • • Breadcrumb navigation
  • • Accessible form with labels
  • • Descriptive alt text for images
2

Add ARIA Attributes

Now let's enhance our HTML with ARIA attributes to provide additional context for assistive technologies and agents.

Update these sections in your HTML:
<!-- Navigation with current page indicator -->
<nav aria-label="Main navigation">
  <a href="/">
    <img src="/logo.svg" alt="AudioTech Home" width="120" height="40">
  </a>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/products" aria-current="page">Products</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

<!-- Breadcrumb with proper structure -->
<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/products/headphones">Headphones</a></li>
    <li aria-current="page">Premium Wireless Headphones</li>
  </ol>
</nav>

<!-- Form with enhanced accessibility -->
<form action="/cart/add" method="POST" aria-label="Add to cart">
  <input type="hidden" name="product-id" value="headphones-001">

  <label for="quantity">Quantity</label>
  <input
    type="number"
    id="quantity"
    name="quantity"
    min="1"
    max="10"
    value="1"
    required
    aria-required="true"
    aria-describedby="quantity-hint"
  >
  <p id="quantity-hint">Choose between 1 and 10</p>

  <button type="submit" aria-label="Add Premium Wireless Headphones to cart">
    Add to Cart
  </button>
</form>

<!-- Reviews with better structure -->
<section aria-labelledby="reviews-heading">
  <h2 id="reviews-heading">Customer Reviews</h2>

  <div aria-label="Average rating">
    <p>
      <strong>Average Rating:</strong>
      <span aria-label="4.5 out of 5 stars">4.5 out of 5</span>
      (based on 142 reviews)
    </p>
  </div>

  <article aria-labelledby="review-1-title">
    <h3 id="review-1-title">Excellent sound quality</h3>
    <p aria-label="5 out of 5 stars">
      <strong>Rating:</strong> 5 out of 5
    </p>
    <p>
      <strong>By:</strong> Sarah J.
    </p>
    <p>
      <strong>Date:</strong> <time datetime="2024-12-15">December 15, 2024</time>
    </p>
    <p>
      The noise cancellation is incredible and the battery lasts
      forever. Best headphones I've ever owned!
    </p>
  </article>
</section>

✓ What We Accomplished

  • • Added aria-label for navigation regions
  • • Used aria-current for active pages
  • • Enhanced form with aria-required and aria-describedby
  • • Improved screen reader announcements for ratings
3

Add Structured Data

Add Schema.org JSON-LD to make your product data explicitly machine-readable.

Add this to your <head> section:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Premium Wireless Headphones",
  "description": "Premium noise-canceling wireless headphones with 30-hour battery life",
  "image": [
    "https://example.com/headphones-front.jpg",
    "https://example.com/headphones-side.jpg"
  ],
  "brand": {
    "@type": "Brand",
    "name": "AudioTech"
  },
  "sku": "headphones-001",
  "offers": {
    "@type": "Offer",
    "price": "299.99",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "url": "https://example.com/products/headphones",
    "seller": {
      "@type": "Organization",
      "name": "AudioTech"
    }
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.5",
    "reviewCount": "142",
    "bestRating": "5",
    "worstRating": "1"
  },
  "review": [
    {
      "@type": "Review",
      "reviewRating": {
        "@type": "Rating",
        "ratingValue": "5",
        "bestRating": "5"
      },
      "author": {
        "@type": "Person",
        "name": "Sarah J."
      },
      "datePublished": "2024-12-15",
      "reviewBody": "The noise cancellation is incredible and the battery lasts forever. Best headphones I've ever owned!",
      "name": "Excellent sound quality"
    },
    {
      "@type": "Review",
      "reviewRating": {
        "@type": "Rating",
        "ratingValue": "4",
        "bestRating": "5"
      },
      "author": {
        "@type": "Person",
        "name": "Mike T."
      },
      "datePublished": "2024-12-10",
      "reviewBody": "I wear these all day for work. Super comfortable and great sound.",
      "name": "Very comfortable"
    }
  ],
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "Battery Life",
      "value": "30 hours"
    },
    {
      "@type": "PropertyValue",
      "name": "Connectivity",
      "value": "Bluetooth 5.0"
    },
    {
      "@type": "PropertyValue",
      "name": "Weight",
      "value": "250g"
    },
    {
      "@type": "PropertyValue",
      "name": "Noise Cancellation",
      "value": "Active (ANC)"
    }
  ]
}
</script>

💡 Test Your Structured Data

Validate your Schema.org markup with Google's Rich Results Test:

Test with Google →

✓ What We Accomplished

  • • Product details in machine-readable format
  • • Price and availability explicitly stated
  • • Reviews structured with ratings
  • • Specifications as PropertyValue objects
  • • Agents can now extract all product data
4

Add Agent Attributes

Add custom data attributes to help agents understand actions and extract data easily.

Update these sections:
<!-- Product article with entity type -->
<article
  data-entity-type="product"
  data-product-id="headphones-001"
>
  <h1 data-field="name">Premium Wireless Headphones</h1>

  <!-- Pricing with data attributes -->
  <div data-entity-type="pricing">
    <p>
      <strong>Price:</strong>
      <span
        data-field="price"
        data-amount="299.99"
        data-currency="USD"
      >
        $299.99
      </span>
    </p>
    <p>
      <strong>Availability:</strong>
      <span
        data-field="availability"
        data-status="in-stock"
      >
        In Stock
      </span>
    </p>
  </div>

  <!-- Specifications with data fields -->
  <h3>Specifications</h3>
  <dl data-entity-type="specifications">
    <dt>Battery Life</dt>
    <dd data-field="battery-life" data-value="30" data-unit="hours">
      30 hours
    </dd>

    <dt>Connectivity</dt>
    <dd data-field="connectivity" data-value="Bluetooth 5.0">
      Bluetooth 5.0
    </dd>

    <dt>Weight</dt>
    <dd data-field="weight" data-value="250" data-unit="g">
      250g
    </dd>

    <dt>Noise Cancellation</dt>
    <dd data-field="noise-cancellation" data-value="Active">
      Active (ANC)
    </dd>
  </dl>

  <!-- Add to cart form with action intent -->
  <form
    action="/cart/add"
    method="POST"
    data-action="add-to-cart"
    data-intent="transact"
    data-requires-auth="false"
  >
    <input type="hidden" name="product-id" value="headphones-001">

    <label for="quantity">Quantity</label>
    <input
      type="number"
      id="quantity"
      name="quantity"
      min="1"
      max="10"
      value="1"
      required
      aria-required="true"
      data-field="quantity"
    >

    <button
      type="submit"
      data-action="add-to-cart"
      data-product-id="headphones-001"
    >
      Add to Cart
    </button>
  </form>

  <!-- Reviews with rating data -->
  <section aria-labelledby="reviews-heading">
    <h2 id="reviews-heading">Customer Reviews</h2>

    <div
      data-entity-type="aggregate-rating"
      data-rating-value="4.5"
      data-review-count="142"
    >
      <p>
        <strong>Average Rating:</strong>
        <span data-field="rating">4.5 out of 5</span>
        (based on <span data-field="review-count">142</span> reviews)
      </p>
    </div>

    <article
      data-entity-type="review"
      data-rating="5"
    >
      <h3 data-field="review-title">Excellent sound quality</h3>
      <p>
        <strong>Rating:</strong>
        <span data-field="rating" data-value="5">5 out of 5</span>
      </p>
      <p>
        <strong>By:</strong>
        <span data-field="author">Sarah J.</span>
      </p>
      <p>
        <strong>Date:</strong>
        <time datetime="2024-12-15" data-field="date">
          December 15, 2024
        </time>
      </p>
      <p data-field="review-body">
        The noise cancellation is incredible and the battery lasts
        forever. Best headphones I've ever owned!
      </p>
    </article>
  </section>
</article>

✓ What We Accomplished

  • • Product entity clearly identified
  • • All key data points have data-field attributes
  • • Form action and intent explicitly marked
  • • Specifications are easily extractable
  • • Agents can programmatically access all data
5

Add CSS Styling

Now let's make it beautiful with CSS. Remember: the site already works; CSS only enhances the presentation.

Add this in your <head> or external stylesheet:
<style>
/* Reset and base styles */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.6;
  color: #333;
  background: #fff;
}

/* Skip link */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
  text-decoration: none;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

/* Header and navigation */
header {
  background: #fff;
  border-bottom: 1px solid #e5e7eb;
  padding: 1rem 2rem;
}

nav[aria-label="Main navigation"] {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: 1200px;
  margin: 0 auto;
}

nav ul {
  display: flex;
  gap: 2rem;
  list-style: none;
}

nav a {
  text-decoration: none;
  color: #374151;
  font-weight: 500;
  transition: color 0.2s;
}

nav a:hover,
nav a:focus {
  color: #10b981;
}

nav a[aria-current="page"] {
  color: #10b981;
  font-weight: 600;
}

/* Breadcrumb */
nav[aria-label="Breadcrumb"] {
  max-width: 1200px;
  margin: 1rem auto;
  padding: 0 2rem;
}

nav[aria-label="Breadcrumb"] ol {
  display: flex;
  gap: 0.5rem;
  list-style: none;
  flex-wrap: wrap;
}

nav[aria-label="Breadcrumb"] li:not(:last-child)::after {
  content: "›";
  margin-left: 0.5rem;
  color: #9ca3af;
}

nav[aria-label="Breadcrumb"] a {
  color: #6b7280;
  text-decoration: none;
}

nav[aria-label="Breadcrumb"] a:hover {
  color: #10b981;
}

/* Main content */
main {
  max-width: 1200px;
  margin: 2rem auto;
  padding: 0 2rem;
}

article {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
}

/* Product images */
section[aria-label="Product images"] {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

section[aria-label="Product images"] img {
  width: 100%;
  height: auto;
  border-radius: 8px;
  border: 1px solid #e5e7eb;
}

/* Product details */
h1 {
  font-size: 2rem;
  font-weight: 700;
  margin-bottom: 1rem;
  color: #111827;
}

h2 {
  font-size: 1.5rem;
  font-weight: 600;
  margin: 2rem 0 1rem;
  color: #111827;
}

h3 {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 1.5rem 0 0.75rem;
  color: #111827;
}

p {
  margin-bottom: 1rem;
  color: #4b5563;
}

/* Pricing */
[data-entity-type="pricing"] {
  background: #f9fafb;
  padding: 1.5rem;
  border-radius: 8px;
  margin: 1rem 0;
}

[data-entity-type="pricing"] p {
  margin: 0.5rem 0;
  font-size: 1.125rem;
}

[data-field="price"] {
  color: #10b981;
  font-size: 2rem;
  font-weight: 700;
}

[data-status="in-stock"] {
  color: #10b981;
  font-weight: 600;
}

/* Specifications */
dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.75rem 1.5rem;
  margin: 1rem 0;
}

dt {
  font-weight: 600;
  color: #374151;
}

dd {
  color: #6b7280;
}

/* Form */
form {
  margin: 2rem 0;
  padding: 1.5rem;
  background: #fff;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: #374151;
}

input[type="number"] {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  font-size: 1rem;
  margin-bottom: 1rem;
}

input:focus {
  outline: 2px solid #10b981;
  outline-offset: 2px;
}

button {
  width: 100%;
  padding: 1rem;
  background: #10b981;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1.125rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

button:hover {
  background: #059669;
}

button:focus {
  outline: 2px solid #10b981;
  outline-offset: 2px;
}

/* Reviews */
article[data-entity-type="review"] {
  display: block;
  background: #f9fafb;
  padding: 1.5rem;
  border-radius: 8px;
  margin: 1rem 0;
  border: 1px solid #e5e7eb;
}

/* Footer */
footer {
  background: #111827;
  color: #fff;
  padding: 2rem;
  margin-top: 4rem;
  text-align: center;
}

footer nav ul {
  display: flex;
  gap: 2rem;
  justify-content: center;
  list-style: none;
  margin-top: 1rem;
}

footer a {
  color: #9ca3af;
  text-decoration: none;
}

footer a:hover {
  color: #fff;
}

/* Responsive */
@media (max-width: 768px) {
  article {
    grid-template-columns: 1fr;
  }

  nav ul {
    flex-direction: column;
    gap: 1rem;
  }
}
</style>

✓ What We Accomplished

  • • Beautiful, modern design
  • • Responsive layout for all screen sizes
  • • Clear visual hierarchy
  • • Visible focus indicators
  • • Site still works if CSS fails to load
6

Add JavaScript Interactivity

Finally, let's add JavaScript to enhance the user experience. The site already works—this just makes it better.

Add before closing </body> tag:
<script>
// Image gallery enhancement
const images = document.querySelectorAll('[aria-label="Product images"] img');
let currentImage = 0;

images.forEach((img, index) => {
  img.style.cursor = 'pointer';
  img.addEventListener('click', () => {
    // Could open a lightbox or zoom
    console.log('Image clicked:', index);
  });
});

// Add to cart form enhancement
const form = document.querySelector('form[data-action="add-to-cart"]');
const button = form.querySelector('button');
const quantityInput = form.querySelector('#quantity');

// Real-time quantity validation
quantityInput.addEventListener('input', () => {
  const value = parseInt(quantityInput.value);
  const min = parseInt(quantityInput.min);
  const max = parseInt(quantityInput.max);

  if (value < min || value > max || isNaN(value)) {
    quantityInput.setAttribute('aria-invalid', 'true');
  } else {
    quantityInput.setAttribute('aria-invalid', 'false');
  }
});

// AJAX form submission
form.addEventListener('submit', async (e) => {
  e.preventDefault();

  // Show loading state
  const originalText = button.textContent;
  button.textContent = 'Adding...';
  button.disabled = true;

  try {
    const formData = new FormData(form);
    const response = await fetch(form.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      // Show success message
      button.textContent = '✓ Added to Cart!';
      button.style.background = '#10b981';

      // Reset after 2 seconds
      setTimeout(() => {
        button.textContent = originalText;
        button.disabled = false;
      }, 2000);

      // Could also update cart count in header
      console.log('Added to cart successfully');
    } else {
      throw new Error('Failed to add to cart');
    }
  } catch (error) {
    console.error('Error:', error);

    // Fallback to standard form submission
    button.textContent = originalText;
    button.disabled = false;
    form.submit();
  }
});

// Smooth scroll for skip link
document.querySelector('.skip-link').addEventListener('click', (e) => {
  e.preventDefault();
  document.querySelector('#main').scrollIntoView({
    behavior: 'smooth'
  });
  document.querySelector('#main').focus();
});

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
  // Press 'C' to focus on cart button
  if (e.key === 'c' && !e.ctrlKey && !e.metaKey) {
    const target = e.target;
    if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') {
      button.focus();
      e.preventDefault();
    }
  }
});
</script>

✓ What We Accomplished

  • • AJAX form submission (faster, no page reload)
  • • Real-time quantity validation
  • • Smooth scrolling for skip link
  • • Keyboard shortcuts
  • • Fallback to standard form if JS fails
  • • Site still works without JavaScript
7

Test Your Page

Now let's verify everything works for both humans and agents.

✓ Accessibility Testing

  • Keyboard Navigation: Unplug your mouse and navigate using only Tab, Enter, and Space
  • Screen Reader: Turn on VoiceOver (Mac) or NVDA (Windows) and navigate the page
  • Color Contrast: Use the WAVE tool to check contrast ratios
  • Focus Indicators: Verify all interactive elements show focus

✓ Agent Testing

  • The Curl Test: Run curl https://yoursite.com and verify all content is visible
  • Disable JavaScript: Turn off JS in DevTools and verify the form still works
  • Schema Validator: Test with Google Rich Results Test
  • Data Extraction: Use console to test document.querySelector('[data-field="price"]')

✓ Performance Testing

  • Lighthouse: Run in Chrome DevTools (aim for 90+ in all categories)
  • Page Load: Verify content visible in under 2 seconds
  • Mobile: Test on actual mobile devices

✓ Automated Testing

  • axe DevTools: Browser extension for accessibility issues
  • HTML Validator: Check for valid HTML at validator.w3.org
  • Schema Validator: Verify JSON-LD at validator.schema.org

🎉 Congratulations!

You've built a complete BiModal product page that works for both humans and AI agents!

What You've Accomplished:

  • ✓ Semantic HTML structure
  • ✓ Full keyboard accessibility
  • ✓ Screen reader compatible
  • ✓ WCAG AA compliant
  • ✓ Schema.org structured data
  • ✓ Agent-friendly markup
  • ✓ Progressive enhancement
  • ✓ Works without JavaScript
  • ✓ Responsive design
  • ✓ Production-ready code

Next Steps

Expand Your Knowledge

Dive deeper into BiModal Design principles and advanced techniques.

Read the Guide →

See More Examples

Explore complete code examples for different types of sites and components.

View Examples →

Join the Community

Share your BiModal sites and get feedback from other developers.

Join Discussion →