trmnl
TRMNL Content Generator
Generate HTML content for TRMNL e-ink display devices.
Quick Start Workflow
- Check for
$TRMNL_WEBHOOKenvironment variable - If missing, prompt user for webhook URL
- Ask user to verify TRMNL display markup is set to:
<div>{{content}}</div> - Confirm device type (default: TRMNL OG, 2-bit, 800x480)
- Read relevant reference docs based on content needs
- Generate HTML using TRMNL framework classes
- Send via POST to webhook (use temp file method)
- Send minimal confirmation only - Do NOT echo content back to chat
Device & Setup
Default target: TRMNL OG (7.5" e-ink, 800x480px, 2-bit)
Display markup required:
<div>{{content}}</div>
Webhook:
export TRMNL_WEBHOOK="https://trmnl.com/api/custom_plugins/{uuid}"
Sending content (temp file method):
cat > /tmp/trmnl.json << 'EOF'
{"merge_variables":{"content":"<div class=\"layout\">HTML</div>"}}
EOF
curl "$TRMNL_WEBHOOK" -H "Content-Type: application/json" -d @/tmp/trmnl.json -X POST
Webhook Limits
| Tier | Payload Size | Rate Limit |
|---|---|---|
| Free | 2 KB (2,048 bytes) | 12 requests/hour |
| TRMNL+ | 5 KB (5,120 bytes) | 30 requests/hour |
Check payload size before sending:
python3 scripts/check_payload.py /tmp/trmnl.json
Tips to reduce payload size:
- Minify HTML (remove unnecessary whitespace)
- Use framework classes instead of inline styles
- Use short class names the framework provides
- Remove comments from HTML
- Use
deep_mergestrategy for incremental updates
Reference Documentation
Read these files as needed:
| File | When to Read |
|---|---|
references/patterns.md |
Start here - Common plugin patterns from official examples |
references/framework-overview.md |
Device specs, e-ink constraints, responsive prefixes |
references/css-utilities.md |
Colors, typography, sizing, spacing utilities |
references/layout-systems.md |
Flexbox, grid, overflow/clamp engines |
references/components.md |
Title bar, dividers, items, tables, charts |
references/webhook-api.md |
Payload format, rate limits, troubleshooting |
assets/anti-patterns.md |
Common mistakes to avoid |
assets/good-examples/ |
HTML reference implementations |
Scripts:
| Script | Purpose |
|---|---|
scripts/check_payload.py |
Verify payload size before sending (run on /tmp/trmnl.json) |
Standard Plugin Structure
Every plugin follows this pattern:
<div class="layout layout--col gap--space-between">
<!-- Content sections separated by dividers -->
</div>
<div class="title_bar">
<img class="image" src="icon.svg">
<span class="title">Plugin Name</span>
<span class="instance">Context</span>
</div>
layout+layout--col= vertical flex containergap--space-between= push sections to edgestitle_bar= always at bottom, outside layoutdivider= separate major sections- CRITICAL: Only ONE
.layoutelement per view (no nesting)
Grid System (10-Column)
Column spans should sum to 10:
<div class="grid">
<div class="col--span-3">30%</div>
<div class="col--span-7">70%</div>
</div>
Simple equal columns: grid--cols-2, grid--cols-3, etc.
Item Component
Standard data display pattern:
<div class="item">
<div class="meta"><span class="index">1</span></div>
<div class="content">
<span class="value value--xlarge value--tnums">$159,022</span>
<span class="label">Total Sales</span>
</div>
</div>
Value Typography
Always use value--tnums for numbers.
| Class | Usage |
|---|---|
value--xxxlarge |
Hero KPIs |
value--xxlarge |
Large prices |
value--xlarge |
Secondary metrics |
value--small |
Tertiary data |
value--tnums |
Always for numbers |
Auto-fit: <span class="value" data-fit-value="true">...</span>
Columns (for Lists)
<div class="columns">
<div class="column" data-overflow="true" data-overflow-counter="true">
<span class="label label--medium group-header">Section</span>
<div class="item">...</div>
</div>
</div>
Grayscale Dithering
Use dithered classes, not inline gray colors:
bg--black,bg--gray-60,bg--gray-30,bg--gray-10,bg--whitetext--black,text--gray-50
Data Attributes
| Attribute | Purpose |
|---|---|
data-fit-value="true" |
Auto-resize text to fit |
data-value-format="true" |
Auto-format numbers (locale-aware) |
data-clamp="N" |
Limit to N lines |
data-overflow="true" |
Enable overflow management |
data-overflow-counter="true" |
Show "and X more" |
data-overflow-max-cols="N" |
Max columns for overflow |
data-content-limiter="true" |
Auto-adjust text size |
data-pixel-perfect="true" |
Crisp text rendering |
data-table-limit="true" |
Table overflow with "and X more" |
Label & Title Variants
<span class="label label--small">Small</span>
<span class="label label--medium">Medium</span>
<span class="label label--underline">Underlined</span>
<span class="label label--gray">Muted/completed</span>
<span class="title title--small">Compact title</span>
Gap Utilities
gap--space-between | gap--xxlarge | gap--xlarge | gap--large | gap--medium | gap | gap--small | gap--xsmall | gap--none
Typography Guidelines
Recommended: Georgia serif font for e-ink readability.
Content-aware sizing:
- Short content = bigger fonts (24-28px body)
- Long content = smaller fonts (20px body)
- Headings: 36-48px
User Experience
Critical: Do NOT echo content back to chat. Just confirm "Sent to TRMNL".
Anti-Patterns
- Tiny fonts for short content
- Center-aligning columns with different lengths (use
layout--start) - Spoiling content in chat confirmation
- Missing
value--tnumson numbers - Missing
title_bar - Not using
data-fit-valueon primary metrics - Skipping
data-overflowon variable lists - Using inline gray colors instead of
bg--gray-* - Forgetting dividers between sections
Best Practices
- Verify
<div>{{content}}</div>display markup - Use
layout+title_barstructure - Always
value--tnumsfor numbers - Use
data-fit-valueon primary metrics - Use
data-overflowon variable lists - Use
itemcomponent pattern - Use
dividerbetween sections - Use
bg--gray-*dithered classes - Content-aware font sizing
- Top-align columns (
layout--start) - Temp file method for curl
- Minimal confirmations
Mashup Layouts (Multi-Plugin)
For dashboard views with multiple plugins:
| Layout | Description |
|---|---|
mashup--1Lx1R |
2 columns (50/50) |
mashup--1Tx1B |
2 rows (50/50) |
mashup--2x2 |
4 quadrants |
See references/layout-systems.md for all 7 layouts.
Troubleshooting
| Problem | Solution |
|---|---|
| Webhook fails | Verify URL, check rate limits (12/hour free) |
| Content missing | Check display markup is <div>{{content}}</div> |
| Payload too large | Run scripts/check_payload.py, keep under 2KB (free) or 5KB (TRMNL+) |
| Numbers misaligned | Add value--tnums |
| Text overflow | Use data-clamp or data-overflow |
| Columns misaligned | Use layout--start not layout--center |
| Multiple layouts error | Keep only ONE .layout element per view |
| Nested content fails | Use .richtext for nested/formatted content |