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:
- Auto-generated layouts: Potato generates an HTML layout file that you can edit
- Custom layout files: Create your own HTML template with full control over styling
Quick Start
Using Auto-generated Layouts
- Run your server once, and Potato writes
layouts/task_layout.html - Edit the generated file to customize styling
- Your changes persist across server restarts (unless you modify
annotation_schemesin 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
- Create your layout file (e.g.,
layouts/custom_task_layout.html) - Reference it in your config:
task_layout: layouts/custom_task_layout.htmlThe 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:
base_css: styles/custom.cssPotato 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:
<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:
| Attribute | Description |
|---|---|
id | Must match the name in your config's annotation_schemes |
class | Include annotation-form and the type (e.g., radio, multiselect) |
data-annotation-id | Sequential index (0, 1, 2...) |
schema | Attribute on fieldset and inputs matching the schema name |
Required Input Attributes
<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.
python potato/flask_server.py start examples/custom-layouts/content-moderation/config.yaml -p 80002. 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.
python potato/flask_server.py start examples/custom-layouts/dialogue-qa/config.yaml -p 80003. 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.
python potato/flask_server.py start examples/custom-layouts/medical-review/config.yaml -p 8000CSS Techniques
Grid Layouts
Create multi-column layouts:
.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:
.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:
.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:
.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.
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
- Match schema names: Form
idmust exactly matchnameinannotation_schemes - Sequential annotation IDs: Use 0, 1, 2... for
data-annotation-id - Include required handlers: Use
onclick="onlyOne(this);registerAnnotation(this);"for radio,onclick="registerAnnotation(this);"for checkbox - Test responsiveness: Use media queries for mobile support
- Keep accessibility: Use proper labels and maintain keyboard navigation
Troubleshooting
Annotations not saving
Check that:
- Form
idmatches annotation schemename - Inputs have
schemaandlabel_nameattributes - 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_layoutis relative to config file - Check for HTML syntax errors
- Review server logs for error messages
Further Reading
- Instance Display - Configure what content to show
- UI Configuration - Interface customization options
- Annotation Schemes - Available annotation types
For implementation details, see the source documentation.