Skip to content

Layout Customization

Build custom annotation layouts in Potato with HTML templates and CSS, covering side-by-side comparisons, multi-column schemas, and project-wide styling.

New in v2.1.0

Potato provides two approaches for customizing the annotation interface layout:

  1. Auto-generated layouts: Potato generates an HTML layout file that you can edit
  2. Custom layout files: Create your own HTML template with full control over styling

Quick Start

Using Auto-generated Layouts

  1. Run your server once, and Potato writes layouts/task_layout.html
  2. Edit the generated file to customize styling
  3. Your changes persist across server restarts (unless you modify annotation_schemes in the config)

Editing annotation_schemes regenerates the file, because the form markup has to match the schemes it collects. Move a layout you want to keep to its own filename before changing the schemes.

Using Custom Layout Files

  1. Create your layout file (e.g., layouts/custom_task_layout.html)
  2. Reference it in your config:
yaml
task_layout: layouts/custom_task_layout.html

The path is resolved relative to the config file's directory, so a layout stored beside the config travels with the project.

Project-wide Base CSS

A layout file styles the annotation page. To style every page a project serves, including the consent and instructions pages, point base_css at a stylesheet:

yaml
base_css: styles/custom.css

Potato reads the file at server startup and injects it as a <style id="potato-project-base-css"> block in the <head> of every page, after all built-in styles, so your rules take precedence. Changes require a server restart, and a missing file logs a warning rather than stopping the server.

Layout File Structure

A custom layout file must include:

html
<style>
    /* Your custom CSS */
</style>
 
<div class="annotation_schema">
    <!-- Your annotation forms -->
    <form id="schema_name" class="annotation-form radio" data-annotation-id="0">
        <fieldset schema="schema_name">
            <legend>Question text</legend>
            <!-- Input elements -->
        </fieldset>
    </form>
</div>

Required Form Attributes

Potato matches the markup back to your config through these four attributes, so a mismatch in any of them means the annotation never reaches the output file:

AttributeDescription
idMust match the name in your config's annotation_schemes
classInclude annotation-form and the type (e.g., radio, multiselect)
data-annotation-idSequential index (0, 1, 2...)
schemaAttribute on fieldset and inputs matching the schema name

Required Input Attributes

html
<input class="schema_name annotation-input"
       type="radio"
       name="schema_name"
       value="label_value"
       schema="schema_name"
       label_name="label_value"
       onclick="onlyOne(this);registerAnnotation(this);">

Example Layouts

Potato ships three worked example layouts. The commands below run them from the root of a cloned Potato repository, since the configs and their data files are part of the checkout. To run a layout of your own against your own config, use potato start <your-config>.yaml -p 8000 instead.

1. Content Moderation Dashboard

Location: examples/custom-layouts/content-moderation/

Features a warning banner header with content metadata, 2-column grid for violation categories, color-coded severity levels, and a moderation workflow.

bash
python potato/flask_server.py start examples/custom-layouts/content-moderation/config.yaml -p 8000

2. Customer Service Dialogue QA

Location: examples/custom-layouts/dialogue-qa/

Features a case header with metadata badges, grouped assessment sections, circular Likert-scale ratings, a quality issues checklist, and color-coded resolution indicators.

bash
python potato/flask_server.py start examples/custom-layouts/dialogue-qa/config.yaml -p 8000

3. Medical Image Review

Location: examples/custom-layouts/medical-review/

Features medical UI styling, a two-column layout for location and severity, grouped findings sections, structured reporting, and recommendation cards with descriptions.

bash
python potato/flask_server.py start examples/custom-layouts/medical-review/config.yaml -p 8000

CSS Techniques

Grid Layouts

Create multi-column layouts:

css
.annotation-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
}
 
.full-width {
    grid-column: 1 / -1;
}
 
@media (max-width: 768px) {
    .annotation-grid {
        grid-template-columns: 1fr;
    }
}

Color-Coded Options

Style radio buttons with severity colors:

css
.severity-option input[type="radio"] {
    position: absolute;
    opacity: 0;
}
 
.severity-label {
    display: block;
    padding: 10px;
    border-radius: 6px;
    border: 2px solid transparent;
    cursor: pointer;
    transition: all 0.2s;
}
 
/* Green for "None" */
.severity-none .severity-label {
    background: #dcfce7;
    color: #166534;
}
.severity-none input:checked + .severity-label {
    background: #22c55e;
    color: white;
}
 
/* Red for "Severe" */
.severity-severe .severity-label {
    background: #fee2e2;
    color: #991b1b;
}
.severity-severe input:checked + .severity-label {
    background: #ef4444;
    color: white;
}

Hiding the native input and styling its label keeps the whole colored block clickable, which matters when the option set is a grid rather than a vertical list.

Section Styling

Create visual groupings:

css
.annotation-section {
    background: #f8fafc;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    padding: 16px;
    margin-bottom: 16px;
}
 
.section-title {
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 2px solid #3b82f6;
}

Circular Likert Ratings

Circles read as a scale at a glance and take less vertical space than a stack of radio rows, which keeps a multi-question form on one screen:

css
.likert-circle {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 2px solid #e2e8f0;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
 
.likert-option input:checked + .likert-circle {
    background: #8b5cf6;
    color: white;
    border-color: #7c3aed;
}

Combining with Instance Display

Custom layouts work alongside instance_display configuration. The instance content (images, text, dialogues) renders separately above your annotation forms, so a layout file controls the questions and instance_display controls the material being annotated.

yaml
instance_display:
  fields:
    - key: image_url
      type: image
      display_options:
        zoomable: true
 
task_layout: layouts/custom_task_layout.html
 
annotation_schemes:
  - annotation_type: radio
    name: category
    description: "Which category does this image belong to?"
    labels: [A, B, C]

Best Practices

  1. Match schema names: Form id must exactly match name in annotation_schemes
  2. Sequential annotation IDs: Use 0, 1, 2... for data-annotation-id
  3. Include required handlers: Use onclick="onlyOne(this);registerAnnotation(this);" for radio, onclick="registerAnnotation(this);" for checkbox
  4. Test responsiveness: Use media queries for mobile support
  5. Keep accessibility: Use proper labels and maintain keyboard navigation

Troubleshooting

Annotations not saving

Check that:

  • Form id matches annotation scheme name
  • Inputs have schema and label_name attributes
  • Click handlers (registerAnnotation) are present

Styles not applying

  • Ensure CSS specificity is high enough to override defaults
  • Check that your <style> block is inside the layout file
  • Use browser dev tools to inspect applied styles

Layout not loading

  • Verify path in task_layout is relative to config file
  • Check for HTML syntax errors
  • Review server logs for error messages

Further Reading

For implementation details, see the source documentation.