# Custom HTML Templates in Potato

Source: https://www.potatoannotator.com/blog/custom-html-templates

Potato's built-in annotation types cover most tasks, but sometimes the interface you want just is not one of them. Custom HTML templates let you write your own layout, drop in CSS, and wire up JavaScript when you need to. This guide walks through building one.

## When to reach for a custom template

A custom template earns its keep when the built-in types can't give you the layout you need: a medical or legal interface with its own conventions, an interactive visualization, a one-off input widget, or a layout that has to match a particular brand. If a built-in type already does the job, use it instead. Custom templates are more to maintain.

## Basic template structure

### Template configuration

```yaml
annotation_task_name: "Custom Template Annotation"

html_layout: "templates/my_template.html"
```

### Template File

```html
<!-- templates/my_template.html -->
<div class="custom-annotation-container">
  <!-- Display the text -->
  <div class="content-display">
    <div class="text-content">
      {{text}}
    </div>
  </div>

  <!-- Custom annotation interface -->
  <div class="annotation-area">
    <!-- Potato will inject annotation schemes here -->
    {{annotation_schemes}}
  </div>
</div>
```

## Template Variables

### Available Variables

```html
<!-- Item data -->
{{id}}           <!-- Item ID -->
{{text}}         <!-- Main text content -->
{{image_url}}    <!-- Image URL if present -->
{{audio_url}}    <!-- Audio URL if present -->

<!-- Metadata -->
{{metadata.field_name}}  <!-- Any metadata field -->
```

## Styling Custom Templates

### Inline CSS

```html
<style>
  .custom-annotation-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    font-family: -apple-system, BlinkMacSystemFont, sans-serif;
  }

  .task-header {
    background: linear-gradient(135deg, #6E56CF, #9F7AEA);
    color: white;
    padding: 15px 20px;
    border-radius: 8px;
    margin-bottom: 20px;
  }

  .content-display {
    background: #F8FAFC;
    border: 1px solid #E2E8F0;
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 20px;
  }

  .text-content {
    font-size: 16px;
    line-height: 1.6;
    color: #1E293B;
  }

  .metadata {
    margin-top: 15px;
    padding-top: 15px;
    border-top: 1px solid #E2E8F0;
    font-size: 14px;
    color: #64748B;
  }

  .annotation-area {
    background: white;
    border: 1px solid #E2E8F0;
    border-radius: 8px;
    padding: 20px;
  }

  /* Custom highlight styles */
  .highlight-positive {
    background-color: #D1FAE5;
    padding: 2px 4px;
    border-radius: 3px;
  }

  .highlight-negative {
    background-color: #FEE2E2;
    padding: 2px 4px;
    border-radius: 3px;
  }
</style>
```

## JavaScript Integration

### Basic Interactivity

```html
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Get the text content element
    const textContent = document.querySelector('.text-content');

    // Add click-to-highlight functionality
    textContent.addEventListener('mouseup', function() {
      const selection = window.getSelection();
      if (selection.toString().trim()) {
        highlightSelection(selection);
      }
    });

    function highlightSelection(selection) {
      const range = selection.getRangeAt(0);
      const span = document.createElement('span');
      span.className = 'user-highlight';
      range.surroundContents(span);
    }
  });
</script>
```

## Advanced Templates

### Side-by-Side Comparison

```html
<div class="comparison-container">
  <style>
    .comparison-container {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 20px;
    }

    .comparison-item {
      border: 1px solid #E2E8F0;
      border-radius: 8px;
      padding: 15px;
    }

    .comparison-item.selected {
      border-color: #6E56CF;
      box-shadow: 0 0 0 2px rgba(110, 86, 207, 0.2);
    }

    .item-label {
      font-weight: 600;
      margin-bottom: 10px;
      color: #6E56CF;
    }
  </style>

  <div class="comparison-item" data-option="A" onclick="selectOption('A')">
    <div class="item-label">Option A</div>
    <div class="item-content">{{option_a}}</div>
  </div>

  <div class="comparison-item" data-option="B" onclick="selectOption('B')">
    <div class="item-label">Option B</div>
    <div class="item-content">{{option_b}}</div>
  </div>

  <script>
    function selectOption(option) {
      // Update visual selection
      document.querySelectorAll('.comparison-item').forEach(el => {
        el.classList.remove('selected');
      });
      document.querySelector(`[data-option="${option}"]`).classList.add('selected');
    }
  </script>
</div>
```

### Interactive Highlighting

```html
<div class="highlight-annotation">
  <style>
    .highlight-toolbar {
      display: flex;
      gap: 10px;
      margin-bottom: 15px;
    }

    .highlight-btn {
      padding: 8px 16px;
      border: 1px solid #E2E8F0;
      border-radius: 6px;
      cursor: pointer;
      background: white;
    }

    .highlight-btn.active {
      background: #6E56CF;
      color: white;
      border-color: #6E56CF;
    }

    .highlightable-text {
      line-height: 1.8;
    }

    .highlight-positive { background: #D1FAE5; }
    .highlight-negative { background: #FEE2E2; }
    .highlight-neutral { background: #FEF3C7; }
  </style>

  <div class="highlight-toolbar">
    <button class="highlight-btn" data-color="positive" onclick="setHighlightMode('positive')">
      Positive
    </button>
    <button class="highlight-btn" data-color="negative" onclick="setHighlightMode('negative')">
      Negative
    </button>
    <button class="highlight-btn" data-color="neutral" onclick="setHighlightMode('neutral')">
      Neutral
    </button>
    <button class="highlight-btn" onclick="clearHighlights()">
      Clear All
    </button>
  </div>

  <div class="highlightable-text" id="textContent">
    {{text}}
  </div>

  <script>
    let currentMode = null;
    let highlights = [];

    function setHighlightMode(mode) {
      currentMode = mode;
      document.querySelectorAll('.highlight-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.color === mode);
      });
    }

    document.getElementById('textContent').addEventListener('mouseup', function() {
      if (!currentMode) return;

      const selection = window.getSelection();
      if (selection.toString().trim()) {
        const range = selection.getRangeAt(0);
        const span = document.createElement('span');
        span.className = `highlight-${currentMode}`;

        // Store highlight data
        highlights.push({
          text: selection.toString(),
          type: currentMode,
          start: range.startOffset,
          end: range.endOffset
        });

        range.surroundContents(span);
        selection.removeAllRanges();
      }
    });

    function clearHighlights() {
      highlights = [];
      document.getElementById('textContent').innerHTML = '{{text}}';
    }
  </script>
</div>
```

### Tabbed Interface

```html
<div class="tabbed-interface">
  <style>
    .tab-buttons {
      display: flex;
      border-bottom: 2px solid #E2E8F0;
    }

    .tab-btn {
      padding: 12px 24px;
      border: none;
      background: none;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      color: #64748B;
      border-bottom: 2px solid transparent;
      margin-bottom: -2px;
    }

    .tab-btn.active {
      color: #6E56CF;
      border-bottom-color: #6E56CF;
    }

    .tab-content {
      display: none;
      padding: 20px 0;
    }

    .tab-content.active {
      display: block;
    }
  </style>

  <div class="tab-buttons">
    <button class="tab-btn active" onclick="showTab('text')">Text</button>
    <button class="tab-btn" onclick="showTab('metadata')">Metadata</button>
    <button class="tab-btn" onclick="showTab('context')">Context</button>
  </div>

  <div class="tab-content active" id="tab-text">
    <div class="text-content">{{text}}</div>
  </div>

  <div class="tab-content" id="tab-metadata">
    <pre>{{metadata | json}}</pre>
  </div>

  <div class="tab-content" id="tab-context">
    <p><strong>Source:</strong> {{metadata.source}}</p>
    <p><strong>Date:</strong> {{metadata.date}}</p>
    <p><strong>Author:</strong> {{metadata.author}}</p>
  </div>

  <script>
    function showTab(tabName) {
      document.querySelectorAll('.tab-btn').forEach(btn => {
        btn.classList.remove('active');
      });
      document.querySelectorAll('.tab-content').forEach(content => {
        content.classList.remove('active');
      });

      event.target.classList.add('active');
      document.getElementById(`tab-${tabName}`).classList.add('active');
    }
  </script>
</div>
```

## A few things worth doing

Keep the template itself thin. If logic starts creeping in, push it back into the config where it belongs. Test on a narrow screen before you ship, because grid layouts that look fine on a laptop fall apart on a tablet. Mind the basics of accessibility: readable contrast, real labels, keyboard navigation. Go easy on the JavaScript and the image sizes. Handle the case where a field is missing instead of rendering a blank gap. And leave a comment or two so the next person (probably you, in six months) can tell what the template is doing.

For the configuration keys behind custom layouts, see the [layout customization](https://github.com/davidjurgens/potato/blob/master/docs/configuration/layout_customization.md) and [form layout](https://github.com/davidjurgens/potato/blob/master/docs/configuration/form_layout.md) documentation.

---

*Full documentation at [/docs/core-concepts/annotation-schemes](/docs/core-concepts/annotation-schemes).*
