# V3 Overview Framework v3 introduces a complete color system for ePaper devices. The framework now supports chromatic palettes, semantic color roles, and an extended grayscale scale, while shifting its internal architecture to CSS custom properties for cleaner, smaller stylesheets. Existing markup continues to work without changes. ### Most Important Upgrades - **Color support:** v3 adds a complete color system with chromatic palettes, semantic color roles, and automatic adaptation to each device's supported palette and bit-depth. - **Architecture overhaul:** the framework moves from a rules-based selector system to CSS custom properties, greatly reducing combinatorial mode rules while keeping existing class names stable. - **High-density support:** framework rendering now supports high-density `1bit` and `2bit` output modes. - **Expanded 1bit grayscale:** the usable `1bit` grayscale palette increases from 7 shades to 14. - **Raw / Preview simulation:** Device Preview can compare full-bright tokens (Raw) against panel-accurate output that simulates true colors and white point. ### What's New - **Chromatic utilities:** `bg--red`, `bg--blue-40`, `text--green-60` and similar classes for all 10 hues and 14 steps. - **Semantic colors:** `bg--primary`, `text--success`, `label--error`, `bg--warning` - intent-based styling that maps to underlying hues. - **Colors reference:** new [Colors](/framework/docs/3.1/colors) page documenting the full grayscale, chromatic, and semantic palette. - **Label color variants:** [Label](/framework/docs/3.1/label) gains `label--primary`, `label--success`, `label--error`, and `label--warning` for colored badges. - **Closest-hue mapping:** when a selected device cannot render a requested color directly, framework tokens map to the nearest supported hue automatically. - **Color pattern images:** auto-generated dither patterns for limited-palette devices in `public/images/color-*/`. ### What's Enhanced - **Background utility:** [Background](/framework/docs/3.1/background) refactored to reference CSS custom properties. Now supports grayscale, chromatic, and semantic tokens in a single class, including high-density `1bit` and `2bit` rendering modes. - **Text utility:** [Text](/framework/docs/3.1) follows the same CSS variable pattern, supporting chromatic and semantic text colors alongside grayscale. - **Border and Outline:** [Border](/framework/docs/3.1/border) and [Outline](/framework/docs/3.1/outline) use shared mixins for DRY, consistent rendering across bit-depths and color palettes, including high-density `1bit` and `2bit` modes. - **Dark mode:** grayscale tokens invert automatically; chromatic hues stay stable while their lightness steps mirror (light to dark). - **Progress component:** [Progress](/framework/docs/3.1/progress) updated to render with color palette awareness. ### What's Changed - **Grayscale scale:** the primary naming convention is now `gray-10` through `gray-75` (14 steps of 5). In `1bit`, the usable grayscale palette expands from 7 shades to 14. The legacy names `gray-1` through `gray-7` remain functional but are deprecated. - **Rendering model:** mode-dependent styling (bit-depth, dark mode, palette) is driven by CSS custom properties on `.screen` rather than combinatorial selector rules. This is an internal change - existing class names are unaffected. - **Default font bundle:** Framework 3.1 uses the TRMNL bundle from [Font Family](/framework/docs/3.1/font_family) by default on low-density displays. Add `screen--fonts-classic` to keep the 3.0 Classic typography. ### Start Here - Upgrading from v2? → [V3 Upgrade Guide](/framework/docs/3.1/v3_upgrade_guide) . - Looking to use colors and the new palette system? → [V3 Enhancement Guide](/framework/docs/3.1/v3_enhancement_guide) . - New to the framework? Start with the [Colors](/framework/docs/3.1/colors) reference page. Next [V3 Upgrade Guide Steps to upgrade your plugins from Framework v2 to v3](/framework/docs/3.1/v3_upgrade_guide) --- # V3 Upgrade Guide Framework v3 is structurally backward-compatible with v2: your existing class names still compile and render. However, the `1bit` grayscale dither patterns have been rebuilt on a new linear scale, so most shade names now produce different lightness values than they did in v2. Review the migration table below to restore your original look or take advantage of the finer 14-step palette. ### 1-Bit Grayscale Dither Scale In `1bit` mode, v3 replaces the 7-step non-linear dither scale (12×12 px tiles) with a 14-step linear scale (16×16 px tiles). Each step now increments by 6.25% white-pixel density. In v2 the 14 extended shade names were rendered in pairs: `gray-15` produced the same pattern as `gray-10`, `gray-25` the same as `gray-20`, and so on. In v3 every shade has its own distinct pattern, so nearly all shades now sit at **different lightness values** than they did in v2. #### Lightness Shift The table below shows the white-pixel density each shade produced in v2 `1bit` mode, what it now produces in v3, and which v3 shade to use to restore the original appearance. | Class | v2 Lightness | v3 Lightness | Shift | Use Instead | | --- | --- | --- | --- | --- | | `gray-10` | 6.25% | 6.25% | — | — | | `gray-15` | 6.25% | 12.5% | +6.25% | `gray-10` | | `gray-20` | 12.5% | 18.75% | +6.25% | `gray-15` | | `gray-25` | 12.5% | 25% | +12.5% | `gray-15` | | `gray-30` | 25% | 31.25% | +6.25% | `gray-25` | | `gray-35` | 25% | 37.5% | +12.5% | `gray-25` | | `gray-40` | 50% | 43.75% | −6.25% | `gray-45` | | `gray-45` | 50% | 56.25% | +6.25% | `gray-40` | | `gray-50` | 75% | 62.5% | −12.5% | `gray-60` | | `gray-55` | 75% | 68.75% | −6.25% | `gray-60` | | `gray-60` | 87.5% | 75% | −12.5% | `gray-70` | | `gray-65` | 87.5% | 81.25% | −6.25% | `gray-70` | | `gray-70` | 93.75% | 87.5% | −6.25% | `gray-75` | | `gray-75` | 93.75% | 93.75% | — | — | To preserve your v2 look, switch to the shade listed in the “Use Instead” column. To adopt the new linear scale and take advantage of the finer 14-step palette, keep your current class names - they now each produce a unique dither pattern. See the[Colors](/framework/docs/3.1/colors) reference for the full scale. ### CSS Variable Architecture Under the hood, utilities like[Background](/framework/docs/3.1/background) and [Text](/framework/docs/3.1) now reference CSS custom properties instead of containing per-mode selector rules. This is an internal refactor. The class names you use in your markup (`bg--gray-50`, `text--black`, etc.) are unchanged. If you have custom CSS that directly targets framework internals (e.g., overriding `.screen--1bit .bg--gray-3` selectors), you may need to update those overrides to use the new CSS variable approach. If you use only framework-provided class names, no action is needed. If you have custom CSS overriding framework internals, review the updated[Background](/framework/docs/3.1/background) and [Text](/framework/docs/3.1) pages for the new variable-based approach. ### Background and Border Rendering Modes Utility class names are unchanged, but rendering support is broader in v3. Both[Background](/framework/docs/3.1/background) and [Border](/framework/docs/3.1/border) now support high-density `1bit` and `2bit` output modes without markup changes. ### Color Palette on Grayscale Devices If you adopt the new chromatic color classes (e.g., `bg--red`, `text--blue-40`) in your plugin, they automatically adapt by device capability. On grayscale hardware, colors fall back to perceptually equivalent gray values; on limited-color hardware, unavailable colors map to the closest supported hue. No conditional logic is needed. The framework handles this mapping via perceptual lightness (LAB L*). ### Raw / Preview Verification Use the Raw Colors / Preview Colors toggle in Device Preview while upgrading. Raw mode shows full-bright token values; Preview mode simulates panel-accurate output, including true-color behavior and device white point. ### Next Steps Once you've reviewed the deprecation notes above, head to the[V3 Enhancement Guide](/framework/docs/3.1/v3_enhancement_guide) to learn how to use chromatic colors, semantic roles, and the new label variants in your plugins. Previous [V3 Overview What's new in Framework v3: color palette, semantic colors, extended grayscale, and CSS variable architecture](/framework/docs/3.1/v3_overview) Next [V3 Enhancement Guide Use chromatic colors, semantic roles, and label variants in your plugins](/framework/docs/3.1/v3_enhancement_guide) --- # V3 Enhancement Guide Framework v3 opens up the full color spectrum for ePaper plugins. This guide walks through the key enhancements you can adopt, from chromatic backgrounds and text to semantic labels and the extended grayscale. ### 1. Use Chromatic Colors The framework now provides 10 hues, each with 14 lightness steps. Apply them with the same `bg--` and `text--` prefixes you already know. - **Base hue:** `bg--red`, `text--blue` - the pure, full-saturation color. - **Hue + step:** `bg--red-40`, `text--green-60` - lighter or darker variants using steps 10 (darkest) through 75 (lightest). - **Available hues:** red, orange, yellow, lime, green, cyan, blue, violet, purple, pink. On grayscale devices, chromatic tokens automatically fall back to perceptually equivalent gray shades. On limited-color panels, unavailable colors map to the closest supported hue automatically, so no conditional markup is needed. See the full palette on the[Colors](/framework/docs/3.1/colors) page. ### 2. Apply Semantic Colors Semantic roles let you style by intent rather than specific hue. The framework maps each role to an underlying color token that can be themed via CSS variables. - `bg--primary` / `text--primary` - blue (highlights, accents) - `bg--success` / `text--success` - green (confirmations, positive states) - `bg--error` / `text--error` - red (errors, critical states) - `bg--warning` / `text--warning` - orange (cautions, alerts) Semantic tokens inherit all device and bit-depth behavior. To override the default mapping, set `--color-primary`, `--color-success`, etc. in your custom CSS. ### 3. Use Label Color Variants The[Label](/framework/docs/3.1/label) element now supports semantic color variants for colored badges and status indicators. - `label--primary` - blue label for key information - `label--success` - green label for positive states - `label--error` - red label for errors or alerts - `label--warning` - orange label for cautions The default filled label (`label--filled`) continues to use black (the darkest shade). The new color variants use semantic color backgrounds while adapting to the current device palette. ### 4. Leverage the Extended Grayscale The extended scale doubles the available gray shades from 7 to 14, giving you finer control over tonal variation in your layouts. In `1bit` mode, the usable grayscale palette now expands from 7 shades to 14. - **Old scale:** `gray-1` through `gray-7` (still works, deprecated) - **New scale:** `gray-10`, `gray-15`, `gray-20`, …, `gray-75` Intermediate steps like `gray-15`, `gray-25`, and `gray-35` let you create subtler contrasts and more nuanced visual hierarchies, especially on 2-bit and 4-bit displays. ### 5. Use the Raw / Preview Toggle The device picker now includes a Raw / Preview switch that lets you compare how colors render across different modes. - **Raw:** shows the full-bright token values, useful for verifying your color choices. - **Preview:** shows panel-accurate simulation, including device true-color behavior and lowered white points on displays like TRMNL BWRY, so you can see what end users actually see on hardware. ### 6. Target High-Density 1bit / 2bit Rendering Core utilities now render consistently on high-density ePaper modes. Use the same classes and let the framework adapt to panel capabilities. - **Background utility:** [Background](/framework/docs/3.1/background) supports high-density `1bit` and `2bit` output in addition to broader color-capable modes. - **Border utility:** [Border](/framework/docs/3.1/border) supports the same high-density `1bit` and `2bit` rendering behavior for consistent edge contrast. Previous [V3 Upgrade Guide Steps to upgrade your plugins from Framework v2 to v3](/framework/docs/3.1/v3_upgrade_guide) Next [TRMNL X Guide Framework changes for TRMNL X compatibility](/framework/docs/3.1/trmnl_x_guide) --- # TRMNL X Guide TRMNL X is a larger, 4-bit ePaper display. This guide covers everything that changed in the Framework to support it. Your existing plugins will continue to work, but by adopting these new features you can build layouts that take full advantage of the larger screen, portrait orientation, and expanded grayscale. ### The --base Modifier Every element and component now supports an explicit `--base` size modifier. It produces the exact same result as using the class without any size modifier, but it lets you **reset to the default size at a specific breakpoint**. Previously, if you set a smaller size for compact screens, there was no way to undo it at a larger breakpoint. Now you can: Dashboard 48,206
...
The `--base` modifier is available on: - **Typography:** [Title](/framework/docs/3.1/title) (`title--base`), [Value](/framework/docs/3.1/value) (`value--base`), [Label](/framework/docs/3.1/label) (`label--base`), [Description](/framework/docs/3.1/description) (`description--base`) - **Rich Text:** [Rich Text](/framework/docs/3.1/rich_text) (`content--base`) - **Components:** [Table](/framework/docs/3.1/table) (`table--base`), [Progress](/framework/docs/3.1/progress) (`progress-bar--base`, `progress-dots--base`) - **Utilities:** [Gap](/framework/docs/3.1/gap) (`gap--base`), [Rounded](/framework/docs/3.1/rounded) (`rounded--base`), [Text Stroke](/framework/docs/3.1/text_stroke) (`text-stroke--base`), [Image Stroke](/framework/docs/3.1/image_stroke) (`image-stroke--base`) ### New Larger Typography Sizes To take full advantage of TRMNL X's larger display, new size tiers have been added across all typography elements. All new sizes support responsive prefixes and render correctly on 2-bit and 4-bit screens. #### Value Four new sizes for hero-style numbers and display text. See the[Value](/framework/docs/3.1/value) docs for full details. 42 42 42 42 42 #### Title Three new large sizes for headings. See the[Title](/framework/docs/3.1/title) docs. Heading Heading Heading #### Label, Description, Rich Text New large sizes for[Label](/framework/docs/3.1/label) (`label--large`, `label--xlarge`, `label--xxlarge`), [Description](/framework/docs/3.1/description) (`description--large`, `description--xlarge`, `description--xxlarge`), and [Rich Text](/framework/docs/3.1/rich_text) content (`content--xlarge`, `content--xxlarge`, `content--xxxlarge`). ### Container Query Units The[Layout](/framework/docs/3.1/layout) element now establishes a CSS Container Query context, enabling a new set of container-relative sizing utilities. Unlike viewport units, these are relative to the layout's dimensions, so they work correctly inside mashup slots (half vertical, quadrant, etc.) where the available space is a fraction of the full screen.
Half the layout width
80% of layout height
Constrained width
Full width on small, half on large
Available utilities: `w--[Ncqw]`, `h--[Ncqh]` (0 to 100), plus `w--min-[Ncqw]`, `w--max-[Ncqw]`, `h--min-[Ncqh]`, `h--max-[Ncqh]`. All support responsive variants. See the [Size](/framework/docs/3.1/size) docs for full details. ### Responsive Arbitrary Sizes The `w--[Npx]` and `h--[Npx]` utilities now support responsive prefixes. Previously, these were static-only. The maximum value has been reduced from 1000px to 800px. See the [Size](/framework/docs/3.1/size) docs.
Responsive arbitrary width
Orientation-aware height
### Responsive Overflow Columns The[Overflow](/framework/docs/3.1/overflow) engine now reads responsive data attributes to determine how many columns to generate, based on screen size and orientation. Resolution order (most specific wins): size + portrait, then size, then portrait, then base.
...
...
Supported suffixes: `-sm`, `-md`, `-lg`, `-portrait`, `-sm-portrait`, `-md-portrait`, `-lg-portrait`. The same pattern works for `data-overflow-cols` (fixed column count). ### Layout Improvements #### Axis-Correct stretch-x / stretch-y `stretch-x` and `stretch-y` now behave correctly relative to the [Layout](/framework/docs/3.1/layout) direction. In `layout--row`, `stretch-x` grows along the horizontal axis and `stretch-y` stretches vertically. In `layout--col`, the axes are swapped. These utilities also now include `min-width: 0` / `min-height: 0` to prevent flex children from overflowing their containers. #### Responsive Grid Column Spans The[Grid](/framework/docs/3.1/grid)`col--span-*` classes now work with all responsive prefixes. Responsive parent variants also work: if the grid itself has a responsive prefix (e.g., `portrait:grid`), nested children resolve correctly.
Full width on small, half on large
Full width on small, half on large
...
#### Item Component The[Item](/framework/docs/3.1/item) component now gives `.icon` elements the same flex styling as `.content`. Items in [Flex](/framework/docs/3.1/flex) row containers automatically stretch to match the tallest sibling, and items in [Grid](/framework/docs/3.1/grid) containers stretch to fill their cell height. ### Gap and Rounded Utilities #### Gap New[Gap](/framework/docs/3.1/gap) classes: `gap--base` (explicit base, useful for responsive reset), `gap--auto` (distributes space evenly around items), and `gap--distribute` (first item at start, last at end, space between). Arbitrary gaps now start from `gap--[0px]` (previously 5px). `gap--space-between` remains as a legacy alias. #### Rounded New[Rounded](/framework/docs/3.1/rounded) class: `rounded--base` (explicit base, 10px). Arbitrary values now start from `rounded--[0px]` (previously 1px). ### Rich Text Improvements The[Rich Text](/framework/docs/3.1/rich_text) component has several improvements: - `content--center` now correctly centers text on 4-bit screens. - Rich text content max-width is now size-aware and adjusts automatically per screen size (small: 380px, medium: 640px, large: 780px). In portrait orientation, it uses the full screen width. - All content size variants support responsive prefixes.

Small on compact devices, default on large screens

### Responsive Clamp The[Clamp](/framework/docs/3.1/clamp) system now supports the `lg` breakpoint. Long label text... Available attributes: `data-clamp`, `data-clamp-sm`, `data-clamp-md`, `data-clamp-lg`, `data-clamp-portrait`, `data-clamp-sm-portrait`, `data-clamp-md-portrait`, `data-clamp-lg-portrait`. ### Landscape Orientation Default `landscape:` prefixed classes now work correctly even without an explicit `.screen--landscape` class on the screen element. Since landscape is the default orientation, `landscape:` prefixes activate whenever `.screen--portrait` is not present. See the [Responsive](/framework/docs/3.1/responsive) docs. ### Bug Fixes - **Title bar on 4-bit screens:** Fixed an issue where the background image could bleed through on 4-bit displays. The title bar now explicitly clears the background image before applying the background color. - **Half horizontal layout height:** The layout height inside half horizontal views was incorrectly using quadrant dimensions. It now correctly uses half horizontal dimensions. - **Available height computation:** Improved height calculation for elements inside flex column layouts. The engine now sums sibling heights and gaps instead of relying on `getBoundingClientRect()`, which can be inaccurate before layout settles. ### Internal Changes The following changes are under-the-hood improvements to how the Framework generates its CSS. They don't introduce new classes or markup patterns, but they make the responsive system more robust and maintainable. - Responsive mixin system refactored to use a shared `parse-selector-components` helper, fixing edge cases with nested selectors. - New `with-responsive-parent-child-variants` mixin powers responsive grid column spans and flex child utilities. - New device-specific responsive generators (`responsive-for-bit-depth`, `responsive-orientation-for-bit-depth`) for scoped bit-depth overrides. - Value and rich text device overrides refactored from manual per-class rules to generated loops. - `screen--sm`, `screen--md`, and `screen--lg` now apply size-specific CSS variable overrides (previously empty). Previous [V3 Enhancement Guide Use chromatic colors, semantic roles, and label variants in your plugins](/framework/docs/3.1/v3_enhancement_guide) Next [Size Define exact width and height dimensions for elements](/framework/docs/3.1/size) --- # Size The Size system provides utility classes for controlling width and height dimensions. It includes both fixed sizes and responsive utilities to handle various layout needs. ## Setting Width and Height Control element dimensions using fixed sizes from the design scale, arbitrary pixel values, or dynamic sizes that adapt to their container. ### Fixed Sizes Use predefined size classes from the design scale. Apply `w--{size}` for width and `h--{size}` for height with these size values: `w/h--0` `w/h--0.5` `w/h--1` `w/h--1.5` `w/h--2` `w/h--2.5` `w/h--3` `w/h--3.5` `w/h--4` `w/h--5` `w/h--6` `w/h--7` `w/h--8` `w/h--9` `w/h--10` `w/h--11` `w/h--12` `w/h--14` `w/h--16` `w/h--20` `w/h--24` `w/h--28` `w/h--32` `w/h--36` `w/h--40` `w/h--44` `w/h--48` `w/h--52` `w/h--56` `w/h--60` `w/h--64` `w/h--72` `w/h--80` `w/h--96` `0px` `2px` `4px` `6px` `8px` `10px` `12px` `14px` `16px` `20px` `24px` `28px` `32px` `36px` `40px` `44px` `48px` `56px` `64px` `80px` `96px` `112px` `128px` `144px` `160px` `176px` `192px` `208px` `224px` `240px` `256px` `288px` `320px` `384px` ### Arbitrary Sizes Need a specific dimension? Use arbitrary size classes for precise pixel values with `w--[Npx]` and `h--[Npx]` syntax, where N can be any value from 0 to 800. `w/h--[150px]` `w/h--[225px]` `w/h--[300px]` `150px` `225px` `300px` ### Dynamic Sizes Use dynamic sizes to set dimensions relative to the container or content. `w--full` and `h--full` set dimensions to 100% of the container, while `w--auto` and `h--auto` let the browser calculate dimensions based on content. Full Width Auto Width SizeDynamic Widths
Full width
Auto width
Full height
Auto height
### Container Query Sizes Container query sizes let you size elements as a percentage of the `.layout` container. Use `w--[Ncqw]` for width and `h--[Ncqh]` for height, where N is 0-100 (representing 0-100% of the layout's dimensions). This works automatically because `.layout` is configured as a CSS container query context. Any element inside a layout can use these units to size itself relative to the layout's width or height—useful for responsive images, flexible columns, or proportional spacing. For advanced cases where you need to reference a different container (e.g., a specific column), add `style="container-type: size;"` to that element. It must have explicit dimensions set. `w--[50cqw]` `w--[75cqw]` `h--[50cqh]` `50% container width` `75% container width` `50% container height`
50% of layout width
33% of layout height
## Min/Max Dimensions Control minimum and maximum element dimensions independently using min and max classes. These constraints work with all sizing methods—fixed sizes, arbitrary sizes, container query units, and dynamic sizes. ### Fixed Sizes Use `w--min-{size}`, `w--max-{size}`, `h--min-{size}`, and `h--max-{size}` to constrain dimensions using fixed size values. Min Width 72 (288px) Max Width 32 (128px) SizeFixed Sizes
Min Width 72 (288px)
Max Width 32 (128px)
Min Height 72 (288px)
Max Height 8 (32px)
### Arbitrary Sizes Use `w--min-[Npx]`, `w--max-[Npx]`, `h--min-[Npx]`, and `h--max-[Npx]` to constrain dimensions using precise pixel values. `w--min-[100px]` `w--max-[200px]` `h--min-[50px]` `h--max-[150px]` `min-width: 100px` `max-width: 200px` `min-height: 50px` `max-height: 150px` ### Dynamic Sizes Use `w--min-full`, `w--max-full`, `h--min-full`, `h--max-full`, `w--min-auto`, `w--max-auto`, `h--min-auto`, and `h--max-auto` to constrain dynamic dimensions. `w--min-full` `w--max-full` `h--min-auto` `h--max-auto` `min-width: 100%` `max-width: 100%` `min-height: auto` `max-height: none` ### Container Query Sizes Use `w--min-[Ncqw]`, `w--max-[Ncqw]`, `h--min-[Ncqh]`, and `h--max-[Ncqh]` to constrain dimensions relative to the container. `w--min-[100cqw]` `w--max-[50cqw]` `h--min-[75cqh]` `h--max-[90cqh]` `min-width: 100cqw` `max-width: 50cqw` `min-height: 75cqh` `max-height: 90cqh` ## Responsive Sizes Size utilities support responsive variants, allowing you to set different dimensions at different screen breakpoints. Use the pattern `breakpoint:size-class` to apply sizes conditionally. ### Responsive Examples Apply different width and height values at different screen sizes using responsive prefixes. The framework follows a mobile-first approach where styles apply to the target breakpoint and larger. Responsive Width Responsive Height SizeResponsive Sizes
Responsive Width
Responsive Height
Responsive Min Width
Responsive Container Query
### Supported Responsive Classes Responsive variants are available for most size utilities. Use prefixes like `md:`, `portrait:`, and `md:portrait:` to target different breakpoints and orientations. | Category | Responsive Support | Example Usage | | --- | --- | --- | | Fixed Sizes | ✓ Supported | `md:w--16, lg:h--24` | | Full/Auto Dimensions | ✓ Supported | `md:w--full, lg:h--auto` | | Min/Max Dimensions | ✓ Supported | `md:w--min-16, lg:h--max-full` | | Arbitrary Dimensions | ✗ Not Supported | `md:w--[150px], lg:w--[225px]` | | Container Query Sizes | ✓ Supported | `md:w--[50cqw], lg:h--[75cqh]` | Previous [TRMNL X Guide Framework changes for TRMNL X compatibility](/framework/docs/3.1/trmnl_x_guide) Next [Spacing Control element spacing with fixed margin and padding values](/framework/docs/3.1/spacing) --- # Spacing The Spacing system provides utility classes for controlling margins and padding. It includes both fixed sizes and decimal values to handle precise spacing needs. Available spacing sizes and their pixel values [View Size Documentation](/framework/docs/3.1/size) ### Margin Utilities Control element margins using these utility classes. Each class follows the pattern `{property}--{size}` and supports responsive modifiers for **Size** [Size](/framework/docs/3.1/size) , **Orientation**, and **Size + Orientation** [Responsive](/framework/docs/3.1/responsive) . `m--{size}`All sides margin `mt--{size}`Top margin `mr--{size}`Right margin `mb--{size}`Bottom margin `ml--{size}`Left margin `mx--{size}`Horizontal margin `my--{size}`Vertical margin `md:my--{size}`Size-based example `portrait:mx--{size}`Orientation-based example `lg:portrait:mt--{size}`Size + Orientation example ### Padding Utilities Control element padding using these utility classes. Each class follows the pattern `{property}--{size}`. See [Size](/framework/docs/3.1/size) for sizing tokens. `p--{size}`All sides padding `pt--{size}`Top padding `pr--{size}`Right padding `pb--{size}`Bottom padding `pl--{size}`Left padding `px--{size}`Horizontal padding `py--{size}`Vertical padding `sm:px--{size}`Size-based example `portrait:pb--{size}`Orientation-based example `md:portrait:pt--{size}`Size + Orientation example ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--gap-large` | 20px | — | — | — | | `--gap-medium` | 16px | — | — | — | | `--gap-small` | 7px | — | — | — | | `--gap-xlarge` | 30px | — | — | — | | `--gap-xsmall` | 5px | — | — | — | | `--gap-xxlarge` | 40px | — | — | — | | `--list-gap-large` | 16px | — | — | — | | `--list-gap-small` | 8px | — | — | — | Previous [Size Define exact width and height dimensions for elements](/framework/docs/3.1/size) Next [Gap Set precise spacing between elements with predefined gap values](/framework/docs/3.1/gap) --- # Gap The Gap system provides consistent spacing between elements using CSS gap property. It offers predefined sizes, responsive spacing, and custom values to maintain visual rhythm throughout your interface. ### Size Variants The gap system includes predefined base sizes and arbitrary pixel values. These standardized spaces help maintain consistent spacing across your application's grid and flex layouts. #### Base The base `gap` class without size modifiers and the `gap--base` class both produce the same visual result, providing the standard spacing. Use `gap--base` when you need to explicitly set the base size in responsive contexts. See the [Responsive Gaps](#responsive-gap) section for examples. gap--none gap--none gap--none gap--xsmall gap--xsmall gap--xsmall gap--small gap--small gap--small gap gap gap gap--medium gap--medium gap--medium gap--large gap--large gap--large gap--xlarge gap--xlarge gap--xlarge gap--xxlarge gap--xxlarge gap--xxlarge Predefined GapsDesign System
...
...
...
...
...
...
...
...
...
#### Arbitrary Use `gap--[Npx]` syntax to specify exact pixel values from **0px to 50px**. This works with both grid and flex layouts, but does not support responsive variants. gap--[0px] gap--[0px] gap--[0px] gap--[10px] gap--[10px] gap--[10px] gap--[30px] gap--[30px] gap--[30px] gap--[50px] gap--[50px] gap--[50px] Arbitrary Pixel GapsDesign System
...
...
...
...
...
Arbitrary gap values using the `gap--[Npx]` syntax do not support responsive variants. Use predefined gap classes if you need responsive behavior. ### Distribution Modifiers Beyond fixed gaps, you can use special modifiers to control how space is distributed between elements. These modifiers are particularly useful for creating flexible, dynamic layouts. #### Auto Distribution The `gap--auto` modifier distributes available space evenly between elements, including equal spacing at the edges. This uses `justify-content: space-evenly`. gap--auto gap--auto gap--auto Auto Distribution GapDesign System
...
...
...
#### Distribute The `gap--distribute` modifier places the first item at the start of the container and the last item at the end, with equal spacing between items. This uses `justify-content: space-between`. gap--distribute gap--distribute gap--distribute Distribute GapDesign System
First item (at start)
Middle item
Last item (at end)
#### Legacy: Space Between The `gap--space-between` modifier is maintained for backwards compatibility. It behaves the same as `gap--auto`, using `justify-content: space-evenly`. For the actual `space-between` behavior, use `gap--distribute`. ### Responsive Gaps Gap utilities support size-based breakpoints, orientation variants, and their combination. Use prefixes like `md:`, `portrait:`, and `md:portrait:` to target conditions. #### Responsive Gap Examples Apply different gap values at different breakpoints using the size-based responsive system. The framework follows a mobile-first approach where larger breakpoints inherit smaller ones. The `--base` modifier is particularly useful for resetting to the default size at specific breakpoints. Responsive Gap Responsive Gap Responsive Gap Small by default, large on md+, xlarge on lg+, medium gap in portrait, xlarge in md+ portrait Responsive GapsSize-Based
...
...
...
...
...
...
...
...
Gap utilities only support size-based responsive variants. Bit-depth variants (like `1bit:` or `4bit:`) are not available for gap classes. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--gap-large` | 20px | — | — | — | | `--gap-medium` | 16px | — | — | — | | `--gap-small` | 7px | — | — | — | | `--gap-xlarge` | 30px | — | — | — | | `--gap-xsmall` | 5px | — | — | — | | `--gap-xxlarge` | 40px | — | — | — | | `--list-gap-large` | 16px | — | — | — | | `--list-gap-small` | 8px | — | — | — | Previous [Spacing Control element spacing with fixed margin and padding values](/framework/docs/3.1/spacing) Next [Flex Arrange elements with flexible layouts and alignment options](/framework/docs/3.1/flex) --- # Flex The Flex system provides utility classes for creating flexible layouts using Flexbox. It supports both row and column directions with various alignment, centering, and stretching options. ### When to Use Flex Use Flex inside[Layout](/framework/docs/3.1/layout) when you need flexible row or column arrangements. Flex lets items size by their content: width and height follow what's inside, rather than a fixed grid. It's the right choice when you want natural, content-driven layouts without strict column structure. #### Content-Based Sizing Flex items grow and shrink based on their content by default. You can override this with stretch modifiers, grow/shrink utilities, or basis classes. Use Flex when your layout should adapt to the content rather than forcing content into a fixed grid. Examples: toolbars, inline groups of labels and values, or stacks of variable-height cards. #### Standalone or Nested You can use Flex alone as the only child of Layout for simpler layouts. You can also nest Flex inside[Grid](/framework/docs/3.1/grid) . Each grid cell can contain a Flex container for row or column flexibility within that cell. And you can nest Flex inside [Columns](/framework/docs/3.1/columns) columns for per-column arrangement. #### Compared to Grid and Columns Choose Flex when you need flexible, content-sized layouts. If you need strict column alignment and spans, use Grid. If you have lots of same-type data and want the system to handle column distribution and overflow, use[Columns](/framework/docs/3.1/columns) . ### Base Structure The Flex system provides two fundamental ways to organize content: horizontal (row) and vertical (column) arrangements. These base structures can be combined with alignment and stretch modifiers for complex layouts. #### Row Direction Use `flex flex--row` to create a horizontal layout: Item 1 Item 2 Item 3 FlexRow Direction
Item 1
Item 2
Item 3
#### Column Direction Use `flex flex--col` to create a vertical layout: Item 1 Item 2 Item 3 FlexColumn Direction
Item 1
Item 2
Item 3
### Alignment Modifiers Once you've chosen a base direction, you can apply alignment modifiers to control how items are positioned within their container. The system provides directional alignment (left/right/top/bottom) and centering options. #### Row Horizontal Alignment For row layouts, use `flex--left`, `flex--center-x`, or `flex--right` to control horizontal alignment: Left Center X Right FlexRow Horizontal Alignment
Item
Item
Item
Item
Item
Item
Item
Item
Item
#### Row Vertical Alignment For row layouts, use `flex--top`, `flex--center-y`, or `flex--bottom` to control vertical alignment: Top Center Y Bottom FlexRow Vertical Alignment
Item
Item
Item
Item
Item
Item
Item
Item
Item
#### Column Horizontal Alignment For column layouts, use `flex--left`, `flex--center-x`, or `flex--right` to control horizontal alignment: Left Center X Right FlexColumn Horizontal Alignment
Item
Item
Item
Item
Item
Item
Item
Item
Item
#### Column Vertical Alignment For column layouts, use `flex--top`, `flex--center-y`, or `flex--bottom` to control vertical alignment: Top Center Y Bottom FlexColumn Vertical Alignment
Item
Item
Item
### Stretch Modifiers The Flex system provides both container-level and individual item stretch controls. Container modifiers affect all children, while item classes only affect the specific element they're applied to. #### Container Stretch Use `flex--stretch`, `flex--stretch-x`, or `flex--stretch-y` to control how children fill the container: Stretch All Stretch X Stretch Y FlexContainer Stretch
Item
Item
Item
Item
Item
Item
Item
Item
Item
#### Individual Item Stretch (Row) Use `stretch`, `stretch-x`, or `stretch-y` on individual items in a row layout: Stretch Normal Stretch Stretch X Normal Stretch X Stretch Y Normal Stretch Y FlexItem Stretch (Row)
Stretches in cross-axis
Normal item
Stretches in cross-axis
Stretches horizontally
Normal item
Stretches horizontally
Stretches vertically
Normal item
Stretches vertically
#### Individual Item Stretch (Column) Use `stretch`, `stretch-x`, or `stretch-y` on individual items in a column layout: Stretch Normal Stretch Stretch X Normal Stretch X Stretch Y Normal Stretch Y FlexItem Stretch (Column)
Stretches in cross-axis
Normal item
Stretches in cross-axis
Stretches horizontally
Normal item
Stretches horizontally
Stretches vertically
Normal item
Stretches vertically
#### Preventing Item Shrinkage Use `no-shrink` on flex children to prevent them from shrinking when other items try to take up more space: Can Shrink Stretching Content That Pushes Others Won't Shrink Stretching Content That Pushes Others FlexPrevent Shrinking
Maintains its width
Stretches but won't squish the no-shrink item
### Orientation-Responsive Layouts Flexbox utilities support orientation-responsive variants, allowing you to change layouts when the screen is rotated. This is particularly useful for adapting navigation bars, toolbars, and content grids. #### Adaptive Direction Use `portrait:` prefix to change flex direction or alignment when the screen is in portrait orientation. Try rotating the device preview to see the effect. Nav Item 1 Nav Item 2 Nav Item 3 Row in landscape, column in portrait FlexOrientation Responsive
Nav Item 1
Nav Item 2
Nav Item 3
### Extended Directions In addition to standard directions, Flex supports reverse flow for quick reordering on the main axis. Use `flex--row-reverse` and `flex--col-reverse` to invert visual order without changing the DOM. #### Row Reverse A B C FlexRow Reverse
A
B
C
#### Column Reverse 1 2 3 FlexColumn Reverse
1
2
3
### Wrapping and Multi‑Line Alignment Control line breaks with `flex--wrap`, `flex--nowrap`, and `flex--wrap-reverse`. When wrapping, use align‑content modifiers to distribute lines: `flex--content-start|center|end|between|around|evenly|stretch`. #### Wrap vs No‑wrap Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 FlexWrap vs No‑wrap
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
#### Wrapping Item Elements `.item` elements will wrap in flex rows. Team MeetingWeekly team sync-up Team MeetingWeekly team sync-up Team MeetingWeekly team sync-up Team MeetingWeekly team sync-up Team MeetingWeekly team sync-up FlexWrapping Items
Team Meeting Weekly team sync-up
#### Align Content Across Lines These only apply when wrapping is enabled. L1 L2 L3 L4 L5 L6 L7 L8 L1 L2 L3 L4 L5 L6 L7 L8 FlexAlign Content
### Main‑Axis Distribution Use `flex--between`, `flex--around`, and `flex--evenly` to control spacing along the main axis. This differs from `gap`, which inserts physical gaps between items. #### Row Distribution Start Middle End Around Evenly FlexRow Distribution
...
...
...
### Item‑Level Controls Per‑item utilities control alignment and flexing behavior without affecting siblings: self alignment, grow/shrink, flex shorthand, and basis sizing. #### Self Alignment (align-self) self--start self--center self--end self--stretch FlexSelf Alignment
self--start
self--center
self--end
self--stretch
#### Grow/Shrink and Flex Shorthand grow shrink-0 flex-none flex-initial FlexGrow/Shrink & Flex
grow
shrink-0
flex-none
flex-initial
#### Basis and Order basis--36 basis--20 basis--24 order--last order--first order--2 order---1 FlexBasis & Order
basis--36
basis--20
basis--24
order--last
order--first
order--2
order---1
### Inline Flex Containers Use `inline-flex` for inline‑level flex containers that align alongside text. Text before Text after FlexInline Flex Text before
Text after Previous [Gap Set precise spacing between elements with predefined gap values](/framework/docs/3.1/gap) Next [Grid Create grid layouts with predefined column structures](/framework/docs/3.1/grid) --- # Grid The Grid system provides a flexible way to create both column-based and row-based layouts. It supports various column counts, column spans, and responsive behaviors to create complex layouts easily. ### When to Use Grid Use Grid inside[Layout](/framework/docs/3.1/layout) when you need a strict, grid-based layout. Grid gives you precise control over column count and span, so items align to a consistent rhythm and every element snaps to the same underlying grid. #### Grid-Based Distribution You define how many columns the grid has with `grid--cols-{number}`, and you can let individual cells span multiple columns with `col--span-{number}`. The result is a predictable, aligned layout where everything shares the same column structure. Ideal for Swiss-style or editorial designs where visual consistency matters. #### Multiple Grids and Nesting You can place multiple Grid components as direct children of Layout; Layout's modifiers (row/col, alignment, stretch) arrange those grids within the available space. Inside each grid cell, you can nest[Flex](/framework/docs/3.1/flex) for row or column flexibility within that cell. For example, a grid cell that stacks items vertically or aligns them horizontally. #### Compared to Flex and Columns Choose Grid when you need fixed column structure and spans. If you need content-sized flexibility (items that grow or shrink by content), use Flex. If you have lots of same-type data and want the system to handle column distribution and overflow, use[Columns](/framework/docs/3.1/columns) . ### Related [Columns](/framework/docs/3.1/columns)[Flex](/framework/docs/3.1/flex)[Gap](/framework/docs/3.1/gap)[Layout](/framework/docs/3.1/layout) ### Ways to Define the Grid The grid system provides two ways to define column layouts: - **Column Count:** Set `grid--cols-{number}` on the parent to create equal-width columns - **Column Spans:** Set `col--span-{number}` on individual columns to control their width #### Column Count Use `grid--cols-{number}` to specify any number of columns. Here are examples with 4 and 3 columns: Col 1/4 Col 1/4 Col 1/4 Col 1/4 Col 1/3 Col 1/3 Col 1/3 GridColumn Count
1/4
1/4
1/4
1/4
1/3
1/3
1/3
#### Column Spans Use `col--span-{number}` to make a column span multiple grid columns. In a grid row, the sum of all column spans should equal the total number of grid columns. For example, you might have spans of 1 and 2, or spans of 3, 6, and 2. Col Span 1 Col Span 2 Col Span 3 Col Span 6 Col Span 2 GridColumn Spans
Span 1
Span 2
Span 3
Span 6
Span 2
### Column Layouts Use columns to create vertical layouts within the grid. Columns can be positioned and aligned using modifier classes. #### Basic Column Layout Use the `col` class to create vertical layouts. Item 1 Item 2 Item 3 Item 4 GridColumn Layout
Item
Item
Item
Item
#### Column Positioning Use `col--{position}` where position can be `start`, `center`, or `end` to control vertical alignment: Start Center End GridColumn Positioning
Item
Item
Item
### Row Layouts Use rows to create horizontal layouts within the grid. Rows can be positioned and aligned using modifier classes. #### Basic Row Layout Use the `row` class to create horizontal layouts. Item 1 Item 2 Item 3 Item 4 GridRow Layout
Item
Item
Item
Item
#### Row Positioning Use `row--{position}` where position can be `start`, `center`, or `end` to control horizontal alignment: Start Center End GridRow Positioning
Item
Item
Item
### Grid Wrapping Enable responsive wrapping based on a minimum column width using `grid--wrap`. Combine with `grid--min-{size}` to set the minimum track size. #### Different Minimum Sizes As the container shrinks, the grid reduces column count to respect the minimum size. Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 Item 7 Item 8 Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 Item 7 Item 8 GridGrid Wrapping
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Previous [Flex Arrange elements with flexible layouts and alignment options](/framework/docs/3.1/flex) Next [Aspect Ratio Maintain consistent proportions for elements regardless of their content](/framework/docs/3.1/aspect_ratio) --- # Aspect Ratio The Aspect Ratio utility uses the native CSS aspect-ratio property to maintain consistent proportions for elements. Perfect for images, videos, and containers that need to maintain specific width-to-height ratios across different screen sizes. ### Basic Usage Use predefined aspect ratio classes to constrain element dimensions to specific proportions. These utilities apply the CSS `aspect-ratio` property directly to elements. 1:1 16:9 3:4 Aspect RatioBasic Usage
...
...
...
## Available Aspect Ratios Complete reference of all available aspect ratio utilities. | Class | Ratio | | --- | --- | | `aspect--auto` | No constraints | | `aspect--1/1` | 1:1 | | `aspect--4/3` | 4:3 | | `aspect--3/2` | 3:2 | | `aspect--16/9` | 16:9 | | `aspect--21/9` | 21:9 | | `aspect--3/4` | 3:4 | | `aspect--2/3` | 2:3 | | `aspect--9/16` | 9:16 | | `aspect--9/21` | 9:21 | Previous [Grid Create grid layouts with predefined column structures](/framework/docs/3.1/grid) Next [Responsive Adapt styles based on screen width using breakpoint prefixes](/framework/docs/3.1/responsive) --- # Responsive The Responsive system provides two complementary approaches for creating adaptive layouts: **Size-based** breakpoints that respond to screen dimensions, and **Bit-depth** variants that adapt to color capabilities. Together, they enable precise control over how your content appears across TRMNL's diverse range of devices. ## Component Support Not all framework components support responsive variants. We're trying to keep the framework as minimal as we can while offering the features you need. This table shows which responsive features each framework component supports. Use this reference to understand what's possible with each component type. | Component | Size | Orientation | Bit-Depth | Example Usage | | --- | --- | --- | --- | --- | | Background | Yes | Yes | Auto | `md:bg--gray-50` | | Border | No | No | Auto | `border--h-3 (auto adapts)` | | Text | Yes | Yes | Auto | `lg:2bit:text--center` | | Visibility | Yes | Yes | Yes | `sm:1bit:hidden` | | Value | Yes | Yes | No | `md:value--large` | | Label | Yes | Yes | Yes | `md:portrait:2bit:label--filled` | | Spacing | Yes | Yes | No | `md:p--large, lg:m--xlarge, md:portrait:my--24` | | Layout | Yes | Yes | No | `md:layout--row, lg:layout--col` | | Gap | Yes | Yes | No | `md:gap--large, lg:gap--xlarge` | | Flexbox | Yes | Yes | No | `md:flex--center, portrait:flex--col` | | Rounded | Yes | Yes | No | `md:rounded--large, lg:rounded--xlarge` | | Size | Yes | Yes | No | `md:w--large, lg:h--full` | | Grid | Yes | Yes | No | `md:grid--cols-3, md:portrait:col--span-2` | | Clamp | Yes | Yes | No | `data-clamp-md-portrait="3"` | | Overflow (Smart columns) | Yes | Yes | No | `data-overflow-max-cols-lg="4"` | ### Legend Auto Built-in adaptive behavior Yes Full support No Not supported ## Size-Based Responsive ### How It Works Each device automatically sets a size class (e.g., `screen--md`) based on its width, activating the appropriate responsive utilities. The system follows a mobile-first approach. When you use `md:value--large`, it applies on medium screens and larger. ### Basic Usage Prefix any utility class with a breakpoint name followed by a colon. The style applies at that breakpoint and all larger sizes. Responsive Value ResponsiveSize Based This example shows progressive sizing: the text starts at regular size, becomes large on medium screens (md:) and larger, then becomes xlarge on large screens (lg:) and larger. Responsive Value ### Available Breakpoints Three standard breakpoints cover all current supported TRMNL devices. Each breakpoint represents a minimum screen width. | Prefix | Screen Class | Min Width | Example Devices | | --- | --- | --- | --- | | `sm:` | `screen--sm` | 600px | Kindle 2024 | | `md:` | `screen--md` | 800px | TRMNL OG, TRMNL OG V2 | | `lg:` | `screen--lg` | 1024px | TRMNL V2 | ## Bit-Depth Responsive ### How It Works Bit-depth responsiveness adapts styles based on the display's color capabilities. Unlike size-based breakpoints, bit-depth variants are not progressive - each variant targets a specific bit-depth only. When you use `4bit:bg--gray-65`, it applies only on 4-bit screens, not on 1-bit or 2-bit screens. ### Basic Usage Prefix utilities with bit-depth values to create display-specific styles. This is especially useful for optimizing appearance across monochrome and grayscale screens. ResponsiveBit Depth This example demonstrates bit-depth adaptation: the square appears black on 1-bit displays, gray-45 on 2-bit displays, and gray-75 on 4-bit displays. Each bit-depth variant targets only that specific display type.
### Available Bit-Depths The framework supports three bit-depth variants corresponding to TRMNL's display technologies. Each targets specific color capabilities. | Prefix | Screen Class | Color Support | Example Devices | | --- | --- | --- | --- | | `1bit:` | `screen--1bit` | Monochrome (2 shades) | TRMNL OG | | `2bit:` | `screen--2bit` | Grayscale (4 shades) | TRMNL OG V2 | | `4bit:` | `screen--4bit` | Grayscale (16 shades) | TRMNL V2, Kindle 2024 | ## Orientation-Based Responsive ### How It Works Orientation variants adapt styles based on whether the screen is in landscape or portrait mode. Since landscape is the default, only `portrait:` variants are provided to avoid redundancy. Portrait variants are particularly useful for layout utilities like Flexbox, where you might want different flex directions or alignments when the screen is rotated. ### Basic Usage Use the `portrait:` prefix to apply styles only when the screen is in portrait orientation: Item 1 Item 2 Item 3 ResponsiveOrientation Based This example shows orientation-responsive layout: items are arranged in a row by default (landscape), but automatically switch to a column layout when the screen is in portrait orientation using `portrait:flex--col`.
Item 1
Item 2
Item 3
## Combining All Systems The responsive system lets you combine size, orientation, and bit-depth variants. This enables highly targeted designs that adapt to screen dimensions, orientation, and color capabilities. Aa TRMNL OG Aa TRMNL OG V2 Aa TRMNL V2 ResponsiveAdvanced Targeting This advanced example combines size and bit-depth variants to target specific device configurations: `md:1bit:` targets medium+ 1-bit screens, `md:2bit:` targets medium+ 2-bit screens, and `lg:4bit:` targets large+ 4-bit screens. Dark-mode-aware utilities also support a dark-first prefix (for scoped utilities): `dark:md:portrait:2bit:`.
...
...
### Pattern and Order When combining variants, follow this pattern: `size:orientation:bit-depth:utility`. This order flows from general layout concerns to specific display characteristics. Each modifier is optional and can be used independently. You might use just `portrait:flex--col` for orientation-specific layouts, or `md:value--large` for size-responsive typography, depending on your design needs. For utilities that support dark-mode variants (currently Visibility, Background, and Text), use: `dark:size:orientation:bit-depth:utility` with `dark:` as the first prefix. ### Specificity Hierarchy When multiple responsive variants target the same property, CSS specificity determines which style applies. The framework follows a clear hierarchy: the more modifiers in a class, the higher its specificity. For example, `portrait:2bit:value--small` will override both `portrait:value--large` and `2bit:value--medium` when all conditions are met, because it has the most specific combination of modifiers. ### Available Combinations The responsive system supports flexible modifier combinations, allowing you to target specific device configurations. The table below shows all available patterns, from simple single modifiers to complex multi-modifier combinations. Each combination becomes active only when all its conditions are met. | Pattern | Example | When Active | Use Case | | --- | --- | --- | --- | | `size:` | `md:value--large` | Medium screens and larger | Responsive sizing based on screen width | | `orientation:` | `portrait:flex--col` | Portrait orientation only | Layout adjustments for vertical screens | | `bit-depth:` | `4bit:bg--gray-75` | 4-bit displays only | Color optimization for specific displays | | `size:orientation:` | `md:portrait:text--center` | Medium+ screens in portrait | Size-aware orientation layouts | | `size:bit-depth:` | `lg:2bit:value--xlarge` | Large+ screens with 2-bit display | Display-specific sizing on larger screens | | `orientation:bit-depth:` | `portrait:2bit:value--small` | Portrait with 2-bit display | Orientation-aware display optimization | | `size:orientation:bit-depth:` | `md:portrait:4bit:gap--large` | Medium+ screens, portrait, 4-bit display | Highly specific device targeting | | `dark:size:orientation:bit-depth:` | `dark:md:portrait:2bit:hidden` | Dark mode, medium+ screens, portrait, 2-bit display | Theme-specific responsive behavior | Previous [Aspect Ratio Maintain consistent proportions for elements regardless of their content](/framework/docs/3.1/aspect_ratio) Next [Responsive Test Test responsive utilities and compare SCSS mixins with CSS classes](/framework/docs/3.1/responsive_test) --- # Responsive Test This page tests responsive utilities by comparing SCSS mixins with CSS classes across different screen conditions. Each test row shows an element styled with SCSS mixins alongside the same element styled with CSS utility classes. Both columns should look identical when the conditions are met, demonstrating that mixins and classes produce equivalent results. ### Utilities #### Background Test Case SCSS Mixin Result CSS Class Result md:bg--gray-50 @include screen.screen('md') Gray bg on md+ screens portrait:bg--gray-50 @include screen.screen('portrait') Gray bg in portrait 2bit:bg--gray-50 @include screen.screen('2bit') Gray bg on 2-bit screens md:portrait:bg--gray-50 @include screen.screen('md', 'portrait') Gray bg on md+ portrait md:2bit:bg--gray-50 @include screen.screen('md', '2bit') Gray bg on md+ 2-bit portrait:2bit:bg--gray-50 @include screen.screen('portrait', '2bit') Gray bg on portrait 2-bit md:portrait:2bit:bg--gray-50 @include screen.screen('md', 'portrait', '2bit') Gray bg on md+ portrait 2-bit #### Visibility Test Case SCSS Mixin Result CSS Class Result sm:hidden @include screen.screen('sm') Hidden on sm+ screens portrait:hidden @include screen.screen('portrait') Hidden on portrait screens 4bit:hidden @include screen.screen('4bit') Hidden on 4-bit screens md:portrait:hidden @include screen.screen('md', 'portrait') Hidden on md+ portrait screens lg:2bit:hidden @include screen.screen('lg', '2bit') Hidden on lg+ 2-bit screens portrait:4bit:hidden @include screen.screen('portrait', '4bit') Hidden on portrait 4-bit screens md:portrait:2bit:hidden @include screen.screen('md', 'portrait', '2bit') Hidden on md+ portrait 2-bit screens #### Text Test Case SCSS Mixin Result CSS Class Result lg:text--center @include screen.screen('lg') Centered text on lg+ screens Aa Aa portrait:text--center @include screen.screen('portrait') Centered text in portrait Aa Aa 2bit:text--center @include screen.screen('2bit') Centered text on 2-bit screens Aa Aa md:portrait:text--center @include screen.screen('md', 'portrait') Centered on md+ portrait Aa Aa lg:4bit:text--center @include screen.screen('lg', '4bit') Centered on lg+ 4-bit screens Aa Aa portrait:2bit:text--center @include screen.screen('portrait', '2bit') Centered on portrait 2-bit screens Aa Aa md:portrait:2bit:text--right @include screen.screen('md', 'portrait', '2bit') Right-aligned on md+ portrait 2-bit Aa Aa #### Flex Test Case SCSS Mixin Result CSS Class Result md:flex--center @include screen.screen('md') Centered on md+ screens portrait:flex--col @include screen.screen('portrait') Column layout in portrait lg:portrait:flex--center @include screen.screen('lg', 'portrait') Centered on lg+ portrait #### Spacing Test Case SCSS Mixin Result CSS Class Result md:p--24 @include screen.screen('md') Padding 24 on md+ screens portrait:mx--20 @include screen.screen('portrait') Horizontal margin 20 in portrait #### Gap Test Case SCSS Mixin Result CSS Class Result lg:gap--xlarge @include screen.screen('lg') Gap xlarge on lg+ screens portrait:gap--large @include screen.screen('portrait') Large gap in portrait #### Size Test Case SCSS Mixin Result CSS Class Result md:w--36 @include screen.screen('md') Large width on md+ screens #### Rounded Test Case SCSS Mixin Result CSS Class Result md:rounded--xlarge @include screen.screen('md') Rounded xlarge on md+ screens #### Grid Test Case SCSS Mixin Result CSS Class Result md:grid--cols-3 @include screen.screen('md') 3 columns on md+ screens ### Base #### Layout Test Case SCSS Mixin Result CSS Class Result md:layout--col @include screen.screen('md') Column layout on md+ screens portrait:layout--bottom @include screen.screen('portrait') Bottom alignment in portrait lg:portrait:layout--bottom @include screen.screen('lg', 'portrait') Bottom alignment on lg+ portrait ### Elements #### Value Test Case SCSS Mixin Result CSS Class Result md:value--large Large value on md+ screens Aa portrait:value--large Large value in portrait Aa 4bit:value--large Large value on 4-bit screens Aa lg:portrait:value--large Large value on lg+ portrait Aa md:2bit:value--large Large value on md+ 2-bit screens Aa portrait:4bit:value--large Large value on portrait 4-bit Aa lg:portrait:4bit:value--xlarge XLarge on lg+ portrait 4-bit Aa #### Label Test Case SCSS Mixin Result CSS Class Result md:label--small Small label on md+ screens Label portrait:label--outline Outlined label in portrait Label 2bit:label--inverted Inverted label on 2-bit screens Label md:portrait:label--underline Underlined label on md+ portrait Label md:2bit:label--gray Gray label on md+ 2-bit Label portrait:2bit:label--small Small label on portrait 2-bit Label md:portrait:2bit:label--inverted Inverted label on md+ portrait 2-bit Label ### Components No component tests have been implemented yet Previous [Responsive Adapt styles based on screen width using breakpoint prefixes](/framework/docs/3.1/responsive) Next [Visibility Control element visibility based on display bit depth](/framework/docs/3.1/visibility) --- # Visibility The visibility and display utilities provide comprehensive control over element visibility and display types across devices. They include hidden/visible controls and display helpers like flex, grid, and inline, with responsive and bit-depth variants for device-specific layouts. ## Visibility Across Devices See how visibility classes behave across different screen sizes. Each column represents a different device size. Small (600px) visible md:hidden Medium (800px) hidden md:visible lg:hidden Large (1024px) hidden lg:visible
visible
md:hidden
lg:hidden
## Display Utilities Control how elements are displayed with specific display types. These classes set the CSS `display` property. | Class | Effect | CSS Output | | --- | --- | --- | | `hidden` | Hide element completely | `display: none` | | `visible` | Display as block element | `display: block` | | `block` | Display as block element | `display: block` | | `inline` | Display as inline element | `display: inline` | | `inline-block` | Display as inline block element | `display: inline-block` | | `flex` | Display as flex container | `display: flex` | | `grid` | Display as grid container | `display: grid` | | `inline-grid` | Display as inline grid container | `display: inline-grid` | | `table` | Display as table element | `display: table` | | `table-row` | Display as table row element | `display: table-row` | | `table-cell` | Display as table cell element | `display: table-cell` | ## Responsive Display Control All display utilities work with responsive prefixes[Responsive](/framework/docs/3.1/responsive) . Size prefixes are mobile-first (apply at that size and larger). You can combine any display utility with responsive breakpoints [Responsive](/framework/docs/3.1/responsive) . | Example Class | Effect | Active On | | --- | --- | --- | | `sm:hidden` | Hide on small screens and larger | All devices (600px+) | | `md:flex` | Display as flex on medium screens and larger | TRMNL OG, TRMNL V2 (800px+) | | `lg:grid` | Display as grid on large screens | TRMNL V2 (1024px+) | | `sm:inline-block` | Display as inline-block on small screens and larger | All devices (600px+) |
Block by default, flex on medium+
Changes display type at each breakpoint
Cell A
Cell B
## Bit-Depth Display Control All display utilities work with bit-depth prefixes to target specific display capabilities. Perfect for optimizing layouts for different ePaper displays. | Example Class | Effect | Active On | | --- | --- | --- | | `1bit:hidden` | Hide on monochrome displays | TRMNL OG only | | `2bit:flex` | Display as flex on 4-shade grayscale displays | TRMNL OG V2 only | | `4bit:grid` | Display as grid on 16-shade grayscale displays | TRMNL V2, Kindle 2024 only | ## Device-Specific Display Control Combine size and bit-depth prefixes with any display utility to target specific devices precisely. Use the pattern: `size:bit-depth:display` | Example Class | Target Device | Effect | | --- | --- | --- | | `md:1bit:block` | TRMNL OG only | Display as block (800px, monochrome) | | `md:2bit:flex` | TRMNL OG V2 only | Display as flex (800px, 4-shade grayscale) | | `lg:4bit:grid` | TRMNL V2 only | Display as grid (1024px, 16-shade grayscale) | | `sm:4bit:table` | Kindle 2024 only | Display as table (600px, 16-shade grayscale) |
Simple layouts for lower bit-depth displays
## Dark Mode Display Control Visibility utilities support dark-first prefixes for screen dark mode targeting. Use `dark:` to show or hide content by screen dark mode. Light-mode behavior is the default state.
Dark mode hides this
Hidden on dark medium+ portrait 2-bit screens
Previous [Responsive Test Test responsive utilities and compare SCSS mixins with CSS classes](/framework/docs/3.1/responsive_test) Next [Background Apply color tokens as backgrounds with bg--{token}](/framework/docs/3.1/background) --- # Background Use the color palette defined in [Colors](/framework/docs/3.1/colors). Apply these shades with bg--{token} for backgrounds. On 1-bit displays, grayscale uses dither patterns; on 2-bit and 4-bit+, solid colors render. ### Grayscale Grayscale background shades only, including the center spacer between 40 and 45. black 10 15 20 25 30 35 40 45 50 55 60 65 70 75 white Grayscale backgrounds **Dark Mode Notice:** The color palette appears inverted because TRMNL's dark mode inverts the entire screen, except the images. ### Base Colors Full base palettes for background tokens: grayscale and all chromatic hues with every shade step. 10 15 20 25 30 35 40 base 45 50 55 60 65 70 75 Base background colors ### Usage Use the `bg--{shade}` utility classes to apply these background patterns to any element. Grayscale: black, gray-10 through gray-75, and white. Chromatic: `bg--{hue}` (pure color, e.g. bg--red, bg--green) or `bg--{hue}-{step}` (e.g. bg--red-50, bg--blue-40). Semantic: `bg--primary`, `bg--success`, `bg--error`, etc. (see [Colors](/framework/docs/3.1/colors)).
Black
Gray 10
Gray 15
Gray 20
Gray 25
Gray 30
Gray 35
Gray 40
Gray 45
Gray 50
Gray 55
Gray 60
Gray 65
Gray 70
Gray 75
White
**Device Preview tip:** Use the Device Preview (top right) to switch between grayscale and color palettes. Try Inky Impression 7.3 (color-7a) or Tidbyt (color-24bit) to see chromatic colors. #### Chromatic tokens Use `bg--{hue}-{step}` and `text--{hue}-{step}` for color backgrounds and text.
Pure red
Red 50
Blue 40
Green 60
Red text
#### Semantic tokens Use `bg--{role}` and `text--{role}` for intent-based colors. Roles: primary, success, error, warning. See [Colors](/framework/docs/3.1/colors) for the full mapping.
Primary
Success
Error
Warning text
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Semantic | | | | | | `--black` | #000000 | — | — | — | | `--color-error` | var(--red) | — | — | — | | `--color-primary` | var(--blue) | — | — | — | | `--color-success` | var(--green) | — | — | — | | `--color-warning` | var(--orange) | — | — | — | | `--white` | #FFFFFF | — | — | — | | Grayscale | | | | | | `--gray-10` | #111111 | — | — | — | | `--gray-15` | #222222 | — | — | — | | `--gray-20` | #333333 | — | — | — | | `--gray-25` | #444444 | — | — | — | | `--gray-30` | #555555 | — | — | — | | `--gray-35` | #666666 | — | — | — | | `--gray-40` | #777777 | — | — | — | | `--gray-45` | #888888 | — | — | — | | `--gray-50` | #999999 | — | — | — | | `--gray-55` | #AAAAAA | — | — | — | | `--gray-60` | #BBBBBB | — | — | — | | `--gray-65` | #CCCCCC | — | — | — | | `--gray-70` | #DDDDDD | — | — | — | | `--gray-75` | #EEEEEE | — | — | — | | Legacy Grayscale | | | | | | `--gray-1` | #111111 | — | — | — | | `--gray-2` | #333333 | — | — | — | | `--gray-3` | #555555 | — | — | — | | `--gray-4` | #777777 | — | — | — | | `--gray-5` | #999999 | — | — | — | | `--gray-6` | #BBBBBB | — | — | — | | `--gray-7` | #DDDDDD | — | — | — | Previous [Visibility Control element visibility based on display bit depth](/framework/docs/3.1/visibility) Next [Border Apply border patterns that create the illusion of different border intensities](/framework/docs/3.1/border) --- # Border The Border system creates the illusion of grayscale borders through carefully designed dither patterns. When rendered on 1-bit displays, these patterns create varying border intensities using alternating black and white pixels. ### Usage Apply borders using `border--h-{n}` for horizontal borders and `border--v-{n}` for vertical borders, where n ranges from 1 (black) to 7 (white), with dithered values in between. #### Horizontal Borders 1 2 3 4 5 6 7 1 2 3 4 5 6 7 Horizontal Borders **Dark Mode Notice:** The color palette appears inverted because TRMNL's dark mode inverts the entire screen, except the images.
Horizontal Border 1
Horizontal Border 2
Horizontal Border 3
Horizontal Border 4
Horizontal Border 5
Horizontal Border 6
Horizontal Border 7
#### Vertical Borders 1 2 3 4 5 6 7 1 2 3 4 5 6 7 Vertical Borders
Vertical Border 1
Vertical Border 2
Vertical Border 3
Vertical Border 4
Vertical Border 5
Vertical Border 6
Vertical Border 7
### Backward Compatibility The Border utility in Framework v2 is **not backward compatible** with the legacy v1 Border utility. This is the only non-backward compatible utility in the new framework. #### What changed? - The visual scale has been redefined to a full-spectrum grayscale that works on *any* background shade: `1` = black … `7` = white. - In v1, borders only produced a faux grayscale on white surfaces and appeared solid black (invisible) on black surfaces. In v2, borders render consistently on both light and dark backgrounds. - Class names remain the same (`border--h-{n}`, `border--v-{n}`), but the visual output for a given `{n}` looks different. #### How to upgrade existing plugins - Keep your markup unchanged. Continue using `border--h-{n}` and `border--v-{n}`. - Re-evaluate the chosen `{n}` values based on the new scale. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--rounded-full` | 9999px | — | — | — | | `--rounded-large` | 20px | — | — | — | | `--rounded-medium` | 15px | — | — | — | | `--rounded-none` | 0px | — | — | — | | `--rounded-small` | 7px | — | — | — | | `--rounded-xlarge` | 25px | — | — | — | | `--rounded-xsmall` | 5px | — | — | — | | `--rounded-xxlarge` | 30px | — | — | — | Previous [Background Apply color tokens as backgrounds with bg--{token}](/framework/docs/3.1/background) Next [Rounded Control element rounding with predefined values](/framework/docs/3.1/rounded) --- # Rounded The Rounded system provides consistent border radius values for creating smooth corners on elements. It offers predefined sizes, corner-specific controls, and custom values to maintain visual consistency throughout your interface. ### Size Variants The rounded system includes predefined base sizes and arbitrary pixel values. These standardized radii help maintain consistent corner rounding across your application's components. #### Base The base `rounded` class without size modifiers and the `rounded--base` class both produce the same visual result, providing the standard border radius (10px). Use `rounded--base` when you need to explicitly set the base size in responsive contexts. See the [Responsive Rounded](#responsive-rounded) section for examples. rounded--none rounded--xsmall rounded--small rounded rounded--medium rounded--large rounded--xlarge rounded--xxlarge rounded--full Predefined RoundedDesign System
...
...
...
...
...
...
...
...
...
...
...
#### Arbitrary Use `rounded--[Npx]` syntax to specify exact pixel values from **0px to 50px**. This works with all rounded utilities, but does not support responsive variants. rounded--[0px] rounded--[10px] rounded--[30px] rounded--[20px] rounded--[40px] rounded--[50px] Arbitrary Pixel RoundedDesign System
...
...
...
...
...
...
...
Arbitrary rounded values using the `rounded--[Npx]` syntax do not support responsive variants. Use predefined rounded classes if you need responsive behavior. ### Corner-Specific Rounding Apply border radius to specific corners or sides of an element. This allows for more complex shapes and asymmetric designs while maintaining consistency. #### Individual Corners Target specific corners with `rounded-{corner}{-size}` where corner can be tl (top-left), tr (top-right), br (bottom-right), or bl (bottom-left). rounded-tl--large rounded-tr--large rounded-bl--large rounded-br--large Corner-Specific RoundingDesign System #### Side Rounding Round entire sides with `rounded-{side}{-size}` where side can be t (top), r (right), b (bottom), or l (left). rounded-t--large rounded-r--large rounded-b--large rounded-l--large Side RoundingDesign System
Top left corner
Top right corner
Bottom right corner
Bottom left corner
Top corners
Right corners
Bottom corners
Left corners
### Responsive Rounded Rounded utilities support size-based breakpoints, orientation variants, and their combination. Use prefixes like `md:`, `portrait:`, and `md:portrait:` to target conditions. #### Base Examples Apply different border radius values at different breakpoints using the size-based responsive system. The framework follows a mobile-first approach where larger breakpoints inherit smaller ones. The `--base` modifier is particularly useful for resetting to the default size at specific breakpoints. Responsive Xlarge in landscape, small in portrait Responsive RoundedSize-Based
Xlarge in landscape, small in portrait
#### Corner-Specific Examples Corner-specific rounding utilities support responsive variants just like base rounded utilities. Use prefixes like `md:`, `portrait:`, and `md:portrait:` to apply different corner rounding at different breakpoints. Responsive Xlarge in landscape, small in portrait Responsive Corner RoundingSize-Based
Xlarge in landscape, small in portrait
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--progress-bar-radius` | 10px | — | — | — | | `--rounded-full` | 9999px | — | — | — | | `--rounded-large` | 20px | — | — | — | | `--rounded-medium` | 15px | — | — | — | | `--rounded-none` | 0px | — | — | — | | `--rounded-small` | 7px | — | — | — | | `--rounded-xlarge` | 25px | — | — | — | | `--rounded-xsmall` | 5px | — | — | — | | `--rounded-xxlarge` | 30px | — | — | — | | `--title-bar-border-radius` | 10px | 10px | — | 10px | Previous [Border Apply border patterns that create the illusion of different border intensities](/framework/docs/3.1/border) Next [Outline Pixel-perfect rounded borders using border-image for 1-bit displays](/framework/docs/3.1/outline) --- # Outline The Outline utility provides pixel-perfect rounded borders using CSS border-image with a 9-slice composite image. On 1-bit displays, it renders crisp, dithered corner patterns that scale with the element. On 2-bit and 4-bit displays, it falls back to standard CSS borders with border-radius. ### Basic Usage The outline utility applies a pixel-perfect dotted rounded border to any element. On 1-bit displays, it uses pure CSS gradients to place single-pixel dots at exact integer coordinates along a rounded rectangle path. On 2-bit and 4-bit displays, it falls back to a standard CSS border with border-radius. #### Applying an Outline Add the `outline` class to any element to give it a pixel-perfect rounded border. With outline Without outline Outline UtilityDesign System
Content with pixel-perfect rounded border
### How It Works The outline utility uses 20 CSS background layers to place each dot at an exact integer pixel coordinate. Edge dots use `repeating-linear-gradient` for a 1px dot every 4px. Corner dots use individual `linear-gradient` blocks sized to 1x1px and positioned with pixel-precise offsets. #### CSS Gradient Dots No images are used. Each dot is computed mathematically by the CSS engine, guaranteeing pixel-grid alignment at any element size. The border color comes from `--framework-border-strong`, so dark mode works automatically without separate assets. /* How the CSS works internally (simplified) */ .outline::after { background: /* Edges: repeating 1px dot every 4px */ repeating-linear-gradient(to right, black 0 1px, transparent 1px 4px) 12px 0 / calc(100% - 24px) 1px no-repeat, /* ... 3 more edges ... */ /* Corners: individual 1x1px dots */ linear-gradient(black, black) 8px 0 / 1px 1px no-repeat, linear-gradient(black, black) 4px 1px / 1px 1px no-repeat, /* ... 14 more corner dots ... */ } ### Bit-Depth Behavior The outline utility adapts to different display bit-depths automatically. On 1-bit displays, it uses CSS gradient dots for pixel-perfect rendering. On 2-bit and 4-bit displays, it falls back to standard CSS borders with border-radius for smoother rendering. #### 1-bit Displays Uses pure CSS gradients to place sparse single-pixel dots at exact integer coordinates. Dark mode works automatically via `--framework-border-strong` which inverts to white. #### 2-bit and 4-bit Displays Falls back to a standard 1px solid border with 10px border-radius for smoother rendering that takes advantage of the additional grayscale capabilities. /* 1-bit: CSS gradient dots (via outline-dots mixin) */ .outline::after { @include outline-dots; } /* 2-bit and 4-bit: Falls back to CSS border */ .screen--2bit .outline::after, .screen--4bit .outline::after { background: none; border: 1px solid var(--framework-border-strong); border-radius: 10px; } ### Screen Backdrop Modifier For mashup layouts, the `screen--backdrop` modifier provides an alternative appearance where views sit on a patterned background instead of having outlined borders. #### Default vs Backdrop Mashups By default, mashups use a white background with bordered views for a clean, separated look. The `screen--backdrop` modifier changes this to a patterned background (1-bit) or solid gray background (2-bit/4-bit) with plain white views on top. Plugin A Plugin B
...
...
...
...
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--rounded-full` | 9999px | — | — | — | | `--rounded-large` | 20px | — | — | — | | `--rounded-medium` | 15px | — | — | — | | `--rounded-none` | 0px | — | — | — | | `--rounded-small` | 7px | — | — | — | | `--rounded-xlarge` | 25px | — | — | — | | `--rounded-xsmall` | 5px | — | — | — | | `--rounded-xxlarge` | 30px | — | — | — | Previous [Rounded Control element rounding with predefined values](/framework/docs/3.1/rounded) Next [Image Optimize images using dithering techniques for 1-bit rendering](/framework/docs/3.1/image) --- # Image Image creates the illusion of grayscale through carefully designed dither patterns. When rendered on 1-bit (black and white only) displays, these patterns create an illusion of different shades of gray by using specific arrangements of black and white pixels. ### Dithering Use the class `image-dither` to dither an image. ![Plugin icon](/images/framework/image/image--1bit.png)![Plugin icon](/images/framework/image/image--2bit.png)![Plugin icon](/images/framework/image/image--4bit.png) Image ### Object Fit Control how images are displayed when not shown in their original aspect ratio. #### Options - **Fill:** The image is resized to fill the given dimension. If necessary, the image will be stretched or squished to fit. - **Contain:** The image keeps its aspect ratio, but is resized to fit within the given dimension. - **Cover:** The image keeps its aspect ratio and fills the given dimension. The image will be clipped to fit. ![Plugin icon](/images/screensaver/rover.bmp) Fill ![Plugin icon](/images/screensaver/rover.bmp) Contain ![Plugin icon](/images/screensaver/rover.bmp) Cover Object Fit Options Previous [Outline Pixel-perfect rounded borders using border-image for 1-bit displays](/framework/docs/3.1/outline) Next [Image Stroke Legible images when displayed on shaded backgrounds](/framework/docs/3.1/image_stroke) --- # Image Stroke The Image Stroke system allows you to add an outline to a vector or transparent raster images with customizable stroke width and color. This is useful for creating images that stand out against shaded backgrounds. ### Usage The Image Stroke system includes preset size modifiers that allow you to quickly apply different stroke widths to your images. The default stroke is 1.5px white, with additional options for base (1.5px, equivalent to default), small (1px), medium (2px), large (2.5px), and extra large (3px). The `image-stroke--base` modifier explicitly sets the default stroke width and is useful for responsive layouts. No Stroke Small Base Default Medium Large Extra Large Image StrokePreset Sizes ### Stroke Colors Use the black modifier for images on dark backgrounds. No Stroke Small Base Default Medium Large Extra Large Image StrokeColor Variants Previous [Image Optimize images using dithering techniques for 1-bit rendering](/framework/docs/3.1/image) Next [Scale Scale interface to affect content density and readability](/framework/docs/3.1/scale) --- # Scale The Scale system provides utility classes to scale the entire interface by adjusting the UI scale factor. This is useful for adapting content density for different viewing distances or user preferences. ### Basic Usage Apply scale modifiers to the `screen` element to scale all interface elements proportionally. The scale affects fonts, spacing, dimensions, and other UI elements that use the `--ui-scale` CSS variable. Scale is available on 4bit devices. #### Available Scale Levels The framework provides six predefined scale levels: | Class | Scale Factor | Use Case | | --- | --- | --- | | `screen--scale-xsmall` | 0.75 (75%) | Maximum content density | | `screen--scale-small` | 0.875 (87.5%) | Increased content density | | `screen--scale-regular` | 1.0 (100%) | Default scale, no scaling applied | | `screen--scale-large` | 1.125 (112.5%) | Increased size for better readability | | `screen--scale-xlarge` | 1.25 (125%) | Large scale for increased readability | | `screen--scale-xxlarge` | 1.5 (150%) | Maximum scale for accessibility needs | ### Scale Examples The following examples demonstrate how scale levels affect the same content layout. Notice how all elements scale proportionally. #### Extra Small Scale (75%) Maximum content density - useful when viewing up close or when you need to fit more information on screen. Today 1 Morning Meeting: Threat Level Check-inTeam sync and updates 9:00 AM - 9:30 AMDaily 2 Identity Theft WatchReview suspicious 'Jim' behaviours 10:30 AM - 11:30 AMReview 3 Lunch Break: Pretzel Day PrepTeam lunch at downtown 12:30 PM - 1:30 PMBreak 4 Client Call with JanWeekly check-in with stakeholders 2:00 PM - 3:00 PMClient 5 Complaint Sorting: Product RecallPrioritize reported issues 3:30 PM - 4:30 PMComplaints 6 Bulletin Board Update: DundiesUpdate nominations and categories 4:30 PM - 5:30 PMDocs 7 End of Day Sync: Café DiscoReview progress and blockers 5:30 PM - 6:00 PMSync Tomorrow 1 Beach Games Roll-CallConfirm capacity without hot coals 10:00 AM - 12:00 PMPlanning 2 Stakeholder Presentation: Threat Level MidnightTasteful metrics, minimal fireworks 2:00 PM - 3:30 PMPresentation 3 Oscar’s Index Intervention (Of Spreadsheets)Deep dive into the budget tabs 9:00 AM - 11:00 AMNumbers 4 Parkour QA Gauntlet (Very Gentle)Functionality verified: walking 1:00 PM - 3:00 PMQA-ish 5 Campaign Analysis: WUPHF Without The WUPHFLess shouting, more smiling 4:00 PM - 5:30 PMMarketing This Week 1 Warehouse to Cloud (No Forklifts)Move boxes, label feelings WednesdayInfrastructure-ish 2 Customer Satisfaction Review: 'Did I Stutter?'Improve smiles per hour ThursdayCustomer Success 3 Benihana to Back Office CoordinationWe will know who is who FridayIntegration-ish 4 Data Deep Dive: Boom, Roasted (With Charts)Roasts limited to pie charts MondayAnalytics 5 Accessibility: Conference Room B UpgradesLess squinting, more seeing TuesdayAccessibility 6 Respect the Dashboard (Of Feelings)Set baselines for vibes WednesdayMonitoring 7 The Dundies of GrowthSkills, mentoring, zero karaoke tears FridayDevelopment Scale LevelExtra Small (75%)
#### Small Scale (87.5%) Reduced scale for fitting more content while maintaining good readability. Today 1 Morning Meeting: Threat Level Check-inTeam sync and updates 9:00 AM - 9:30 AMDaily 2 Identity Theft WatchReview suspicious 'Jim' behaviours 10:30 AM - 11:30 AMReview 3 Lunch Break: Pretzel Day PrepTeam lunch at downtown 12:30 PM - 1:30 PMBreak 4 Client Call with JanWeekly check-in with stakeholders 2:00 PM - 3:00 PMClient 5 Complaint Sorting: Product RecallPrioritize reported issues 3:30 PM - 4:30 PMComplaints 6 Bulletin Board Update: DundiesUpdate nominations and categories 4:30 PM - 5:30 PMDocs 7 End of Day Sync: Café DiscoReview progress and blockers 5:30 PM - 6:00 PMSync Tomorrow 1 Beach Games Roll-CallConfirm capacity without hot coals 10:00 AM - 12:00 PMPlanning 2 Stakeholder Presentation: Threat Level MidnightTasteful metrics, minimal fireworks 2:00 PM - 3:30 PMPresentation 3 Oscar’s Index Intervention (Of Spreadsheets)Deep dive into the budget tabs 9:00 AM - 11:00 AMNumbers 4 Parkour QA Gauntlet (Very Gentle)Functionality verified: walking 1:00 PM - 3:00 PMQA-ish 5 Campaign Analysis: WUPHF Without The WUPHFLess shouting, more smiling 4:00 PM - 5:30 PMMarketing This Week 1 Warehouse to Cloud (No Forklifts)Move boxes, label feelings WednesdayInfrastructure-ish 2 Customer Satisfaction Review: 'Did I Stutter?'Improve smiles per hour ThursdayCustomer Success 3 Benihana to Back Office CoordinationWe will know who is who FridayIntegration-ish 4 Data Deep Dive: Boom, Roasted (With Charts)Roasts limited to pie charts MondayAnalytics 5 Accessibility: Conference Room B UpgradesLess squinting, more seeing TuesdayAccessibility 6 Respect the Dashboard (Of Feelings)Set baselines for vibes WednesdayMonitoring 7 The Dundies of GrowthSkills, mentoring, zero karaoke tears FridayDevelopment Scale LevelSmall (87.5%)
#### Regular Scale (100%) Default scale - this is the baseline that all other scale levels are relative to. Today 1 Morning Meeting: Threat Level Check-inTeam sync and updates 9:00 AM - 9:30 AMDaily 2 Identity Theft WatchReview suspicious 'Jim' behaviours 10:30 AM - 11:30 AMReview 3 Lunch Break: Pretzel Day PrepTeam lunch at downtown 12:30 PM - 1:30 PMBreak 4 Client Call with JanWeekly check-in with stakeholders 2:00 PM - 3:00 PMClient 5 Complaint Sorting: Product RecallPrioritize reported issues 3:30 PM - 4:30 PMComplaints 6 Bulletin Board Update: DundiesUpdate nominations and categories 4:30 PM - 5:30 PMDocs 7 End of Day Sync: Café DiscoReview progress and blockers 5:30 PM - 6:00 PMSync Tomorrow 1 Beach Games Roll-CallConfirm capacity without hot coals 10:00 AM - 12:00 PMPlanning 2 Stakeholder Presentation: Threat Level MidnightTasteful metrics, minimal fireworks 2:00 PM - 3:30 PMPresentation 3 Oscar’s Index Intervention (Of Spreadsheets)Deep dive into the budget tabs 9:00 AM - 11:00 AMNumbers 4 Parkour QA Gauntlet (Very Gentle)Functionality verified: walking 1:00 PM - 3:00 PMQA-ish 5 Campaign Analysis: WUPHF Without The WUPHFLess shouting, more smiling 4:00 PM - 5:30 PMMarketing This Week 1 Warehouse to Cloud (No Forklifts)Move boxes, label feelings WednesdayInfrastructure-ish 2 Customer Satisfaction Review: 'Did I Stutter?'Improve smiles per hour ThursdayCustomer Success 3 Benihana to Back Office CoordinationWe will know who is who FridayIntegration-ish 4 Data Deep Dive: Boom, Roasted (With Charts)Roasts limited to pie charts MondayAnalytics 5 Accessibility: Conference Room B UpgradesLess squinting, more seeing TuesdayAccessibility 6 Respect the Dashboard (Of Feelings)Set baselines for vibes WednesdayMonitoring 7 The Dundies of GrowthSkills, mentoring, zero karaoke tears FridayDevelopment Scale LevelRegular (100%)
#### Large Scale (112.5%) Increased size for better readability Today 1 Morning Meeting: Threat Level Check-inTeam sync and updates 9:00 AM - 9:30 AMDaily 2 Identity Theft WatchReview suspicious 'Jim' behaviours 10:30 AM - 11:30 AMReview 3 Lunch Break: Pretzel Day PrepTeam lunch at downtown 12:30 PM - 1:30 PMBreak 4 Client Call with JanWeekly check-in with stakeholders 2:00 PM - 3:00 PMClient 5 Complaint Sorting: Product RecallPrioritize reported issues 3:30 PM - 4:30 PMComplaints 6 Bulletin Board Update: DundiesUpdate nominations and categories 4:30 PM - 5:30 PMDocs 7 End of Day Sync: Café DiscoReview progress and blockers 5:30 PM - 6:00 PMSync Tomorrow 1 Beach Games Roll-CallConfirm capacity without hot coals 10:00 AM - 12:00 PMPlanning 2 Stakeholder Presentation: Threat Level MidnightTasteful metrics, minimal fireworks 2:00 PM - 3:30 PMPresentation 3 Oscar’s Index Intervention (Of Spreadsheets)Deep dive into the budget tabs 9:00 AM - 11:00 AMNumbers 4 Parkour QA Gauntlet (Very Gentle)Functionality verified: walking 1:00 PM - 3:00 PMQA-ish 5 Campaign Analysis: WUPHF Without The WUPHFLess shouting, more smiling 4:00 PM - 5:30 PMMarketing This Week 1 Warehouse to Cloud (No Forklifts)Move boxes, label feelings WednesdayInfrastructure-ish 2 Customer Satisfaction Review: 'Did I Stutter?'Improve smiles per hour ThursdayCustomer Success 3 Benihana to Back Office CoordinationWe will know who is who FridayIntegration-ish 4 Data Deep Dive: Boom, Roasted (With Charts)Roasts limited to pie charts MondayAnalytics 5 Accessibility: Conference Room B UpgradesLess squinting, more seeing TuesdayAccessibility 6 Respect the Dashboard (Of Feelings)Set baselines for vibes WednesdayMonitoring 7 The Dundies of GrowthSkills, mentoring, zero karaoke tears FridayDevelopment Scale LevelLarge (112.5%)
#### Extra Large Scale (125%) Large scale for increased readability Today 1 Morning Meeting: Threat Level Check-inTeam sync and updates 9:00 AM - 9:30 AMDaily 2 Identity Theft WatchReview suspicious 'Jim' behaviours 10:30 AM - 11:30 AMReview 3 Lunch Break: Pretzel Day PrepTeam lunch at downtown 12:30 PM - 1:30 PMBreak 4 Client Call with JanWeekly check-in with stakeholders 2:00 PM - 3:00 PMClient 5 Complaint Sorting: Product RecallPrioritize reported issues 3:30 PM - 4:30 PMComplaints 6 Bulletin Board Update: DundiesUpdate nominations and categories 4:30 PM - 5:30 PMDocs 7 End of Day Sync: Café DiscoReview progress and blockers 5:30 PM - 6:00 PMSync Tomorrow 1 Beach Games Roll-CallConfirm capacity without hot coals 10:00 AM - 12:00 PMPlanning 2 Stakeholder Presentation: Threat Level MidnightTasteful metrics, minimal fireworks 2:00 PM - 3:30 PMPresentation 3 Oscar’s Index Intervention (Of Spreadsheets)Deep dive into the budget tabs 9:00 AM - 11:00 AMNumbers 4 Parkour QA Gauntlet (Very Gentle)Functionality verified: walking 1:00 PM - 3:00 PMQA-ish 5 Campaign Analysis: WUPHF Without The WUPHFLess shouting, more smiling 4:00 PM - 5:30 PMMarketing This Week 1 Warehouse to Cloud (No Forklifts)Move boxes, label feelings WednesdayInfrastructure-ish 2 Customer Satisfaction Review: 'Did I Stutter?'Improve smiles per hour ThursdayCustomer Success 3 Benihana to Back Office CoordinationWe will know who is who FridayIntegration-ish 4 Data Deep Dive: Boom, Roasted (With Charts)Roasts limited to pie charts MondayAnalytics 5 Accessibility: Conference Room B UpgradesLess squinting, more seeing TuesdayAccessibility 6 Respect the Dashboard (Of Feelings)Set baselines for vibes WednesdayMonitoring 7 The Dundies of GrowthSkills, mentoring, zero karaoke tears FridayDevelopment Scale LevelExtra Large (125%)
#### Extra Extra Large Scale (150%) Maximum scale for accessibility needs Today 1 Morning Meeting: Threat Level Check-inTeam sync and updates 9:00 AM - 9:30 AMDaily 2 Identity Theft WatchReview suspicious 'Jim' behaviours 10:30 AM - 11:30 AMReview 3 Lunch Break: Pretzel Day PrepTeam lunch at downtown 12:30 PM - 1:30 PMBreak 4 Client Call with JanWeekly check-in with stakeholders 2:00 PM - 3:00 PMClient 5 Complaint Sorting: Product RecallPrioritize reported issues 3:30 PM - 4:30 PMComplaints 6 Bulletin Board Update: DundiesUpdate nominations and categories 4:30 PM - 5:30 PMDocs 7 End of Day Sync: Café DiscoReview progress and blockers 5:30 PM - 6:00 PMSync Tomorrow 1 Beach Games Roll-CallConfirm capacity without hot coals 10:00 AM - 12:00 PMPlanning 2 Stakeholder Presentation: Threat Level MidnightTasteful metrics, minimal fireworks 2:00 PM - 3:30 PMPresentation 3 Oscar’s Index Intervention (Of Spreadsheets)Deep dive into the budget tabs 9:00 AM - 11:00 AMNumbers 4 Parkour QA Gauntlet (Very Gentle)Functionality verified: walking 1:00 PM - 3:00 PMQA-ish 5 Campaign Analysis: WUPHF Without The WUPHFLess shouting, more smiling 4:00 PM - 5:30 PMMarketing This Week 1 Warehouse to Cloud (No Forklifts)Move boxes, label feelings WednesdayInfrastructure-ish 2 Customer Satisfaction Review: 'Did I Stutter?'Improve smiles per hour ThursdayCustomer Success 3 Benihana to Back Office CoordinationWe will know who is who FridayIntegration-ish 4 Data Deep Dive: Boom, Roasted (With Charts)Roasts limited to pie charts MondayAnalytics 5 Accessibility: Conference Room B UpgradesLess squinting, more seeing TuesdayAccessibility 6 Respect the Dashboard (Of Feelings)Set baselines for vibes WednesdayMonitoring 7 The Dundies of GrowthSkills, mentoring, zero karaoke tears FridayDevelopment Scale LevelExtra Extra Large (150%)
### How It Works The scale system works by modifying the `--ui-scale` CSS variable, which is used throughout the framework to calculate sizes. #### Affected Properties When you apply a scale modifier, it scales the following properties: - Font sizes (all text elements) - Line heights - Component dimensions (title bar height, progress bar sizes, etc.) - Spacing that uses the ui-scale multiplier - Any custom properties that reference `var(--ui-scale)` **Note:** The scale utility only affects elements that use the `--ui-scale` variable in their calculations. Fixed pixel values and screen dimensions remain unchanged. ### Combining with Device Configurations Scale modifiers can be combined with device-specific classes to override the default UI scale for particular devices. | Class Combination | Description | | --- | --- | | `screen screen--v2` | Uses device default scale | | `screen screen--v2 screen--scale-small` | Overrides to 87.5% scale | | `screen screen--amazon_kindle_2024 screen--scale-large` | Overrides to 112.5% scale |
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--gap-large` | 20px | — | — | — | | `--gap-medium` | 16px | — | — | — | | `--gap-small` | 7px | — | — | — | | `--gap-xlarge` | 30px | — | — | — | | `--gap-xsmall` | 5px | — | — | — | | `--gap-xxlarge` | 40px | — | — | — | | `--list-gap-large` | 16px | — | — | — | | `--list-gap-small` | 8px | — | — | — | | `--ui-scale` | 1 | — | — | — | Previous [Image Stroke Legible images when displayed on shaded backgrounds](/framework/docs/3.1/image_stroke) Next [Colors Complete palette definition: grayscale, chromatic hues, and blend pairs](/framework/docs/3.1/colors) --- # Colors The Colors system defines the complete palette for the framework: grayscale, chromatic hues, blend pairs, and semantic roles (primary, success, error, warning). Use these tokens with bg--, text--, and other utilities. See Background and Text Color for usage examples. ### Grayscale Palette The complete range of grayscale shades available in the framework, from pure black to pure white. These tokens use the same lightness scale as chromatic and blend palettes. black black 10 gray-10 15 gray-15 20 gray-20 25 gray-25 30 gray-30 35 gray-35 40 gray-40 45 gray-45 50 gray-50 55 gray-55 60 gray-60 65 gray-65 70 gray-70 75 gray-75 white white **Dark Mode Notice:** The color palette appears inverted because TRMNL's dark mode inverts the entire screen, except the images. ### Chromatic Palette The framework offers 10 hues × 14 lightness steps (red, orange, yellow, lime, green, cyan, blue, violet, purple, pink), using the same steps as grayscale (10, 15, 20, …, 75). Select a color device (e.g. Inky Impression 7.3, Tidbyt) from the Device Preview above to see these colors in action. Use the Raw Colors / Preview Colors toggle to compare full-bright tokens vs device-representative rendering. red red-10 red-15 red-20 red-25 red-30 red-35 red-40 red red-45 red-50 red-55 red-60 red-65 red-70 red-75 orange orange-10 orange-15 orange-20 orange-25 orange-30 orange-35 orange-40 orange orange-45 orange-50 orange-55 orange-60 orange-65 orange-70 orange-75 yellow yellow-10 yellow-15 yellow-20 yellow-25 yellow-30 yellow-35 yellow-40 yellow yellow-45 yellow-50 yellow-55 yellow-60 yellow-65 yellow-70 yellow-75 lime lime-10 lime-15 lime-20 lime-25 lime-30 lime-35 lime-40 lime lime-45 lime-50 lime-55 lime-60 lime-65 lime-70 lime-75 green green-10 green-15 green-20 green-25 green-30 green-35 green-40 green green-45 green-50 green-55 green-60 green-65 green-70 green-75 cyan cyan-10 cyan-15 cyan-20 cyan-25 cyan-30 cyan-35 cyan-40 cyan cyan-45 cyan-50 cyan-55 cyan-60 cyan-65 cyan-70 cyan-75 blue blue-10 blue-15 blue-20 blue-25 blue-30 blue-35 blue-40 blue blue-45 blue-50 blue-55 blue-60 blue-65 blue-70 blue-75 violet violet-10 violet-15 violet-20 violet-25 violet-30 violet-35 violet-40 violet violet-45 violet-50 violet-55 violet-60 violet-65 violet-70 violet-75 purple purple-10 purple-15 purple-20 purple-25 purple-30 purple-35 purple-40 purple purple-45 purple-50 purple-55 purple-60 purple-65 purple-70 purple-75 pink pink-10 pink-15 pink-20 pink-25 pink-30 pink-35 pink-40 pink pink-45 pink-50 pink-55 pink-60 pink-65 pink-70 pink-75 **Device Preview tip:** Use the Device Preview (top right) to switch between grayscale and color palettes, then toggle Raw Colors / Preview Colors to compare full-bright and device-accurate previews. Try Inky Impression 7.3 (color-7a) or Tidbyt (color-24bit) to see chromatic colors. ### Semantic Colors Semantic color roles map meaning to base hues. Use `bg--primary`, `text--success`, and similar utilities for intent-based styling. These alias underlying tokens (e.g. primary → blue) and inherit all device/bit-depth behavior. Themes can override via `--color-{role}` CSS variables. Primary bg--primary → blue Success bg--success → green Error bg--error → red Warning bg--warning → orange | Role | Underlying Token | Use | | --- | --- | --- | | primary | `blue` | Main actions, accents | | success | `green` | Confirmations, positive states | | error | `red` | Errors, destructive actions | | warning | `orange` | Cautions, alerts | ### Token Syntax Apply these color tokens with utility prefixes. The [Background](/framework/docs/3.1/background) page documents `bg--{token}`; the [Text Color](/framework/docs/3.1/text_color) page documents `text--{token}`. Other utilities (border, outline, etc.) may use the same tokens where applicable. | Utility | Example | Use | | --- | --- | --- | | bg-- | `bg--gray-50`, `bg--red-40` | Background colors | | text-- | `text--gray-50`, `text--blue-60`, `text--success` | Text colors | | bg-- / text-- (semantic) | `bg--primary`, `text--error`, `text--success` | Semantic roles (primary, success, error, warning) | **Grayscale:** black, gray-10 through gray-75, white. **Chromatic:** `{hue}` (pure color) or `{hue}-{step}` (e.g. red-50, blue-40). **Blend:** `{colorA}-{colorB}-{step}` (e.g. red-blue-50, green-yellow-25). **Semantic:** `primary`, `success`, `error`, `warning` (alias base hues). ### Backward Compatibility For backward compatibility, the original shade names (`gray-1` through `gray-7`) are still supported but deprecated. These map to equivalent extended shades: gray-1 (deprecated) gray-1 gray-2 (deprecated) gray-2 gray-3 (deprecated) gray-3 gray-4 (deprecated) gray-4 gray-5 (deprecated) gray-5 gray-6 (deprecated) gray-6 gray-7 (deprecated) gray-7
Gray 1
Gray 2
Gray 10
Gray 20
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Semantic | | | | | | `--black` | #000000 | — | — | — | | `--color-error` | var(--red) | — | — | — | | `--color-primary` | var(--blue) | — | — | — | | `--color-success` | var(--green) | — | — | — | | `--color-warning` | var(--orange) | — | — | — | | `--white` | #FFFFFF | — | — | — | | Grayscale | | | | | | `--gray-10` | #111111 | — | — | — | | `--gray-15` | #222222 | — | — | — | | `--gray-20` | #333333 | — | — | — | | `--gray-25` | #444444 | — | — | — | | `--gray-30` | #555555 | — | — | — | | `--gray-35` | #666666 | — | — | — | | `--gray-40` | #777777 | — | — | — | | `--gray-45` | #888888 | — | — | — | | `--gray-50` | #999999 | — | — | — | | `--gray-55` | #AAAAAA | — | — | — | | `--gray-60` | #BBBBBB | — | — | — | | `--gray-65` | #CCCCCC | — | — | — | | `--gray-70` | #DDDDDD | — | — | — | | `--gray-75` | #EEEEEE | — | — | — | | Legacy Grayscale | | | | | | `--gray-1` | #111111 | — | — | — | | `--gray-2` | #333333 | — | — | — | | `--gray-3` | #555555 | — | — | — | | `--gray-4` | #777777 | — | — | — | | `--gray-5` | #999999 | — | — | — | | `--gray-6` | #BBBBBB | — | — | — | | `--gray-7` | #DDDDDD | — | — | — | Previous [Scale Scale interface to affect content density and readability](/framework/docs/3.1/scale) Next [Tokens Complete CSS variable reference with root defaults, density, and bit-depth overrides](/framework/docs/3.1/tokens) --- # Tokens The Tokens reference lists every Framework CSS variable from `_variables_root.scss` and display overrides in `_variables_overrides.scss`. Use it to understand defaults, 2-bit visual/layout behavior, high-density typography, and 4/8/16-bit scaling. ### How To Read This Table Each row is a CSS custom property token. `Root` comes from `_variables_root.scss`. `2-bit`, `density 2x`, and `4/8/16-bit` come from mixins in `_variables_overrides.scss`. ### Palette | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Semantic | | | | | | `--black` | #000000 | — | — | — | | `--color-error` | var(--red) | — | — | — | | `--color-primary` | var(--blue) | — | — | — | | `--color-success` | var(--green) | — | — | — | | `--color-warning` | var(--orange) | — | — | — | | `--white` | #FFFFFF | — | — | — | | Grayscale | | | | | | `--gray-10` | #111111 | — | — | — | | `--gray-15` | #222222 | — | — | — | | `--gray-20` | #333333 | — | — | — | | `--gray-25` | #444444 | — | — | — | | `--gray-30` | #555555 | — | — | — | | `--gray-35` | #666666 | — | — | — | | `--gray-40` | #777777 | — | — | — | | `--gray-45` | #888888 | — | — | — | | `--gray-50` | #999999 | — | — | — | | `--gray-55` | #AAAAAA | — | — | — | | `--gray-60` | #BBBBBB | — | — | — | | `--gray-65` | #CCCCCC | — | — | — | | `--gray-70` | #DDDDDD | — | — | — | | `--gray-75` | #EEEEEE | — | — | — | | Legacy Grayscale | | | | | | `--gray-1` | #111111 | — | — | — | | `--gray-2` | #333333 | — | — | — | | `--gray-3` | #555555 | — | — | — | | `--gray-4` | #777777 | — | — | — | | `--gray-5` | #999999 | — | — | — | | `--gray-6` | #BBBBBB | — | — | — | | `--gray-7` | #DDDDDD | — | — | — | ### Description | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--description-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--description-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--description-font-smoothing` | none | none | auto | — | | `--description-font-weight` | 400 | 400 | 400 | — | | `--description-line-height` | 1 | 1 | 1.2 | — | | Large | | | | | | `--description-large-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--description-large-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--description-large-font-smoothing` | none | none | auto | — | | `--description-large-font-weight` | 400 | 400 | 700 | — | | `--description-large-line-height` | 1.25 | 1.25 | 1.2 | — | | Xlarge | | | | | | `--description-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--description-xlarge-font-size` | 21px | — | calc(21px * var(--ui-scale)) | — | | `--description-xlarge-font-smoothing` | auto | — | auto | — | | `--description-xlarge-font-weight` | 500 | — | 500 | — | | `--description-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--description-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--description-xxlarge-font-size` | 24px | — | calc(24px * var(--ui-scale)) | — | | `--description-xxlarge-font-smoothing` | auto | — | auto | — | | `--description-xxlarge-font-weight` | 475 | — | 475 | — | | `--description-xxlarge-line-height` | 1.2 | — | 1.2 | — | ### Other | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--font-base-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--font-base-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--font-base-font-smoothing` | none | none | auto | — | | `--font-base-line-height` | 1.25 | 1.25 | calc(22px * var(--ui-scale)) | — | | `--font-giga-font-family` | "Inter Variable", Inter | — | — | — | | `--font-giga-font-size` | 96px | — | — | — | | `--font-giga-font-smoothing` | auto | — | — | — | | `--font-giga-line-height` | 108px | — | — | — | | `--font-large-font-family` | "BlockKie" | "BlockKie" | "Inter Variable", Inter | — | | `--font-large-font-size` | 26px | 26px | calc(21px * var(--ui-scale)) | — | | `--font-large-font-smoothing` | none | none | auto | — | | `--font-large-line-height` | 1 | 1 | 1.2 | — | | `--font-mega-font-family` | "Inter Variable", Inter | — | — | — | | `--font-mega-font-size` | 74px | — | — | — | | `--font-mega-font-smoothing` | auto | — | — | — | | `--font-mega-line-height` | 86px | — | — | — | | `--font-peta-font-family` | "Inter Variable", Inter | — | — | — | | `--font-peta-font-size` | 170px | — | — | — | | `--font-peta-font-smoothing` | auto | — | — | — | | `--font-peta-line-height` | 180px | — | — | — | | `--font-small-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--font-small-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--font-small-font-smoothing` | none | none | auto | — | | `--font-small-line-height` | 1 | 1 | calc(18px * var(--ui-scale)) | — | | `--font-tera-font-family` | "Inter Variable", Inter | — | — | — | | `--font-tera-font-size` | 128px | — | — | — | | `--font-tera-font-smoothing` | auto | — | — | — | | `--font-tera-line-height` | 128px | — | — | — | | `--font-xlarge-font-family` | "Inter Variable", Inter | — | — | — | | `--font-xlarge-font-size` | 26px | — | — | — | | `--font-xlarge-font-smoothing` | auto | — | — | — | | `--font-xlarge-line-height` | 29px | — | — | — | | `--font-xxlarge-font-family` | "Inter Variable", Inter | — | — | — | | `--font-xxlarge-font-size` | 38px | — | — | — | | `--font-xxlarge-font-smoothing` | auto | — | — | — | | `--font-xxlarge-line-height` | 42px | — | — | — | | `--font-xxxlarge-font-family` | "Inter Variable", Inter | — | — | — | | `--font-xxxlarge-font-size` | 58px | — | — | — | | `--font-xxxlarge-font-smoothing` | auto | — | — | — | | `--font-xxxlarge-line-height` | 70px | — | — | — | ### Layout | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--full-h` | calc(var(--screen-h) - var(--gap) * 2) | — | — | — | | `--full-w` | calc(var(--screen-w) - var(--gap) * 2) | — | — | — | | `--half_horizontal-h` | calc((var(--screen-h) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--half_horizontal-w` | calc((var(--screen-w) - var(--gap) * 2)) | — | — | — | | `--half_vertical-h` | calc((var(--screen-h) - var(--gap) * 2)) | — | — | — | | `--half_vertical-w` | calc((var(--screen-w) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--quadrant-h` | calc((var(--screen-h) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--quadrant-w` | calc((var(--screen-w) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--screen-h` | 480px | — | — | — | | `--screen-h-original` | 480px | — | — | — | | `--screen-w` | 800px | — | — | — | | `--screen-w-original` | 800px | — | — | — | ### Spacing | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--gap` | 10px | — | — | — | | `--gap-large` | 20px | — | — | — | | `--gap-medium` | 16px | — | — | — | | `--gap-small` | 7px | — | — | — | | `--gap-xlarge` | 30px | — | — | — | | `--gap-xsmall` | 5px | — | — | — | | `--gap-xxlarge` | 40px | — | — | — | | `--list-gap-large` | 16px | — | — | — | | `--list-gap-small` | 8px | — | — | — | ### Item | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--item-index-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--item-index-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--item-index-font-smoothing` | none | none | auto | — | | `--item-index-font-weight` | 400 | 400 | 600 | — | | `--item-index-line-height` | 1 | 1 | 1 | — | | `--item-meta-width` | 10px | 10px | — | calc(10px * var(--ui-scale)) | ### Label | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--label-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--label-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--label-font-smoothing` | none | none | auto | — | | `--label-font-weight` | 400 | 400 | 500 | — | | `--label-line-height` | 1.25 | 1.25 | 1.25 | — | | Small | | | | | | `--label-small-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--label-small-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--label-small-font-smoothing` | none | none | auto | — | | `--label-small-font-weight` | 400 | 400 | 500 | — | | `--label-small-line-height` | 1 | 1 | 1 | — | | Large | | | | | | `--label-large-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--label-large-font-size` | 21px | — | calc(21px * var(--ui-scale)) | — | | `--label-large-font-smoothing` | auto | — | auto | — | | `--label-large-font-weight` | 500 | — | 500 | — | | `--label-large-line-height` | 1.2 | — | 1.2 | — | | Xlarge | | | | | | `--label-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--label-xlarge-font-size` | 26px | — | calc(26px * var(--ui-scale)) | — | | `--label-xlarge-font-smoothing` | auto | — | auto | — | | `--label-xlarge-font-weight` | 475 | — | 475 | — | | `--label-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--label-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--label-xxlarge-font-size` | 30px | — | calc(30px * var(--ui-scale)) | — | | `--label-xxlarge-font-smoothing` | auto | — | auto | — | | `--label-xxlarge-font-weight` | 450 | — | 450 | — | | `--label-xxlarge-line-height` | 1.2 | — | 1.2 | — | ### Progress | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--progress-bar-height` | 24px | — | — | — | | `--progress-bar-height-large` | 32px | — | — | — | | `--progress-bar-height-small` | 12px | — | — | — | | `--progress-bar-height-xsmall` | 6px | — | — | — | | `--progress-bar-radius` | 10px | — | — | — | | `--progress-dot-size` | 16px | — | — | — | | `--progress-dot-size-large` | 20px | — | — | — | | `--progress-dot-size-small` | 12px | — | — | — | | `--progress-dot-size-xsmall` | 8px | — | — | — | ### Rich Text | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--richtext-content-max-width` | 640px | — | — | — | | `--richtext-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--richtext-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--richtext-font-smoothing` | none | none | auto | — | | `--richtext-font-weight` | 400 | 400 | 500 | — | | `--richtext-line-height` | 22px | 22px | calc(22px * var(--ui-scale)) | — | | Small | | | | | | `--richtext-small-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--richtext-small-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--richtext-small-font-smoothing` | none | none | auto | — | | `--richtext-small-font-weight` | 400 | 400 | 500 | — | | `--richtext-small-line-height` | 16px | 16px | calc(18px * var(--ui-scale)) | — | | Large | | | | | | `--richtext-large-font-family` | "BlockKie" | "BlockKie" | "Inter Variable", Inter | — | | `--richtext-large-font-size` | 26px | 26px | calc(21px * var(--ui-scale)) | — | | `--richtext-large-font-smoothing` | none | none | auto | — | | `--richtext-large-font-weight` | 400 | 400 | 500 | — | | `--richtext-large-line-height` | 1 | 1 | 1.2 | — | | Xlarge | | | | | | `--richtext-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--richtext-xlarge-font-size` | 30px | — | calc(30px * var(--ui-scale)) | — | | `--richtext-xlarge-font-smoothing` | auto | — | auto | — | | `--richtext-xlarge-font-weight` | 425 | — | 425 | — | | `--richtext-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--richtext-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--richtext-xxlarge-font-size` | 35px | — | calc(35px * var(--ui-scale)) | — | | `--richtext-xxlarge-font-smoothing` | auto | — | auto | — | | `--richtext-xxlarge-font-weight` | 400 | — | 400 | — | | `--richtext-xxlarge-line-height` | 1.2 | — | 1.2 | — | | Xxxlarge | | | | | | `--richtext-xxxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--richtext-xxxlarge-font-size` | 40px | — | calc(40px * var(--ui-scale)) | — | | `--richtext-xxxlarge-font-smoothing` | auto | — | auto | — | | `--richtext-xxxlarge-font-weight` | 375 | — | 375 | — | | `--richtext-xxxlarge-line-height` | 1.2 | — | 1.2 | — | ### Rounded | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--rounded` | 10px | — | — | — | | `--rounded-full` | 9999px | — | — | — | | `--rounded-large` | 20px | — | — | — | | `--rounded-medium` | 15px | — | — | — | | `--rounded-none` | 0px | — | — | — | | `--rounded-small` | 7px | — | — | — | | `--rounded-xlarge` | 25px | — | — | — | | `--rounded-xsmall` | 5px | — | — | — | | `--rounded-xxlarge` | 30px | — | — | — | ### Table | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--table-tbody-height` | 46px | — | — | — | | `--table-thead-height` | 36px | — | — | — | | Xsmall | | | | | | `--table-xsmall-tbody-height` | 22px | — | — | — | | `--table-xsmall-thead-height` | 18px | — | — | — | | Small | | | | | | `--table-small-tbody-height` | 31px | — | — | — | | `--table-small-thead-height` | 24px | — | — | — | | Large | | | | | | `--table-large-tbody-height` | 56px | — | — | — | | `--table-large-thead-height` | 44px | — | — | — | | Xlarge | | | | | | `--table-xlarge-tbody-height` | 72px | — | — | — | | `--table-xlarge-thead-height` | 56px | — | — | — | ### Title Bar | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--title-bar-border-radius` | 10px | 10px | — | 10px | | `--title-bar-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--title-bar-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--title-bar-font-smoothing` | none | none | auto | — | | `--title-bar-font-weight` | 400 | 400 | 700 | — | | `--title-bar-height` | 40px | 40px | — | calc(40px * var(--ui-scale)) | | `--title-bar-image-height` | 28px | 28px | — | calc(28px * var(--ui-scale)) | | `--title-bar-line-height` | 1 | 1 | calc(22px * var(--ui-scale)) | — | | `--title-bar-padding-top` | 5px | 5px | 0px | 0px | | `--title-bar-text-stroke-width` | 3.5px | 3.5px | 2px | 2px | | Small | | | | | | `--title-bar-small-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--title-bar-small-height` | 32px | 32px | — | calc(32px * var(--ui-scale)) | | `--title-bar-small-image-height` | 24px | 24px | — | calc(24px * var(--ui-scale)) | ### Title | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--title-font-family` | "BlockKie" | "BlockKie" | "Inter Variable", Inter | — | | `--title-font-size` | 26px | 26px | calc(21px * var(--ui-scale)) | — | | `--title-font-smoothing` | none | none | auto | — | | `--title-font-weight` | 400 | 400 | 400 | — | | `--title-line-height` | 1 | 1 | 1.2 | — | | Small | | | | | | `--title-small-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--title-small-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--title-small-font-smoothing` | none | none | auto | — | | `--title-small-font-weight` | 400 | 400 | 700 | — | | `--title-small-line-height` | 1 | 1 | 1.2 | — | | Large | | | | | | `--title-large-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--title-large-font-size` | 30px | — | calc(30px * var(--ui-scale)) | — | | `--title-large-font-smoothing` | auto | — | auto | — | | `--title-large-font-weight` | 425 | — | 425 | — | | `--title-large-line-height` | 1.2 | — | 1.2 | — | | Xlarge | | | | | | `--title-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--title-xlarge-font-size` | 35px | — | calc(35px * var(--ui-scale)) | — | | `--title-xlarge-font-smoothing` | auto | — | auto | — | | `--title-xlarge-font-weight` | 400 | — | 400 | — | | `--title-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--title-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--title-xxlarge-font-size` | 40px | — | calc(40px * var(--ui-scale)) | — | | `--title-xxlarge-font-smoothing` | auto | — | auto | — | | `--title-xxlarge-font-weight` | 375 | — | 375 | — | | `--title-xxlarge-line-height` | 1.2 | — | 1.2 | — | ### Scaling | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--ui-scale` | 1 | — | — | — | ### Value | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--value-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--value-font-size` | 38px | — | calc(38px * var(--ui-scale)) | — | | `--value-font-smoothing` | auto | — | auto | — | | `--value-font-weight` | 450 | — | 450 | — | | `--value-line-height` | 42px | — | calc(42px * var(--ui-scale)) | — | | Xxsmall | | | | | | `--value-xxsmall-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--value-xxsmall-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--value-xxsmall-font-smoothing` | none | none | auto | — | | `--value-xxsmall-font-weight` | 400 | 400 | 700 | — | | `--value-xxsmall-line-height` | 16px | 16px | calc(14px * var(--ui-scale)) | — | | Xsmall | | | | | | `--value-xsmall-font-size` | 20px | — | calc(20px * var(--ui-scale)) | — | | `--value-xsmall-font-weight` | 600 | — | 600 | — | | `--value-xsmall-line-height` | 24px | — | calc(24px * var(--ui-scale)) | — | | Small | | | | | | `--value-small-font-size` | 26px | — | calc(26px * var(--ui-scale)) | — | | `--value-small-font-weight` | 500 | — | 475 | — | | `--value-small-line-height` | 29px | — | calc(29px * var(--ui-scale)) | — | | Large | | | | | | `--value-large-font-size` | 58px | — | calc(58px * var(--ui-scale)) | — | | `--value-large-font-weight` | 400 | — | 400 | — | | `--value-large-line-height` | 70px | — | calc(70px * var(--ui-scale)) | — | | Xlarge | | | | | | `--value-xlarge-font-size` | 74px | — | calc(74px * var(--ui-scale)) | — | | `--value-xlarge-font-weight` | 375 | — | 375 | — | | `--value-xlarge-line-height` | 86px | — | calc(86px * var(--ui-scale)) | — | | Xxlarge | | | | | | `--value-xxlarge-font-size` | 96px | — | calc(96px * var(--ui-scale)) | — | | `--value-xxlarge-font-weight` | 350 | — | 350 | — | | `--value-xxlarge-line-height` | 108px | — | calc(108px * var(--ui-scale)) | — | | Xxxlarge | | | | | | `--value-xxxlarge-font-size` | 128px | — | calc(128px * var(--ui-scale)) | — | | `--value-xxxlarge-font-weight` | 300 | — | 300 | — | | `--value-xxxlarge-line-height` | 128px | — | calc(128px * var(--ui-scale)) | — | | Mega | | | | | | `--value-mega-font-size` | 170px | — | calc(170px * var(--ui-scale)) | — | | `--value-mega-font-weight` | 275 | — | 275 | — | | `--value-mega-line-height` | 180px | — | calc(180px * var(--ui-scale)) | — | | Giga | | | | | | `--value-giga-font-size` | 220px | — | calc(220px * var(--ui-scale)) | — | | `--value-giga-font-weight` | 250 | — | 250 | — | | `--value-giga-line-height` | 230px | — | calc(230px * var(--ui-scale)) | — | | Tera | | | | | | `--value-tera-font-size` | 290px | — | calc(290px * var(--ui-scale)) | — | | `--value-tera-font-weight` | 225 | — | 225 | — | | `--value-tera-line-height` | 300px | — | calc(300px * var(--ui-scale)) | — | | Peta | | | | | | `--value-peta-font-size` | 380px | — | calc(380px * var(--ui-scale)) | — | | `--value-peta-font-weight` | 200 | — | 200 | — | | `--value-peta-line-height` | 390px | — | calc(390px * var(--ui-scale)) | — | Previous [Colors Complete palette definition: grayscale, chromatic hues, and blend pairs](/framework/docs/3.1/colors) Next [Font Family Switch between Classic and TRMNL font bundles per device](/framework/docs/3.1/font_family) --- # Font Family The Framework ships two pixel font bundles: Classic (NicoPups, NicoClean, BlockKie) and TRMNL (TRMNL12, TRMNL16, TRMNL21). Low-density displays use the selected bundle; high-density displays use Inter Variable for legibility. The original pixel set. Three single-weight fonts: NicoPups, NicoClean, BlockKie. Default in Framework 3.0. The new pixel set. Three font families with Regular and Bold weights: TRMNL12, TRMNL16, TRMNL21. Default in Framework 3.1.
...
...
Both bundles are available in Framework 3.x. Low-density displays use the selected pixel-font bundle. High-density displays use **Inter Variable** regardless of bundle or bit depth. In Framework 3.1, screens without a font-bundle class use **TRMNL** by default; add `screen--fonts-classic` to opt into Classic. ### Classic bundle Three single-weight pixel fonts. Activate by adding `screen--fonts-classic` to the screen root. This controls pixel-font output on low-density displays; high-density displays still resolve to Inter. #### NicoPups Designed at **16px** pixel height. Used for descriptions, small labels, and metadata. Regular 400 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? font-family: "NicoPups" · font-size: 16px #### NicoClean Designed at **16px** pixel height. The workhorse font, used for labels, rich text body copy, and title-bar text. Regular 400 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? font-family: "NicoClean" · font-size: 16px #### BlockKie Designed at **26px** pixel height. Used for titles and large rich-text. The largest pixel font in the Classic bundle. Regular 400 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? font-family: "BlockKie" · font-size: 26px #### On-device preview text--small · Classictext--base · Classictext--large · Classictext--base font--bold · Classic Classic bundle **High-density font notice:** This preview is using Inter because the selected device is high-density. Classic and TRMNL pixel bundles still apply on low-density displays; choose a 1x-density model in Device Preview to compare those bundles. ### TRMNL bundle Three font families, each with Regular and Bold weights. This is the implicit default for Framework 3.1 when no font-bundle class is present. Add `screen--fonts-trmnl` when you want to pin the bundle explicitly. This controls pixel-font output on low-density displays; high-density displays still resolve to Inter. #### TRMNL12 Designed at **12px** pixel height. The smallest pixel font, used for descriptions, small labels, and metadata. Regular 400 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? Bold 700 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? font-family: "TRMNL12" · font-size: 12px #### TRMNL16 Designed at **16px** pixel height. The workhorse font, used for labels, rich text body copy, and title-bar text. Regular 400 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? Bold 700 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? font-family: "TRMNL16" · font-size: 16px #### TRMNL21 Designed at **21px** pixel height. The largest pixel font, used for titles, headings, and large rich-text. Regular 400 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? Bold 700 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$%^&*()-=+[]{}|;:',./<>? font-family: "TRMNL21" · font-size: 21px #### On-device preview text--small · TRMNLtext--base · TRMNLtext--large · TRMNLtext--base font--bold · TRMNL TRMNL bundle **High-density font notice:** This preview is using Inter because the selected device is high-density. Classic and TRMNL pixel bundles still apply on low-density displays; choose a 1x-density model in Device Preview to compare those bundles. ### Component-by-component bundle map Each component picks the appropriate font based on the active bundle. On high-density displays Inter Variable is used for every component regardless of bundle. | Component | Classic (low-density) | TRMNL (low-density) | High-density | | --- | --- | --- | --- | | Title Bar | NicoClean | TRMNL16 | Inter Variable | | Title | BlockKie | TRMNL21 | Inter Variable | | Title (small) | NicoClean | TRMNL16 | Inter Variable | | Label | NicoClean | TRMNL16 | Inter Variable | | Label (small) | NicoPups | TRMNL12 | Inter Variable | | Description | NicoPups | TRMNL12 | Inter Variable | | Description (large) | NicoClean | TRMNL16 | Inter Variable | | Value (xxsmall) | NicoClean | TRMNL16 | Inter Variable | | Value (other sizes) | Inter Variable | Inter Variable | Inter Variable | | Rich Text | NicoClean | TRMNL16 | Inter Variable | | Rich Text (small) | NicoPups | TRMNL12 | Inter Variable | | Rich Text (large) | BlockKie | TRMNL21 | Inter Variable | | Item Index | NicoPups | TRMNL12 | Inter Variable | Previous [Tokens Complete CSS variable reference with root defaults, density, and bit-depth overrides](/framework/docs/3.1/tokens) Next [Font Weight Toggle between regular and bold font weight independently of size](/framework/docs/3.1/font_weight) --- # Font Weight Utility classes for controlling font weight independently of size. Classic ships in a single weight, so font--bold is a no-op on low-density Classic; on low-density TRMNL it picks the bundled bold variant; on high-density displays it sets the Inter Variable weight. ### Usage Use `font--regular` and `font--bold` to control font weight independently of size. Density decides whether the active pixel-font bundle or Inter receives the weight. The bold variant is resolved as follows: - **Classic** bundle: every font ships in a single weight, so `font--bold` has no visual effect on low-density Classic. - **TRMNL** bundle: `font--bold` selects the matching **TRMNL12/16/21 Bold** font file at the active size. - **High-density** displays: both classes simply set the Inter Variable weight to 400 or 700. **High-density font notice:** This preview is using Inter because the selected device is high-density. Classic and TRMNL pixel bundles still apply on low-density displays; choose a 1x-density model in Device Preview to compare those bundles. | Class | Weight | Classic (low-density) | TRMNL (low-density) | High-density | | --- | --- | --- | --- | --- | | `font--regular` | 400 | NicoPups / NicoClean / BlockKie | TRMNL12/16/21 Regular | Inter Variable @ 400 | | `font--bold` | 700 | — (no bold variant) | TRMNL12/16/21 Bold | Inter Variable @ 700 | #### Weight comparison · Classic bundle Each weight shown at every pixel-font size with `screen--fonts-classic` on the screen root. Low-density displays use that bundle; high-density displays use Inter weights instead. text--small font--regulartext--small font--bold text--base font--regulartext--base font--bold text--large font--regulartext--large font--bold Font WeightClassic #### Weight comparison · TRMNL bundle Each weight shown at every pixel-font size with `screen--fonts-trmnl` on the screen root. Low-density displays use that bundle; high-density displays use Inter weights instead. text--small font--regulartext--small font--bold text--base font--regulartext--base font--bold text--large font--regulartext--large font--bold Font WeightTRMNL Small regular Small bold Base regular Base bold Large regular Large bold ### Responsive & bit-depth variants Font weight utilities support responsive, orientation, and bit-depth prefixes. Combine them to fine-tune weight across screen sizes and display types. | Variant | Example | Description | | --- | --- | --- | | Responsive | `md:font--bold` | Bold at medium breakpoint and up | | Orientation | `portrait:font--regular` | Regular weight in portrait orientation | | Bit-depth | `4bit:font--bold` | Bold on 4-bit displays only | | Combined | `md:4bit:font--bold` | Bold at medium breakpoint on 4-bit displays | Bold only on 4-bit displays Bold at medium breakpoint and up Previous [Font Family Switch between Classic and TRMNL font bundles per device](/framework/docs/3.1/font_family) Next [Font Glyphs Browse every glyph available in each Framework font bundle](/framework/docs/3.1/font_glyphs) --- # Font Glyphs Browse every glyph available in each Framework font. Switch between the Classic and TRMNL bundles to view their full character inventory. ### Font selector Classic bundle Original TRMNL pixel fonts. Each family ships in a single weight - there is no bold variant. TRMNL bundle New TRMNL family. Each family ships with both Regular and Bold weight variants. Loading font… Previous [Font Weight Toggle between regular and bold font weight independently of size](/framework/docs/3.1/font_weight) Next [Text Size Control text size with utility classes across all display types](/framework/docs/3.1/text_size) --- # Text Size Utility classes for controlling text size. Each class sets the correct font family, size, line-height, and smoothing for the active density tier: pixel bundle on low-density displays, Inter Variable on high-density displays. ### Text Size Utilities Use `text--{size}` utility classes to set font family, size, line-height, and smoothing in one declaration. Density decides which font family the utility resolves to. On low-density displays, the three smallest sizes use the active pixel-font bundle (Classic NicoPups/NicoClean/BlockKie or TRMNL TRMNL12/16/21). On high-density displays, every text size uses Inter Variable regardless of bundle. Sizes from xlarge onward use Inter Variable on every display. **High-density font notice:** This preview is using Inter because the selected device is high-density. Classic and TRMNL pixel bundles still apply on low-density displays; choose a 1x-density model in Device Preview to compare those bundles. | Class | Size | Line-height | Classic (low-density) | TRMNL (low-density) | High-density | | --- | --- | --- | --- | --- | --- | | `text--small` | 12px | 1 | NicoPups @ 16px | TRMNL12 | Inter Variable | | `text--base` | 16px | 1.25 | NicoClean | TRMNL16 | Inter Variable | | `text--large` | 21px | 1 | BlockKie @ 26px | TRMNL21 | Inter Variable | | `text--xlarge` | 26px | 29px | Inter Variable | Inter Variable | Inter Variable | | `text--xxlarge` | 38px | 42px | Inter Variable | Inter Variable | Inter Variable | | `text--xxxlarge` | 58px | 70px | Inter Variable | Inter Variable | Inter Variable | | `text--mega` | 74px | 86px | Inter Variable | Inter Variable | Inter Variable | | `text--giga` | 96px | 108px | Inter Variable | Inter Variable | Inter Variable | | `text--tera` | 128px | 128px | Inter Variable | Inter Variable | Inter Variable | | `text--peta` | 170px | 180px | Inter Variable | Inter Variable | Inter Variable | #### Small The `text--small` class. Low-density previews show the active pixel-font bundle where that size supports it; high-density previews show Inter. The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog Text SizeSmall Regular text Bold text #### Base The `text--base` class. Low-density previews show the active pixel-font bundle where that size supports it; high-density previews show Inter. The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog Text SizeBase Regular text Bold text #### Large The `text--large` class. Low-density previews show the active pixel-font bundle where that size supports it; high-density previews show Inter. The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog Text SizeLarge Regular text Bold text #### XLarge The `text--xlarge` class. Low-density previews show the active pixel-font bundle where that size supports it; high-density previews show Inter. The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog Text SizeXLarge Regular text Bold text #### XXLarge The `text--xxlarge` class. Low-density previews show the active pixel-font bundle where that size supports it; high-density previews show Inter. The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog Text SizeXXLarge Regular text Bold text #### XXXLarge The `text--xxxlarge` class. Low-density previews show the active pixel-font bundle where that size supports it; high-density previews show Inter. The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog Text SizeXXXLarge Regular text Bold text ### Responsive & bit-depth variants All text size utilities support responsive, orientation, and bit-depth prefixes. Combine them to fine-tune typography across screen sizes and display types. | Variant | Example | Description | | --- | --- | --- | | Responsive | `md:text--large` | Apply at medium breakpoint and up | | Orientation | `portrait:text--small` | Apply in portrait orientation | | Bit-depth | `4bit:text--xlarge` | Apply on 4-bit displays only | | Combined | `md:4bit:text--xxlarge` | Apply at medium breakpoint on 4-bit displays | Responsive text sizing Larger on 4-bit displays Previous [Font Glyphs Browse every glyph available in each Framework font bundle](/framework/docs/3.1/font_glyphs) Next [Text Alignment Control text alignment with responsive breakpoint, orientation, and bit-depth variants](/framework/docs/3.1/text_alignment) --- # Text Alignment The Text Alignment system provides utility classes for controlling horizontal text alignment. Use left, center, right, and justify alignment with responsive variants for breakpoints, orientation, and bit-depth. ### Usage Control text alignment using the alignment utility classes. Options include left, center, right, and justify alignment. See the [Responsive Features](#responsive-text-alignment) section for breakpoint, orientation, and bit-depth variants. | Class | Description | | --- | --- | | `text--left` | Aligns text to the left (default for most elements) | | `text--center` | Centers text horizontally | | `text--right` | Aligns text to the right | | `text--justify` | Justifies text, creating even edges on both sides | This text is left-aligned. This is the default alignment for most text content. This text is center-aligned. Useful for headings and important content. This text is right-aligned. Often used for numerical data or RTL languages. This text is justified. Creates even text edges on both sides but affects readability. Useful for multi-column text layouts. TextAlignment

Left-aligned text

Center-aligned text

Right-aligned text

Justified text

### Responsive Features Alignment classes support all three responsive systems: size-based breakpoints, orientation-based, and bit-depth variants. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different alignment at different screen widths. Responsive alignment Left by default, center on md+, right on lg+ Text AlignmentResponsive

Responsive alignment

Progressive alignment

#### Orientation and Size+Orientation Text alignment can adapt to orientation with `portrait:` and `landscape:`, and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation variant Left by default, center in portrait Text AlignmentOrientation

Orientation variant

Left by default, right on md+ portrait

#### Bit-Depth Responsive Alignment classes support bit-depth prefixes like `1bit:`, `2bit:`, and `4bit:` to apply different alignment on different display color capabilities. Bit-depth alignment Center by default, right on 2-bit screens Text AlignmentBit-Depth Responsive

Bit-depth alignment

Left by default, center on lg+ 4-bit screens

#### Combined Responsive Features Combine size, orientation, and bit-depth modifiers for alignment. Use the pattern `size:orientation:bit-depth:utility` for highly targeted styling.

Right on md+ portrait 2-bit screens

Progressive with bit-depth override

Previous [Text Size Control text size with utility classes across all display types](/framework/docs/3.1/text_size) Next [Text Color Apply grayscale and chromatic color shades to text elements](/framework/docs/3.1/text_color) --- # Text Color The Text Color system creates the illusion of grayscale text through carefully designed dither patterns. When rendered on 1-bit (black and white only) displays, these patterns create an illusion of different shades of gray by using specific arrangements of black and white pixels. The shade scale matches the [Colors](/framework/docs/3.1/colors) palette. ### Usage Use the `text--{shade}` utility classes to apply these text color patterns to any element. Choose from sixteen values: black, gray-10 through gray-75, and white. See the [Responsive Features](#responsive-text-color) section for responsive variants. Aa black Aa gray-10 Aa gray-15 Aa gray-20 Aa gray-25 Aa gray-30 Aa gray-35 Aa gray-40 Aa gray-45 Aa gray-50 Aa gray-55 Aa gray-60 Aa gray-65 Aa gray-70 Aa gray-75 Aa white Aa black Aa gray-10 Aa gray-15 Aa gray-20 Aa gray-25 Aa gray-30 Aa gray-35 Aa gray-40 Aa gray-45 Aa gray-50 Aa gray-55 Aa gray-60 Aa gray-65 Aa gray-70 Aa gray-75 Aa white Text color shades **Dark Mode Notice:** The color palette appears inverted because TRMNL's dark mode inverts the entire screen, except the images.
Black text
Gray 10 text
Gray 15 text
Gray 20 text
Gray 25 text
Gray 30 text
Gray 35 text
Gray 40 text
Gray 45 text
Gray 50 text
Gray 55 text
Gray 60 text
Gray 65 text
Gray 70 text
Gray 75 text
White text
### Backward Compatibility For backward compatibility, the original shade names (`gray-1` through `gray-7`) are still supported but deprecated. These map to equivalent extended shades:
Gray 1 text (deprecated)
Gray 2 text (deprecated)
Gray 10 text (preferred)
Gray 20 text (preferred)
### Responsive Features Text color classes support size-based and orientation-based responsive variants. Bit-depth affects color rendering automatically based on the device (1-bit patterns, 2-bit patterns, 4-bit solid colors)—no bit-depth class prefixes are needed for colors. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different colors at different screen widths. Responsive color Gray 50 by default, gray 30 on md+ Text ColorResponsive Responsive color Progressive color #### Orientation and Size+Orientation Text colors can adapt to orientation with `portrait:` and `landscape:`, and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation color variant Color on md+ portrait Text color classes support size-based and orientation-based variants only. Bit-depth affects color rendering automatically based on the device (1-bit patterns, 2-bit patterns, 4-bit solid colors)—no bit-depth class prefixes are needed for colors. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Semantic | | | | | | `--black` | #000000 | — | — | — | | `--color-error` | var(--red) | — | — | — | | `--color-primary` | var(--blue) | — | — | — | | `--color-success` | var(--green) | — | — | — | | `--color-warning` | var(--orange) | — | — | — | | `--white` | #FFFFFF | — | — | — | | Grayscale | | | | | | `--gray-10` | #111111 | — | — | — | | `--gray-15` | #222222 | — | — | — | | `--gray-20` | #333333 | — | — | — | | `--gray-25` | #444444 | — | — | — | | `--gray-30` | #555555 | — | — | — | | `--gray-35` | #666666 | — | — | — | | `--gray-40` | #777777 | — | — | — | | `--gray-45` | #888888 | — | — | — | | `--gray-50` | #999999 | — | — | — | | `--gray-55` | #AAAAAA | — | — | — | | `--gray-60` | #BBBBBB | — | — | — | | `--gray-65` | #CCCCCC | — | — | — | | `--gray-70` | #DDDDDD | — | — | — | | `--gray-75` | #EEEEEE | — | — | — | | Legacy Grayscale | | | | | | `--gray-1` | #111111 | — | — | — | | `--gray-2` | #333333 | — | — | — | | `--gray-3` | #555555 | — | — | — | | `--gray-4` | #777777 | — | — | — | | `--gray-5` | #999999 | — | — | — | | `--gray-6` | #BBBBBB | — | — | — | | `--gray-7` | #DDDDDD | — | — | — | Previous [Text Alignment Control text alignment with responsive breakpoint, orientation, and bit-depth variants](/framework/docs/3.1/text_alignment) Next [Text Stroke Legible text when displayed on shaded backgrounds](/framework/docs/3.1/text_stroke) --- # Text Stroke The Text Stroke system allows you to add outlined text with customizable stroke width and color. This is useful for creating text that stands out against shaded backgrounds. **Note:** Text Stroke works only on pure black or white text. [Learn More](#browser-limitations) ### Basic Usage Apply `text-stroke` to outline text. Combine with width and shade modifiers as needed. | Class | Description | | --- | --- | | `text-stroke` | Stroke: outline (default 3.5px white) | | `text-stroke--{size}` | Stroke width: `small`, `medium`, `large`, `xlarge` | | `text-stroke--{shade}` | Stroke color: `black`, `gray-10` … `gray-75`, `white`. See [Background](/framework/docs/3.1/background) for the shade scale. | Outlined text ### Widths The Text Stroke system includes preset size modifiers that allow you to quickly apply different stroke widths to your text. The default stroke is 3.5px white, with additional options for base (3.5px, equivalent to default), small (2px), medium (4.5px), large (6px), and extra large (7.5px). The `text-stroke--base` modifier explicitly sets the default stroke width and is useful for responsive layouts. AaNo Stroke AaSmall AaBase AaDefault AaMedium AaLarge AaExtra Large Text StrokePreset Sizes Aa Aa Aa Aa Aa Aa Aa ### Shades Use the `text-stroke--{shade}` modifier to change the stroke color. Choose from sixteen values: black, gray-10 through gray-75, and white. For an overview of the shade scale and how it adapts across bit‑depths, see [Background](/framework/docs/3.1/background) . AaNo Stroke AaSmall AaBase AaDefault AaMedium AaLarge AaExtra Large Text StrokeShades Aa Aa Aa Aa Aa Aa ### Browser Limitations **Text Stroke works only when the text itself is pure black or pure white.** This is due to how browsers render strokes relative to text fills. We simulate grayscale text by applying hand-crafted bitmap patterns as a background and revealing them with `background-clip: text` (with transparent text color). This makes text appear gray, but under the hood the fill is not a solid color - it's a background image clipped to the text. The CSS `paint-order` property cannot treat a background as a pass-through fill layer, so only `paint-order: stroke fill;` is effective when the fill is a solid color. Because clipped backgrounds are not considered a fill for paint-order, we cannot stroke around grayscale (background-clipped) text. Use black or white text when you need a stroke. Previous [Text Color Apply grayscale and chromatic color shades to text elements](/framework/docs/3.1/text_color) Next [Overflow Handle column items overflow](/framework/docs/3.1/overflow) --- # Overflow The Overflow engine automatically lays out items into up to N columns and adds an “and X more” label when content exceeds the available height. It also applies text clamping per-column width and handles grouped headers without leaving orphaned headings. ### Basic Usage You can enable Overflow on any container (e.g., a flex or grid column) by adding `data-overflow="true"` to the container. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowGeneric Container
Title goes here Description goes here
| Attribute | Default | Description | | --- | --- | --- | | `data-overflow` | `false` | Enable Overflow on any container (non-`.columns`). Hides trailing items to fit the height budget. | | `data-overflow-max-height` | `auto` | Override height budget. Pixel value (e.g., `320`) or `auto` to inherit the parent’s content height. | | `data-overflow-counter` | `false` | Show a trailing “and N more” label when items are hidden. | | `data-overflow-max-cols` | `unset` | Best‑fit columns up to N on a `.columns` container. Optimizes for the most visible items. Responsive: `-sm`, `-md`, `-lg`, `-portrait`, `-md-portrait`, etc. | | `data-overflow-cols` | `unset` | Force exactly N columns on a `.columns` container. Same responsive variants as `data-overflow-max-cols`. | ### Smart Columns Place `.item`[Item](/framework/docs/3.1/item) elements inside a single `.column` element, that's nested inside a `.columns`[Columns](/framework/docs/3.1/columns) container. Set the exact or maximum number of columns with `data-overflow-cols` or `data-overflow-max-cols`. #### Number of columns Set `data-overflow-cols` to force an exact column count. Use this when you want a consistent column count regardless of how many items fit. The engine will still respect height constraints, but it will keep the requested number of columns even if that shows fewer items than a best‑fit layout. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowExactly 3 Columns
#### Maximum number of columns Set `data-overflow-max-cols` to allow the engine to choose the best number of columns up to the maximum you specify. This best‑fit mode prioritizes the most visible items. For example, allowing 3 columns may still render 2 columns if that shows more items due to less wrapping. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowMax 3 Columns
#### Responsive `data-overflow-max-cols` and `data-overflow-cols` support the same size and orientation modifiers as other framework components. Specificity (most specific first): size + orientation (e.g. `data-overflow-max-cols-md-portrait`), size (`-sm`, `-md`, `-lg`), orientation (`-portrait`), then base.
### Height Budget Control how much vertical space Overflow can use by specifying `data-overflow-max-height`. When not specified, the height budget defaults to the parent’s content height. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowHeight Budget (220px)
### Overflow Counter Opt‑in to display a trailing “and N more” label by adding `data-overflow-counter="true"` The engine will reserve space for the label during planning so it fits without pushing items out of bounds. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowCounter Enabled
### Clamp-aware Overflow The Clamp engine[Clamp](/framework/docs/3.1/clamp) re-clamps text for the actual column width. Apply `data-clamp` on titles or descriptions as needed. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance Overflow + ClampTitle 1 • Description 3 • Max 2 Columns
### Group Headers Mark headers with `group-header`. The engine avoids orphaned headers and duplicates a header at the start of the next column when a group spills over. Today 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated Tomorrow 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness This Week 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance Group HeadersSmart Distribution
Today Tomorrow This Week
- Headers are never left as the last visible element in a column. - When a group spills, a gray duplicate header is added to the next column for context. ### Harmonious Group Columns When the number of group headers equals the maximum columns and there's enough space, the engine places each group in its own column for a more harmonious layout. Today 1 Morning Meeting at ReceptionTeam sync and birthday signings 9:00 AM - 9:30 AMDaily 2 Printer Jam Review with DwightCheck for bear-related bugs 10:30 AM - 11:30 AMReview Tomorrow 1 Beach Games PlanningPlan next two weeks 10:00 AM - 12:00 PMPlanning 2 Client Demo: WUPHFShow new 'features' of shouting 2:00 PM - 3:00 PMDemo 3 Sabre Triangle Tablet BrainstormDiscuss shape-based confidence 4:00 PM - 5:00 PMTechnical-ish This Week 1 Security Walkthrough with IT GuyQuarterly review WednesdaySecurity 2 Team Retro: Goodbye, TobySprint reflection (but no sprints) FridayRetrospective OverflowHarmonious Groups 3 Columns (2/3/2)
Today Tomorrow This Week
### With dividers When the Overflow engine hides items to fit the height or column budget,[Divider](/framework/docs/3.1/divider) elements between items are hidden when either adjacent item is hidden. That way you never see an orphaned divider - a divider is only shown when both the item above and the item below are visible. Place `.divider` between `.item`[Item](/framework/docs/3.1/item) elements in your overflow container; the engine handles visibility automatically. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowWith dividers
...
...
...
### Backwards compatibility The Overflow engine[Overflow](/framework/docs/3.1/overflow) remains compatible with legacy list attributes used in older plugins. When these attributes are present on a descendant `.column` or `.list`, the engine will promote them onto the enclosing `.columns`[Columns](/framework/docs/3.1/columns) container at runtime. #### Supported legacy attributes - `data-list-limit="true"`: opts the list into Overflow. If the enclosing `.columns` lacks `data-overflow-max-cols`, it will default to `1`. - `data-list-max-columns`: sets the maximum column count. Responsive variants (e.g., `-sm`, `-md`) on the legacy element are honored when present; the base value is used for the engine. - `data-list-max-height`: sets the height budget for the columns. Use a pixel value (e.g., `340`) or `auto` to inherit from the parent container. - `data-list-hidden-count`: older toggle for the trailing label; still honored when present. - Divider visibility when items are hidden: see the With dividers section above. #### Legacy example (still works) 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority 5 Server Closet Check & 'Kevin’s Famous' Recovery DrillOpen the mystery door, wiggle the beeping thing, and attempt a restoration without spilling anything. Update instructions: 'bring oven mitts.' 6:00 PM - 7:00 PMAutomated 6 Security Walkthrough & 'Frame Toby' Boundary ReviewMonthly pass including badge checks, warehouse notes, and a plan that does not involve planting fake drugs in HR. Angela brings a clipboard and a stare. 10:00 AM - 12:00 PMCritical 7 Quarterly Reviews & Growth Plan: The 'Assistant to the' LadderEvaluate performance using a rubric Jim swears isn’t a prank. Clarify that 'Assistant to the Regional Manager' is technically growth if you squint. 1:00 PM - 2:30 PMScheduled 8 Sabre Printer Jam Night (Bring Marshmallows)Apply latest stickers, test that smell of burning goes down, and keep watch while Michael brainstorms catchphrases for 'a printer that catches fire less.' 11:00 PM - 12:00 AMMaintenance 9 Onboarding Workshop: The Dundies for New HiresWelcome session covering desk decor, how to ignore Dwight’s evacuation drills, and what a Dundie is. Hands-on lab: unjamming the copier while smiling. 9:30 AM - 11:30 AMRequired 10 Budget Planning & 'Surplus' Allocation MeetingQ4 planning where Oscar explains the surplus gently, Michael hears 'new chairs,' and Recyclops is briefly appointed Treasurer of Petty Cash. 3:00 PM - 5:00 PMPlanning 11 Vendor Check-In & Utica DétenteQuarterly vendor review covering deliveries, pricing, and whether Karen will accept a peace offering in the form of soft pretzels. 2:00 PM - 3:00 PMBusiness 12 Bulletin Board Refresh & 'Fun Run' PostmortemUpdate flyers, retire 'Run for Rabies' glitter, and pin a cautionary note on carbo-loading before 5Ks. Remove anything last edited by 'William M. Buttlicker.' 4:00 PM - 5:30 PMDocumentation 13 Office Health Check & Beet Farm Capacity PlanCheck plants, temperature wars, and snack drawer diplomacy. Forecast beet yields just in case we pivot to agriculture. 8:00 AM - 9:00 AMMonitoring 14 Coffee Chat & 'Pretzel Day' HypeInformal bonding to share wins, learnings, and toppings. Scheduling anything on Pretzel Day is punishable by Stanley’s glare. 10:15 AM - 10:45 AMSocial 15 Vance Refrigeration Contract Walk-ThroughGo over terms with Phyllis and Bob. Everyone nods, someone says 'classy,' and the thermostat mysteriously gets colder. 11:00 AM - 12:30 PMBusiness 16 Suggestion Box ArchaeologyAnalyze themes, rank 'more jazz' vs 'less jazz,' and translate 'Stop stealing my pens — looking at you, Jim' into action items. 1:30 PM - 2:30 PMResearch 17 Beach Games Capacity AlignmentPlan teams, veto sumo suits, and identify who has 'coal-walk energy.' Crown no one via hot-dog-eating contest this time. 3:00 PM - 4:00 PMPlanning 18 Michael Scott Paper Company Comeback RitualsCodify traditions like celebratory pancakes, van air freshener procurement, and how many cheese puffs is 'a business expense.' 4:30 PM - 6:00 PMCeremony 19 Learning Session: 'Scott’s Tots' (What Not To Promise)A cautionary tale about promises, tuition, and the importance of reading fine print. Bring tissues and a respectful silence. 5:00 PM - 6:00 PMEducation 20 Branch Wars: Lessons LearnedReflect on what went well (matching bandanas), what didn’t (kidnapping a copier), and pledge fewer disguises on weekdays. 6:00 PM - 7:00 PMReview 21 Parkour Safety CommitteeEstablish rules: no vaulting over reception, no roof-to-truck, and absolutely no yelling 'PARKOUR!' near coffee cups. 7:30 PM - 8:30 PMSafety 22 Monthly 'WUPHF' Etiquette & Usage ReportsReview how many people were pinged on fax, pager, and pigeon. Identify experiments that use fewer exclamation points. 9:00 PM - 10:00 PMAnalytics-ish 23 Emergency Response & Fire Drill Protocol (Real, Not Dwight)Runbook review for real incidents including paging, escalation, and humane door-unlocking. Cat remains on the ground. 12:00 AM - 12:00 AMEmergency 24 Quality Check: 'Product Recall' EditionFocus on critical paths, error stickers, and watermarking so the kids don’t prank us back. Apologies printed only last resort. 4:00 PM - 5:30 PMConfirmed 25 Calendar Review & 'Casino Night' PlanningAlign dates, identify risks, and pretend this is not the most dramatic meeting since 'Ryan started a fire.' 10:00 AM - 11:30 AMAlignment 26 Incident Postmortem: 'The Injury' Prevention PlanWalk through the timeline, document contributing factors such as foreheads meeting George Foreman grills, assign owners for fixes. 1:00 PM - 2:00 PMPostmortem 27 Cleanup Session & 'Prison Mike' Speech PracticeTarget messy drawers, reduce chaos, add labels, and rehearse the part about dementors so it lands but HR doesn’t call. 2:30 PM - 4:00 PMTidy 28 ‘Booze Cruise’ Dry Run & Do-Not-Rock-the-BoatSimulate a trip, practice speeches only when the boat is actually moving, and identify the life jacket with Michael’s name. 7:00 PM - 8:00 PMDR Drill 29 New Copier Button Rollout & 'Subtle(?)' Signage PlanDefine phased enablement, teach Michael that a toggle is not a clapper, and pick fonts that don’t start an argument. 11:00 AM - 12:00 PMRollout 30 Benihana Team Dinner Logistics & The Two-Tables ProblemCoordinate seating so we know who is who this time. Confirm orders, identify the correct server, and practice gentle nodding. 3:00 PM - 4:30 PMLogistics 31 Privacy Review & 'Email Surveillance' BoundariesAudit peeking habits, update 'no snooping' posters, and remind the team that surveillance episodes belong on DVDs. 9:00 AM - 10:00 AMCompliance 32 Hiring Panel & 'Chair Model' CalibrationRun interviews, agree that singing to a photo is not a necessary skill, and choose someone who understands copy paper. 12:30 PM - 3:00 PMHiring 33 Community AMA & 'Ask Me Anything Except Sales Numbers'Live Q&A with fans and power users about direction, upcoming bits, and whether Creed actually works here. He does. We think. 5:00 PM - 6:00 PMCommunity 34 Finance Reconciliation & 'Boom Roasted' ReportMonthly tally of invoices, payouts, refunds, and unexplained charges marked 'wolf dot com.' Oscar translates all of it. 4:30 PM - 6:00 PMFinance OverflowLegacy attributes
#### Notes - `data-overflow-cols` (fixed count) takes precedence over `data-overflow-max-cols` (best‑fit up to N) when both are present. - When only `data-list-limit` is present, the engine defaults to 1 column. - By default, the trailing hidden count label is not shown. Enable with `data-overflow-counter="true"`. Legacy `data-list-hidden-count` remains supported. Previous [Text Stroke Legible text when displayed on shaded backgrounds](/framework/docs/3.1/text_stroke) Next [Table Overflow Handle table rows overflow](/framework/docs/3.1/table_overflow) --- # Table Overflow When a table has more rows than can fit within the available vertical space, it constrains its height and appends a trailing “and X more” row to indicate the hidden entries. ### Basic usage To enable table overflow handling, add `data-table-limit="true"` to your ``[Table](/framework/docs/3.1/table) . Control the maximum height with `data-table-max-height` using a pixel value (for example, `240`) or set `auto` to inherit from the parent container. When rows exceed the available height, a trailing "and X more" row is added automatically. | Attribute | Default | Description | | --- | --- | --- | | `data-table-limit="true"` | — | Enable overflow handling and trailing "and X more" row | | `data-table-max-height` | auto | Maximum table height (px). Use `auto` to inherit from parent | | `table-overflow-counter` | true | Set to `false` to hide the "and X more" counter row while still clipping overflow rows |
...
### Table Overflow A regular-sized table demonstrating the overflow behavior within a fixed height budget. | | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | --- | | 1 | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | 2 | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | 3 | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | 4 | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | 5 | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | 6 | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | 7 | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | 8 | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | 9 | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | 10 | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | 11 | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | 12 | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | 13 | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | 14 | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | 15 | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | 16 | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | 17 | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | 18 | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | 19 | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | 20 | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | 21 | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | 22 | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | 23 | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | 24 | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | 25 | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | 26 | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | 27 | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | 28 | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | 29 | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | 30 | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | Table OverflowRegular • Auto height
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
### Small table overflow When a table uses `table--small` or `table--xsmall`, the "and X more" counter row automatically scales to match using a smaller label style. | | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | --- | | 1 | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | 2 | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | 3 | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | 4 | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | 5 | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | 6 | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | 7 | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | 8 | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | 9 | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | 10 | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | 11 | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | 12 | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | 13 | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | 14 | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | 15 | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | 16 | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | 17 | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | 18 | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | 19 | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | 20 | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | 21 | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | 22 | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | 23 | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | 24 | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | 25 | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | 26 | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | 27 | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | 28 | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | 29 | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | 30 | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | Table OverflowSmall • Auto height
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
### Hidden counter Use `table-overflow-counter="false"` to clip the table at its height budget without showing the "and X more" row. | | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | --- | | 1 | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | 2 | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | 3 | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | 4 | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | 5 | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | 6 | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | 7 | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | 8 | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | 9 | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | 10 | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | 11 | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | 12 | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | 13 | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | 14 | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | 15 | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | 16 | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | 17 | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | 18 | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | 19 | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | 20 | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | 21 | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | 22 | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | 23 | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | 24 | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | 25 | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | 26 | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | 27 | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | 28 | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | 29 | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | 30 | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | Table OverflowRegular • Counter hidden
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--table-tbody-height` | 46px | — | — | — | | `--table-thead-height` | 36px | — | — | — | | Xsmall | | | | | | `--table-xsmall-tbody-height` | 22px | — | — | — | | `--table-xsmall-thead-height` | 18px | — | — | — | | Small | | | | | | `--table-small-tbody-height` | 31px | — | — | — | | `--table-small-thead-height` | 24px | — | — | — | | Large | | | | | | `--table-large-tbody-height` | 56px | — | — | — | | `--table-large-thead-height` | 44px | — | — | — | | Xlarge | | | | | | `--table-xlarge-tbody-height` | 72px | — | — | — | | `--table-xlarge-thead-height` | 56px | — | — | — | Previous [Overflow Handle column items overflow](/framework/docs/3.1/overflow) Next [Clamp Manage text overflow with single and multi-line truncation](/framework/docs/3.1/clamp) --- # Clamp The Clamp engine truncates text to a specified number of lines using word-based ellipsis. It preserves the original text, measures available width, and re-applies clamping whenever layouts change. ### Basic usage Add `data-clamp="N"` to any text element to clamp it to N lines. The engine preserves the original text and re-applies clamping when layouts change. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority ClampStandalone: Descriptions 1–4 lines Example description text that will be clamped to two lines ### Responsive Use the same size and orientation modifiers as other framework components. Specificity (most specific first): size + orientation (e.g. `data-clamp-md-portrait`), size only (`data-clamp-sm`, `-md`, `-lg`), orientation only (`data-clamp-portrait`), then base `data-clamp`. Clamp to 2 lines by default, 4 on medium+ screens, 1 in portrait ### Backward Compatibility Legacy class tokens are supported and mapped to the Clamp engine automatically: `clamp--none` disables clamping, and `clamp--1` through `clamp--50` request N lines. 1 Scranton Strategy Alignment & Cross-Department Fire Drill PostmortemWeekly alignment where Michael brings a whiteboard titled 'Vision Board' and Dwight brings a megaphone. We cover priorities (sell paper), blockers (also sell paper), and stakeholder comms (Jan keeps emailing 'no'). Includes a risk roundtable about candles near plasma TVs. 9:00 AM - 10:00 AMConfirmed 2 Client Presentation: 'Threat Level Midnight' Business CutMarquee client session featuring tasteful charts by Pam, heartfelt narration by Agent Michael Scarn, and exactly one confetti pop. Feedback collected, dignity mostly preserved. 2:00 PM - 3:30 PMTentative 3 Café Disco Launch & Dundies Seating ChartTurn in every form, tape down every extension cord, and finalize seating so no one sits near the speaker labelled 'BASS.' Creed volunteers to be DJ and then vanishes. 11:59 PMImportant 4 Complaint Box Sorting & Schrute Compliance SweepSort grievances ('too much jazz,' 'too little jazz'), verify stapler locations, and initial the 'identity theft = not a joke' acknowledgement sheet. 3:30 PM - 4:30 PMHigh Priority Clamp (Legacy)Descriptions: clamp--1, clamp--2, clamp--3, clamp--none Example description with legacy class clamped to two lines of text Previous [Table Overflow Handle table rows overflow](/framework/docs/3.1/table_overflow) Next [Format Value Format numbers and values with consistent styling](/framework/docs/3.1/format_value) --- # Format Value The Value Formatting system automatically formats numeric values to fit within their containers while maintaining readability. It supports various formatting options including abbreviations (K, M, B), dynamic precision adjustment, and currency values with proper symbol placement. ### Basic Usage To enable automatic value formatting, add the `data-value-format="true"` attribute to your element. 2345678XLarge 2345678Regular 2345678Small 456789XLarge 456789Regular 456789Small 34562XLarge 34562Regular 34562Small Value FormattingSize Comparison 2345678 456789 34562 To add a delimiter to large numbers, for example 1234 => 1,234, see [custom filters](https://intercom.help/trmnl/en/articles/10347358-custom-plugin-filters). ### Currency Values Values with currency symbols are automatically formatted while maintaining the symbol placement. $2345678XLarge $2345678Regular $2345678Small $456789XLarge $456789Regular $456789Small $34562XLarge $34562Regular $34562Small Value FormattingCurrency Example $2345678 $456789 $34562 To add a currency symbol, for example 1234 => $1,234, see [custom filters](https://intercom.help/trmnl/en/articles/10347358-custom-plugin-filters). Supported currency symbols include: `$`US Dollar `€`Euro `£`British Pound `¥`Japanese Yen / Chinese Yuan `₴`Ukrainian Hryvnia `₹`Indian Rupee `₪`Israeli Shekel `₩`Korean Won `₫`Vietnamese Dong `₱`Philippine Peso `₽`Russian Ruble `₿`Bitcoin ### Regional Number Formats Numbers can be formatted according to different regional standards using the `data-value-locale` attribute. $123456.78United States (en-US) $123456.78United States (en-US) €123456.78German (de-DE) €123456.78German (de-DE) €123456.78French (fr-FR) €123456.78French (fr-FR) Value FormattingRegional Formats $123456.78 €123456.78 €123456.78 Common locale options include: `en-US`United States (123,456.78) `de-DE`German (123.456,78) `fr-FR`French (123 456,78) `en-GB`British English (123,456.78) `ja-JP`Japanese (123,456.78) If no locale is specified, numbers will be formatted using US format (en-US) by default. Previous [Clamp Manage text overflow with single and multi-line truncation](/framework/docs/3.1/clamp) Next [Fit Value Automatically resize numbers and values to fit within their containers](/framework/docs/3.1/fit_value) --- # Fit Value The Text Fitting system automatically adjusts the font size, weight, and line height of text elements so that they fit within their containers. This dynamic resizing is especially useful for responsive layouts where available space may vary, ensuring that content is always legible. ### Basic Usage To enable automatic text resizing, add the `data-value-fit="true"` attribute to your element. The system will automatically adjust the element's font size and weight to fit the content on a single line within the container width. This works for both numeric and text values. For multi-line text fitting, see the `data-value-fit-max-height` option below. $1,000 Dundies Budget $1,000,000 Scott's Tots $1,000,000,000 Sabre Revenue Fit ValueDunder Mifflin $1,000 $1,000,000 $1,000,000,000 ### Single-line Text Fitting Text values are automatically fitted to a single line when using `data-value-fit="true"` without specifying a max height. The system derives the single-line height from the element's computed line-height, then shrinks the font size and weight until the text fits on one line within the container. Assistant to the Regional Manager Dwight's Title Threat Level Midnight Michael's Film World's Best Boss Michael's Mug Fit ValueDunder Mifflin Assistant to the Regional Manager Threat Level Midnight World's Best Boss ### Multi-line Text Fitting To allow text to wrap across multiple lines while still fitting within a constrained area, specify a maximum height using the `data-value-fit-max-height` attribute (in pixels). This overrides the default single-line behavior, allowing the text to wrap while ensuring it stays within both width and height constraints through automatic font size and weight adjustments. Would I rather be feared or loved? Easy. Both. I want people to be afraid of how much they love me. Michael Scott Identity theft is not a joke, Jim! Millions of families suffer every year. Dwight Schrute That's what she said. Michael Scott Fit ValueDunder Mifflin Would I rather be feared or loved? Easy. Both. I want people to be afraid of how much they love me. Identity theft is not a joke, Jim! Millions of families suffer every year. That's what she said. Previous [Format Value Format numbers and values with consistent styling](/framework/docs/3.1/format_value) Next [Content Limiter Change font size when content overflows to fit within the container](/framework/docs/3.1/content_limiter) --- # Content Limiter The Content Limiter system automatically restricts the height of content areas based on the view type. When content exceeds the height threshold, it applies smaller typography and truncates overflowing text using the Clamp Engine to fit the available space. ### Basic usage To enable automatic content limiting, add the `data-content-limiter="true"` attribute to your content element. Dinner Party Michael finally manages to trick Jim and Pam into coming over to his condo for a couples’ dinner with him and Jan. He has been begging them for ages, and they finally run out of excuses. The first thing Michael shows off is his pride and joy: a plasma TV mounted on the living room wall. The problem is that it is laughably tiny, barely larger than a computer monitor. Michael beams with pride as he demonstrates how it can “push right back against the wall” to save space, while Jim and Pam exchange polite smiles that barely conceal their disbelief. Things take a sharper turn when Jan puts on a CD. The music is recorded by her former assistant, Hunter, and the lyrics make it sound like the two of them were more than just colleagues. Jan sways to the music with a dreamy smile, while Michael tries to ignore the implication. Jim and Pam sit frozen, realizing they have front-row seats to a relationship meltdown. Dinner itself is no relief. Jan’s cooking is nowhere near ready, so the group is stuck nibbling on appetizers for what feels like hours. When the food does arrive, Jan scolds Michael for trying to eat early, and their bickering turns openly hostile. Andy and Angela, the other guests, sit uncomfortably as the couple jabs at each other across the table. Every sarcastic comment cuts deeper, and the laughter that should fill a dinner party never comes. Michael, in a desperate attempt to lighten the mood, suggests games. They try charades, but even that devolves into more fighting. Jan mocks Michael’s answers, Michael whines back, and suddenly it feels less like a game and more like another round of public humiliation. The tiny condo seems to shrink with every cutting remark. It all finally explodes when Jan accuses Michael of being childish and Michael lashes out in return. In her fury, Jan grabs one of his beloved Dundie trophies and hurls it at the plasma TV, shattering it. For Michael, this little TV was his greatest treasure, and now it lies in pieces on the floor. The room falls into stunned silence as everyone realizes the night has gone completely off the rails. The guests slowly make their exit while Jan and Michael continue to argue in the background. Jim and Pam are relieved just to escape with their sanity intact. What started as a simple dinner party turned into one of the most uncomfortable nights anyone could imagine. For viewers, it is both painful and hilarious, a perfect storm of Michael’s desperate need to impress and Jan’s seething resentment. And right at the center of it all, that ridiculous little plasma TV never stood a chance. Content LimiterText Exceeding Threshold World’s Best Boss Michael sits proudly at his desk, sipping from his 'World’s Best Boss' mug - a mug he bought for himself at Spencer’s Gifts. He points it out to the documentary crew, insisting that it’s not just a mug, but an irrefutable piece of evidence of his leadership skills. The cameras meet the Scranton branch: Jim, casually wry; Dwight, rigid and overzealous; Pam, quietly patient at reception; and Ryan, the new temp, still figuring out the terrain. Michael parades the crew through introductions, performing for them as much as managing his team. News of possible downsizing blows in with Jan from corporate. Michael tries to project calm and control, but he dodges straight answers and leans on bad jokes, more concerned with optics than clarity. The unease trickles through the bullpen. Content LimiterText Within Threshold

Contrary to what one might think, the Lorem ipsum text, despite being meaningless, has noble...

When content exceeds the height threshold, the limiter adds the `content--small` class and automatically truncates the first overflowing block using the Clamp Engine [Clamp](/framework/docs/3.1/clamp) so it fits the remaining space. Subsequent blocks are hidden. ### Custom Height Threshold You can specify a custom maximum height using the `data-content-max-height` attribute. George Foreman Grill Accident Michael explains to the office that every morning he wakes up to the smell of sizzling bacon. His method is both elaborate and ill‑advised: he sets a George Foreman Grill at the foot of his bed the night before, lays out strips of bacon, and switches it on so he can rise to the aroma like a king. He frames it as self‑care; everyone else hears 'fire hazard.' One morning, the fantasy meets physics. Half‑asleep, he swings his legs out of bed and plants his bare foot directly onto the hot metal. There is a hiss, a yelp, and a chaotic dance around the bedroom as he tries to untangle himself from the cord. By the time he calls in, the drama has grown from 'burn' to 'catastrophic workplace injury.' He arrives at the office limping with exaggerated gravitas, demanding sympathy, rides, and special parking, and comparing his situation to permanent disability. He asks HR about accommodations, requests that meetings be moved closer to his desk, and insists that no one truly understands the daily challenges he now faces. Pam offers ice and a ride to the clinic, Jim suggests - deadpan - that perhaps bacon should not be cooked in bed, and Dwight prescribes a bizarre regimen of ointments and battlefield procedures. The staff cycle between concern and disbelief as Michael narrates the incident like an inspirational keynote about resilience. Throughout the day he milks the moment for attention, turning routine tasks into obstacles that require applause when completed. He stages slow, heroic walks through the bullpen, interrupts conversations to retell the story, and peppers in phrases like 'bravery' and 'adversity' as though he has survived a mountaineering accident. Prison Mike When the office staff complain that work feels like prison, Michael decides the only responsible thing to do is educate them - by transforming into a cautionary tale. He frames it as a necessary intervention, the kind of hard truth only a courageous leader can deliver, and you can see the excitement building as he prepares to debut his latest persona. He strides back in with a purple bandana tied tight, drops his voice into a cartoonish growl, and declares himself 'Prison Mike.' He prowls the floor between desks, demanding attention like a substitute teacher who has watched one too many crime dramas, punctuating every other sentence with dramatic pauses and finger‑pointing as if he’s narrating a documentary only he can see. What follows is a torrent of wildly inconsistent 'facts' about prison life. Michael talks about hardened criminals and gangs, then veers into a menu of gruel, gruel sandwiches, and gruel omelets, insisting that dessert is 'sometimes more gruel' delivered by 'mean guards' who hate birthdays. The contradictions pile up with every step as he tries to sell a world he clearly only knows from pop culture and half‑remembered movie trailers. The infamous moment arrives when he proclaims that the very worst part of prison was 'the Dementors' - a dead giveaway that his knowledge is borrowed from Harry Potter. The room goes silent. Eyes shift. Someone smirks, someone coughs, and even Michael seems to realize he’s said something unfixable, yet he barrels ahead as though this were privileged information from a maximum‑security wizarding wing. Undeterred, Michael adds threats and warnings that sound like Mad Libs tough‑guy talk: no birthday cake, no daylight, cement pillows, and constant danger. He paints the air with big, frightening shapes, then pivots into a lecture about gratitude for fluorescent lights, ergonomic chairs, and the bounty of the vending machine, as if Snickers bars and swivel bases are society’s thin line against chaos. Jan and Toby try to intervene from the sidelines, steering him toward something resembling a real HR conversation, but Michael only doubles down. He declares this a 'teachable moment,' commands silence with a raised hand, and instructs everyone to thank him for his service as an educator, as though applause could retroactively turn improv into policy. Content LimiterCustom Max Height: 140px

Contrary to what one might think, the Lorem ipsum text, despite being meaningless, has noble...

Contrary to what one might think, the Lorem ipsum text, despite being meaningless, has noble...

### Mashup Example This demonstrates content limiting within a multi‑view mashup. CPR Training After a scare that puts office safety under the microscope, a CPR instructor arrives with a plastic mannequin and a stack of laminated handouts. The room is supposed to be quiet and focused; it never is, because Michael treats 'training' like a stage and 'protocol' like a suggestion. Within minutes, Michael hijacks the lesson with questions that are neither helpful nor on topic. He is equal parts class clown and self‑appointed co‑instructor, correcting the professional with confidence born of zero expertise, leaning on phrases like 'best practices' and 'synergy' as though corporate jargon could resuscitate a heartbeat. When the instructor suggests keeping rhythm to the Bee Gees’ 'Stayin’ Alive,' Michael hears an invitation to perform. He claps, sings too loudly, and turns compressions into choreography. Andy, never one to resist a harmony, joins in until the room resembles a karaoke night held in a first‑aid class, with Kevin attempting a bass line and Phyllis swaying like it’s a slow dance. Dwight, determined to demonstrate 'real' preparedness, starts issuing commands and measuring breaths with militaristic seriousness. His eagerness to escalate quickly outpaces his understanding of what’s appropriate in any setting, let alone a medical one, and he begins inventing scenarios that require handcuffs and a field promotion. The instructor tries to regain control, but the demonstration has become a Michael Scott production. He monologues about leadership, teamwork, and the importance of morale, somehow managing to miss every actual learning objective while drawing a flowchart that labels 'CPR' as 'Celebrate Positive Resilience.' Then comes the unforgettable turn: Dwight produces a knife, slices the face from the mannequin, and wears it like a mask. The room recoils in a synchronized gasp as the instructor’s expression travels from confusion to horror, and Michael declares, inexplicably, that this is 'advanced tactics' they will not be tested on. By the end, no one can say they learned CPR, though everyone can keep time to 'Stayin’ Alive' and will forever remember what not to do with training equipment. The chaos is so complete it loops back into comedy, a memory that will haunt the break room for months every time the song plays on the radio. I Declare Bankruptcy! As bills stack up and the numbers stop making sense, Michael does what he always does when adulthood becomes overwhelming - he looks for a grand gesture that will make the problem disappear. He imagines a clean slate delivered not by accountants or courts, but by the sheer force of a bold announcement. Oscar tries to help, carefully explaining what bankruptcy actually is: a legal process, forms, courts, a plan. Michael nods, absorbing none of it, because he has already decided on a solution that feels simpler and much more theatrical, the financial equivalent of cutting a ribbon and calling it a day. He walks into the bullpen, squares his shoulders like a man about to make history, clears his throat, and bellows at full volume: 'I DECLARE BANKRUPTCY!' The final word echoes against the ceiling tiles as if volume alone could reset his bank account, and he looks around expectantly, awaiting the bureaucratic magic he believes he has just triggered. Silence follows. A few heads pop up over monitors. Confused looks ripple through the room. Kevin wonders out loud if that’s actually how it works; Creed nods as though he’s tried it in three countries. Michael stands tall, waiting for the visible relief that never arrives - no confetti, no instant credit score bump, just awkward quiet and the hum of the copier. Oscar pulls him aside again, gently clarifying that bankruptcy is not an incantation. It is paperwork, not pageantry. Michael seems genuinely stunned, as if someone told him that wishing on a star requires a notary, and he repeats the word 'forms' like it’s a personal insult. Refusing to let the moment die, he pivots to half‑baked fixes: whiteboard budgets with arrows and underlines, promises to 'tighten belts,' and a plan that mostly consists of other people making sacrifices. He proposes eliminating 'non‑essential' expenditures that suspiciously exclude novelty mugs and a rotating snack budget, none of which addresses the actual math. Back in his office, he stares at bank statements with the intensity of a person trying to will the numbers into alignment. He whispers 'I declare bankruptcy' one more time under his breath, as if a quieter version might be legally binding, then practices saying it in a more official tone for the camera. Scott’s Tots Michael proudly struts into a high school classroom filled with expectant seniors, all of whom have been promised by him years earlier that he would pay for their college tuition. The moment is set up like a triumphant return; the students cheer and clap as Michael enters, and for a fleeting second, he basks in the illusion of being a hero. Their excitement is palpable, with handmade signs and chants of 'Thank you, Mr. Scott!' ringing in his ears. But underneath Michael’s forced smile lies sheer panic. As he begins his speech, his voice quivers ever so slightly, betraying his nerves. He tries to stretch out his introduction with jokes and awkward pauses, desperately searching for a way to soften the devastating truth he has to deliver. The students, however, hang on his every word, their faces glowing with hope and admiration for the man they believe has single-handedly secured their futures. When the truth finally comes out - that Michael does not, in fact, have the money to pay for their tuition - the atmosphere in the room collapses instantly. Gasps and groans replace the cheers, and disbelief spreads across the room like a wave. Michael attempts to salvage the moment by offering to pay for everyone’s laptop batteries, a pathetic gesture that only highlights the absurdity of his promise. The crushing disappointment on the students’ faces makes the scene almost unbearable to watch. The pain of the moment is amplified by how sincerely Michael believed in his original promise. Years earlier, he had genuinely thought he would become wealthy enough to follow through, blinded by his own optimism and detachment from reality. Now, forced to confront the impossibility of his pledge, he tries to laugh it off and hide behind humor, but the room is heavy with betrayal and crushed dreams. As the students press him with questions and accusations, Michael becomes visibly smaller, almost shrinking into himself. His usual bravado evaporates as he stammers and dodges eye contact. The power dynamic has shifted completely - no longer the adored benefactor, he is now the object of ridicule and anger. The laughter in this scene comes not from jokes, but from the unbearable awkwardness of Michael’s failed attempt to maintain dignity in an impossible situation. What makes 'Scott’s Tots' legendary is the raw, secondhand embarrassment it evokes. Viewers squirm in discomfort as Michael struggles, yet it is impossible to look away. It’s a perfect example of how The Office blends comedy and tragedy, creating a scene so painful that it loops back around into being hilarious. The genius lies in how Michael’s delusions of grandeur are shattered not with slapstick, but with the crushing weight of reality. By the time Michael leaves the classroom, he is utterly defeated, his reputation destroyed in front of dozens of hopeful young people. For the students, it’s the death of a dream. For Michael, it’s yet another reminder of how his desperate need to be loved and admired leads him into catastrophic decisions. And for the audience, it’s a masterclass in cringe comedy, one of the most excruciating yet unforgettable moments in television history. Previous [Fit Value Automatically resize numbers and values to fit within their containers](/framework/docs/3.1/fit_value) Next [Pixel Perfect Ensure text renders with crisp edges by aligning to the pixel grid](/framework/docs/3.1/pixel_perfect) --- # Pixel Perfect The Pixel Perfect system ensures text renders with crisp edges by aligning text precisely to the pixel grid. We use specialized pixel fonts designed to work at specific pixel sizes, producing the sharpest possible rendering and preventing sub-pixel rendering issues that cause text to appear blurry or inconsistently bold in final renders when converting layouts to a 1-bit color space for ePaper displays. ### Usage To enable pixel perfect text rendering, add the `data-pixel-perfect="true"` attribute to your text element. Pixel Perfect Title This text is rendered with pixel perfect alignment, ensuring that each line falls exactly on the pixel grid. Depending on the parent width, the system automatically breaks text into lines and adjusts each line's width. This produces crisp, sharp text edges that renders perfectly in a 1-bit color space. Pixel PerfectWith Pixel Perfect Applied Pixel Perfect Title

This text will be aligned to the pixel grid for crisp rendering.

### Why? Text rendering on digital displays involves complex anti-aliasing techniques to make text appear smooth at various sizes. This process creates partially opaque pixels (gray pixels) at the edges of characters to create the illusion of smoothness. When text isn't perfectly aligned to the pixel grid, these anti-aliased pixels can appear inconsistently, particularly with centered text. This is especially problematic for ePaper displays that use a 1-bit color space (just black and white, no grays), where anti-aliased gray pixels are forced to become either fully black or fully white. The result is text that appears randomly bold or distorted in final renders, creating an unprofessional and difficult-to-read presentation. Our system uses pixel fonts that are specifically designed to work at particular pixel sizes and their multipliers. These fonts are meticulously crafted to perfectly align with the pixel grid, ensuring each character renders with maximum sharpness and clarity. By combining these specialized fonts with our pixel-perfect alignment technique, we achieve optimal text rendering for ePaper displays. ### How It Works The Pixel Perfect system works by applying the following techniques to elements with `data-pixel-perfect="true"`: - The system measures the parent element's width to determine whether it's odd or even - The text content is broken into individual lines - Each line is wrapped in a span element - Each span's width is adjusted to match the parent's pattern: even widths for even-width parents, odd widths for odd-width parents By analyzing the parent container's dimensions and adjusting each line accordingly, the system ensures text falls precisely on the pixel grid. This precise adjustment ensures text is perfectly aligned to the pixel grid, eliminating sub-pixel rendering issues on 1-bit displays. ### Cross-Platform Consistency Different browsers render text differently across operating systems. For example, Chrome on macOS handles font rendering differently than Chrome on Linux or Windows. This means developers see different results depending on their development environment. The Pixel Perfect system unifies the developer experience across platforms by enforcing consistent rendering rules regardless of the browser or operating system. This ensures that text renders with the same crispness and weight consistency on the final ePaper display, regardless of where it was developed or previewed. Previous [Content Limiter Change font size when content overflows to fit within the container](/framework/docs/3.1/content_limiter) Next [Framework Runtime How the runtime applies layout, clamping, overflow, and presentation adjustments at render time](/framework/docs/3.1/framework_runtime) --- # Framework Runtime Different devices have different, fixed amounts of screen space. The Framework Runtime fills that space the best way it can when a plugin layout renders. It follows a few simple rules and does the heavy, repetitive calculations for you, so the result is clean and reliable every time. You can inspect it from the “Framework Runtime” panel in the top navigation. ### What it does At a high level, it measures the space you have and then plans columns, clamps text, formats and fits values, and adjusts gaps and index widths - so everything fits neatly without manual tweaking. - Normalizes screen context (size, orientation, bit depth, scale) - Maps legacy responsive classes to data attributes for consistency - Formats values and fits numbers into their containers - Adjusts gaps so columns/grids land on integer pixel widths - Plans multi-column layouts, re-clamps text per column, and can add a trailing “and N more” label when enabled - Applies standalone clamping where needed outside columns - Limits overly tall content and schedules pixel-perfect text processing ### Runtime steps and stats When the runtime executes, it performs these steps in order. #### Clamp Clamps text to N lines. - Word‑based ellipsis - Preserves original text - Re‑clamps when widths change - Supports responsive data attributes (size/orientation) - Maps legacy class utilities to `data-clamp` - Applies outside and inside columns (per‑column re‑clamp handled by Overflow) Go to[Clamp](/framework/docs/3.1/clamp) #### Overflow Plans 1..N columns with off‑screen staging and commits the best fit, then re‑clamps per real column width. - Duplicates group headers across columns when needed - Optional trailing “and N more” label for hidden items (enable with `data-overflow-counter="true"`) - Enforces final fit by hiding trailing items if necessary Go to[Overflow](/framework/docs/3.1/overflow) #### Value Formatting Formats numbers to fit available space and abbreviates as needed (k, M, B). - Respects `data-value-locale` - Works with `data-fit-value` for auto‑sizing Go to[Format Value](/framework/docs/3.1/format_value) #### Fit Value Adjusts font size, line‑height, and weight to fit numbers within their containers. - Minimum font size safeguard (default 8px) - Accepts `data-fit-value` or `data-value-fit` Go to[Fit Value](/framework/docs/3.1/fit_value) #### Grid Gaps Tweaks CSS gaps so grid column widths resolve to integer pixels. - Disable with `data-adjust-grid-gaps="false"` - Falls back to measuring child positions when `gap` is not explicitly set Go to[Grid](/framework/docs/3.1/grid) #### Column Gaps Normalizes gaps between `.column` elements so column widths are integers. - Disable with `data-adjust-column-gaps="false"` - Runs as a pre‑pass for non‑overflow columns and a final pass after Overflow Go to[Columns](/framework/docs/3.1/columns) #### Pixel‑Perfect Fonts Wraps lines in spans and enforces even/odd widths for crisp rendering; scheduled in idle time. - Skipped on higher bit‑depth modes - Respects centered alignment Go to[Pixel Perfect](/framework/docs/3.1/pixel_perfect) #### Content Limiter Constrains content height by view type or explicit threshold and flags small content. - Override via `data-content-max-height` - Adds `content--small` and `clamp--N` classes Go to[Content Limiter](/framework/docs/3.1/content_limiter) #### Index Widths Ensures item index badges render at even widths to avoid artifacts. - Observes width changes and adjusts without feedback loops - Runs after column layouts commit to cover in‑column indices Go to[Item](/framework/docs/3.1/item) ### Why this exists Plugins need to fit source data into a static layout space that is device‑defined and varies by model, orientation, and density. While this resembles responsive web design, the runtime provides purpose‑built tools—overflow planning, per‑column clamping, integer pixel alignment, and value fitting—tailored specifically for TRMNL devices. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--full-h` | calc(var(--screen-h) - var(--gap) * 2) | — | — | — | | `--full-w` | calc(var(--screen-w) - var(--gap) * 2) | — | — | — | | `--half_horizontal-h` | calc((var(--screen-h) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--half_horizontal-w` | calc((var(--screen-w) - var(--gap) * 2)) | — | — | — | | `--half_vertical-h` | calc((var(--screen-h) - var(--gap) * 2)) | — | — | — | | `--half_vertical-w` | calc((var(--screen-w) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--quadrant-h` | calc((var(--screen-h) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--quadrant-w` | calc((var(--screen-w) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--screen-h` | 480px | — | — | — | | `--screen-h-original` | 480px | — | — | — | | `--screen-w` | 800px | — | — | — | | `--screen-w-original` | 800px | — | — | — | | `--ui-scale` | 1 | — | — | — | Previous [Pixel Perfect Ensure text renders with crisp edges by aligning to the pixel grid](/framework/docs/3.1/pixel_perfect) Next [Structure The framework's exact div hierarchy and how Screen, View, Layout, Title Bar, Columns, and Mashup work together](/framework/docs/3.1/structure) --- # Structure The framework structure is a fixed hierarchy that defines the display environment. Screen, View, Layout, Title Bar, Columns, and Mashup each have a specific role. Plugins render their content inside Views. Follow the exact div setup; deviating causes layout and rendering issues. You don't specify Screen, Mashup, or View - the platform provides them automatically. You only specify the Layout and optionally a Title Bar. You provide the full hierarchy yourself: Screen, View, Layout, and optionally a Mashup container and a Title Bar.
...
...
...
...
### The Exact Structure The framework uses a fixed div hierarchy. Each level has a specific role. The canonical structure is: **Screen** → (**Mashup** →) **View** → **Layout** (+ optional **Title Bar**) [Screen](/framework/docs/3.1/screen)--portrait --no-bleed --dark-mode --og --v2 --backdrop [Mashup](/framework/docs/3.1/mashup)--1Lx1R --1Tx1B --2x2 --1Lx2R --2Lx1R --2Tx1B --1Tx2B [View](/framework/docs/3.1/view)--full --half_vertical --half_horizontal --quadrant [Layout](/framework/docs/3.1/layout)--row --col --left --center-x --right --top --center-y --bottom --center --stretch --stretch-x --stretch-y [Title Bar](/framework/docs/3.1/title_bar) ### Component Roles Each foundation component has a specific role. Use them as intended. #### Screen Root container. Defines viewport dimensions, padding, and CSS variables that cascade throughout. Go to[Screen](/framework/docs/3.1/screen) #### View Container for a plugin slot. Size modifiers (`view--full`, `view--half_horizontal`, `view--half_vertical`, `view--quadrant`) set how much space the plugin gets. Non-full views must be nested inside a Mashup. Go to[View](/framework/docs/3.1/view) #### Layout Exactly one per View. The content container. Its direct children are typically Columns, Grid, or Flex. Use `layout--row`, `layout--col`, and alignment modifiers to arrange those children. See the Layout page's "What Goes Inside Layout" section for when to use each. Go to[Layout](/framework/docs/3.1/layout) #### Title Bar Optional. Sibling to Layout within a View. Displays icon, title, and instance label. Go to[Title Bar](/framework/docs/3.1/title_bar) #### Columns Use *inside* Layout for column-based content organization. Go to[Columns](/framework/docs/3.1/columns) #### Mashup Wraps multiple Views and arranges them within the Screen (1Lx1R, 1Tx1B, 2x2, etc.). Go to[Mashup](/framework/docs/3.1/mashup) #### Single View For a single plugin occupying the full screen: Layout PluginInstance
...
#### Mashup (Multiple Views) For multiple plugins on one screen, wrap views in a[Mashup](/framework/docs/3.1/mashup) . Each view has exactly one [Layout](/framework/docs/3.1/layout) . Plugin A Plugin B
...
...
Previous [Framework Runtime How the runtime applies layout, clamping, overflow, and presentation adjustments at render time](/framework/docs/3.1/framework_runtime) Next [Screen Device screen dimensions, orientation, and display properties](/framework/docs/3.1/screen) --- # Screen The Screen component is the outermost container that defines the device dimensions and provides global settings for your content. You don't specify the `screen`. The platform provides the correct `screen` container based on the target device. You provide the `screen` yourself. Include it with the appropriate device class (`screen--og`, `screen--v2`) and optional modifiers like `screen--portrait`, `screen--no-bleed`, or `screen--dark-mode`.
...
...
...
...
### Base Structure The Screen component serves as the root container for all content. It establishes the viewport dimensions, padding, and provides CSS variables that cascade throughout the framework. #### Default Screen The base `screen` class creates a container with default dimensions (800x480px landscape). It includes padding controlled by the `--gap` variable. Default Screen
### CSS Variables The Screen component provides CSS variables that cascade throughout the framework. These variables automatically recalculate when device variants or orientation modifiers are applied. #### Available Variables These variables are set on the `screen` element and available to all nested components. | Variable | Description | Default Value | | --- | --- | --- | | `--screen-w` | Screen width | 800px | | `--screen-h` | Screen height | 480px | | `--full-w` | Full width minus padding | `calc(--screen-w - --gap * 2)` | | `--full-h` | Full height minus padding | `calc(--screen-h - --gap * 2)` | | `--ui-scale` | UI scaling factor | 1 | | `--gap-scale` | Gap scaling factor | 1 | | `--color-depth` | Display color depth (bits) | 1 | ### Orientation Screens can be displayed in landscape (default) or portrait orientation. #### Orientation Toggle The `screen--portrait` modifier swaps the width and height dimensions. All layout calculations automatically adjust to the new dimensions.
### Device Variants The Screen component supports device-specific configurations that adjust dimensions, scaling, and color depth. These variants ensure content displays correctly across different TRMNL devices. #### Available Devices Each device variant sets specific dimensions and scaling factors. Combine with orientation and bit-depth modifiers for complete control.
### Modifiers Screen modifiers provide additional control over display properties and behavior. #### No Bleed Modifier The screen container that wraps your views has a no-bleed option that removes padding. This can be controlled through Private and Public Plugin settings, or applied directly in your code when developing locally. The `screen--no-bleed` modifier removes the default padding around the screen container, allowing content to extend fully to the edges. Screen No Bleed / Layout
#### Dark Mode The `screen--dark-mode` modifier remaps framework color tokens and utility output for dark rendering (background, text, border, and stroke utilities included).
#### Backdrop Mashups By default, mashups display with a white background and bordered views. The `screen--backdrop` modifier changes this to a patterned background (1-bit) or solid gray background (2-bit/4-bit) with plain white views. See the [Outline](/framework/docs/3.1/outline) utility for more details.
...
...
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | `--full-h` | calc(var(--screen-h) - var(--gap) * 2) | — | — | — | | `--full-w` | calc(var(--screen-w) - var(--gap) * 2) | — | — | — | | `--half_horizontal-h` | calc((var(--screen-h) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--half_horizontal-w` | calc((var(--screen-w) - var(--gap) * 2)) | — | — | — | | `--half_vertical-h` | calc((var(--screen-h) - var(--gap) * 2)) | — | — | — | | `--half_vertical-w` | calc((var(--screen-w) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--quadrant-h` | calc((var(--screen-h) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--quadrant-w` | calc((var(--screen-w) - var(--gap) * 2) / 2 - var(--gap) / 2) | — | — | — | | `--screen-h` | 480px | — | — | — | | `--screen-h-original` | 480px | — | — | — | | `--screen-w` | 800px | — | — | — | | `--screen-w-original` | 800px | — | — | — | | `--ui-scale` | 1 | — | — | — | Previous [Structure The framework's exact div hierarchy and how Screen, View, Layout, Title Bar, Columns, and Mashup work together](/framework/docs/3.1/structure) Next [View Show your plugin in different sizes with Mashup view containers](/framework/docs/3.1/view) --- # View A View holds content (e.g. a plugin instance). Single views use `view--full` inside the Screen; multiple views go inside a Mashup. The view modifier sets each view's share of space; the Mashup modifier controls the arrangement. View and Layout receive calculated dimensions from the device and orientation. You don't specify the `view`. The markup you write for any plugin layout is automatically wrapped in the appropriate `view` container by the platform. You provide the `view` yourself. Include the appropriate wrapper in your markup: `view view--full`, `view view--half_vertical`, `view view--half_horizontal`, or `view view--quadrant`.
...
...
...
...
### Base Structure The Layout element[Layout](/framework/docs/3.1/layout) is the core component of every View [View](/framework/docs/3.1/view) , providing a consistent container for your content. Views can optionally include a Title Bar [Title Bar](/framework/docs/3.1/title_bar) for additional context. There are four view types: `view--full`, `view--half_horizontal`, `view--half_vertical`, and `view--quadrant`. The default full view (`view view--full`) lives directly inside the `screen` div. Other view types must be nested inside a [Mashup](/framework/docs/3.1/mashup) component. #### With Layout and Title Bar When combined with a title bar, it provides context and navigation options. Layout TitleInstance
TRMNL Logo Title Instance
#### With only Layout For simpler interfaces, you can create a view without a title bar using just the base view classes. Layout
### Views in Mashups When multiple plugins share a single screen, each one gets its own view, and those views must be wrapped in a[Mashup](/framework/docs/3.1/mashup) container. The view modifier (`view--half_vertical`, `view--quadrant`, etc.) determines how much space each plugin gets. The mashup modifier (`mashup--1Lx1R`, `mashup--2x2`, etc.) determines how those views are arranged on screen. Previous [Screen Device screen dimensions, orientation, and display properties](/framework/docs/3.1/screen) Next [Layout Primary container for organizing plugin content](/framework/docs/3.1/layout) --- # Layout The Layout is the content container inside a View. There should be exactly one `layout` per `view`. Its height is calculated automatically based on the device type, orientation, and whether a title bar is present. It can arrange content horizontally (`layout--row`) or vertically (`layout--col`), with alignment and stretch modifiers. For organizing content inside it, use `flex`, `columns`, or `grid`. Use one `layout` per `view`. Organize content inside it with `flex`, `columns`, or `grid`. Don't nest `layout` inside `layout`. There should be exactly one `layout` per `view`.
Item 1
Item 2
Item 1
Item 2
### What Goes Inside Layout Layout is the main content wrapper inside a View. It defines the available space. Its direct children are usually Columns, Grid, or Flex. #### Three ways to lay out content #### Grid Use when you need a strict grid: define column count and spans, so items align to a consistent rhythm. Good for Swiss-style layouts where everything lines up to a fixed grid. Go to[Grid](/framework/docs/3.1/grid) #### Flex Use when you want flexible arrangements where items size by content (width/height). You can use Flex alone for simpler layouts, or nest it inside Grid for per-cell flexibility. Go to[Flex](/framework/docs/3.1/flex) #### Columns Use when you have lots of same-type data and want to display as few or as many items as there are, with the Columns system handling the layout. See the Columns page for details. Go to[Columns](/framework/docs/3.1/columns) You can use multiple of each: multiple Columns components, multiple Grids, multiple Flex containers. You can mix them. The Layout modifiers (`layout--row`, `layout--col`, alignment, stretch) control how these direct children are arranged within the Layout space. 1 2 3 4 5 6 7 8 9 10 11 12 #### Nesting These components can be nested. For example, you might put a Grid inside Layout, give that Grid a column count, and place Flex containers inside each grid cell. Inside each Flex you then place your actual content (items, text, etc.). Layout arranges the top-level Grid(s); the Grid arranges its cells; the Flex arranges items within each cell. ### Base Structure The Layout system provides two fundamental ways to organize content: horizontal and vertical arrangements. These base structures are the building blocks for more complex layouts. #### Row Layout The `layout layout--row` classes create a horizontal layout. Items are arranged horizontally from left to right, with center alignment as the default positioning. Item 1 Item 2 Item 3 LayoutHorizontal
Item 1
Item 2
Item 3
#### Column Layout The `layout layout--col` classes create a vertical layout. Items are arranged vertically from top to bottom, with center alignment as the default positioning. Item 1 Item 2 Item 3 LayoutVertical
Item 1
Item 2
Item 3
### Alignment Modifiers Once you've chosen a base layout structure, you can apply these modifier classes to control how items are aligned within their container. The system provides both directional alignment (top/bottom/left/right) and centering options. #### Horizontal Alignment Use `layout--left`, `layout--center-x`, or `layout--right` to control horizontal alignment. Left LayoutLeft Alignment
Item 1
Item 2
Item 3
#### Vertical Alignment Use `layout--top`, `layout--center-y`, or `layout--bottom` to control vertical alignment. Top LayoutTop Alignment
Item 1
Item 2
Item 3
#### Center Alignment Use `layout--center` to center items both horizontally and vertically, or use `layout--center-x` and `layout--center-y` for individual axis control. Center LayoutCenter Alignment
Item 1
Item 2
Item 3
Item 1
Item 2
Item 3
### Stretch Modifiers Stretch modifiers allow you to control how child elements fill the available space within a layout. You can apply these modifiers either to the layout container or to individual child elements. #### Container Stretch Use `layout--stretch` to make all children stretch in both directions. You can also use `layout--stretch-x` and `layout--stretch-y` for individual axis control. These modifiers work with both row and column layouts. #### Row Layout Stretch Examples of stretch behavior in row layouts. Use `layout--stretch` for both directions, `layout--stretch-x` for horizontal, or `layout--stretch-y` for vertical stretch. Item 1 Item 2 Item 3 Row LayoutFull Stretch
Item 1
Item 2
Item 3
Item 1 Item 2 Item 3 Row LayoutHorizontal Stretch
Item 1
Item 2
Item 3
Item 1 Item 2 Item 3 Row LayoutVertical Stretch
Item 1
Item 2
Item 3
#### Column Layout Stretch Examples of stretch behavior in column layouts. The same modifiers work consistently regardless of layout direction. Item 1 Item 2 Item 3 Column LayoutFull Stretch
Item 1
Item 2
Item 3
Item 1 Item 2 Item 3 Column LayoutHorizontal Stretch
Item 1
Item 2
Item 3
Item 1 Item 2 Item 3 Column LayoutVertical Stretch
Item 1
Item 2
Item 3
#### Child Element Stretch Use `stretch-x` and `stretch-y` classes on individual elements to control their stretch behavior within row or column layouts. Item 1 Item 2 (stretched) Item 3 LayoutRow + Individual Stretch
Item 1
Stretched Item
Item 3
Item 1 Item 2 (stretched) Item 3 LayoutColumn + Individual Stretch
Item 1
Stretched Item
Item 3
Previous [View Show your plugin in different sizes with Mashup view containers](/framework/docs/3.1/view) Next [Title Bar Standardized title bar with plugin information and instance details](/framework/docs/3.1/title_bar) --- # Title Bar The Title Bar component provides a consistent header for terminal-like interfaces, displaying application information such as icons, titles, and instance details. Place Title Bar as a sibling of Layout inside a View. Both `layout` and `title_bar` should be direct children of the view. Don't nest Title Bar inside Layout. `title_bar` and `layout` must be siblings, not parent and child.
...
...
...
### Base Structure The Title Bar[Title Bar](/framework/docs/3.1/title_bar) consists of three main elements: an icon [Image](/framework/docs/3.1/image) , a title [Title](/framework/docs/3.1/title) , and an optional instance label [Label](/framework/docs/3.1/label) . These elements are arranged horizontally and automatically spaced. #### Basic Title Bar The basic Title Bar includes an icon and title. Use the `title_bar` class [Title Bar](/framework/docs/3.1/title_bar) for the container. Basic Title Bar
Basic Title Bar
#### Title Bar with Instance Add an instance label using the `instance` class to display additional context. Title Bar with InstanceProduction
Title Bar with Instance Production
### Title Bar in Mashups When the Title Bar is placed inside a[Mashup](/framework/docs/3.1/mashup) , it automatically receives different styling. Inside a view with a mashup layout (`view--half_vertical`, `view--half_horizontal`, or `view--quadrant`), the title bar uses a reduced height, a smaller icon, and no top or side border radius, with rounded bottom corners only so it aligns with the view's bordered outline. #### Example The same `title_bar` markup is used; the framework applies the compact styling automatically when the title bar is inside a mashup view. Plugin A Calendar Plugin B RSS
Plugin A
Calendar
Plugin B
RSS
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--title-bar-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--title-bar-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--title-bar-font-smoothing` | none | none | auto | — | | `--title-bar-font-weight` | 400 | 400 | 700 | — | | `--title-bar-height` | 40px | 40px | — | calc(40px * var(--ui-scale)) | | `--title-bar-image-height` | 28px | 28px | — | calc(28px * var(--ui-scale)) | | `--title-bar-line-height` | 1 | 1 | calc(22px * var(--ui-scale)) | — | | `--title-bar-padding-top` | 5px | 5px | 0px | 0px | | `--title-bar-text-stroke-width` | 3.5px | 3.5px | 2px | 2px | | Small | | | | | | `--title-bar-small-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--title-bar-small-height` | 32px | 32px | — | calc(32px * var(--ui-scale)) | | `--title-bar-small-image-height` | 24px | 24px | — | calc(24px * var(--ui-scale)) | Previous [Layout Primary container for organizing plugin content](/framework/docs/3.1/layout) Next [Columns Implement zero-config column layouts for content organization](/framework/docs/3.1/columns) --- # Columns The Columns system handles lots of same-type data. You provide the items; it distributes them into columns and manages overflow, so you can display as few or as many items as there are in any given situation. For other layout needs, use Grid or Flex. Columns go inside Layout. Use them in your plugin markup to organize content into balanced columns. Same rules apply. Columns go inside Layout, which you provide as part of the full hierarchy.
...
...
...
...
...
...
### When to Use Columns Use Columns when you have a lot of same-type data to show and you want to display as few or as many items as there are in any given situation. The Columns system takes care of the layout: it distributes content into columns, adapts column count to the available space, and handles overflow when content exceeds the viewport. #### Variable Data, Automatic Layout You provide the items; Columns figures out how to fit them. It distributes content into multiple columns based on available screen real estate, adapts column count when the viewport or orientation changes, and works seamlessly with the framework's overflow and clamping systems. Set a maximum column count or let the system choose the best fit. #### Overflow Handling When content exceeds the available height, Columns doesn't break or overflow. It gracefully hides items that don't fit and, when configured, adds an "and X more" indicator so users know there's additional content. See the[Overflow](/framework/docs/3.1/overflow) page for details. #### Item Grouping and Flow Items can be grouped (for example, by date or category), and the Columns system keeps those groups together as they flow into columns. Group headers stay with their items, so you don't end up with orphaned headings or broken visual hierarchy when space is limited. #### Compared to Grid and Flex Choose Columns when you have lists or feeds of same-type items and want the system to handle distribution and overflow. If you need strict grid alignment with fixed column spans, use[Grid](/framework/docs/3.1/grid) . If you need flexible, content-sized row or column arrangements (toolbars, inline groups, etc.), use [Flex](/framework/docs/3.1/flex) . ### Basic Column Layout The basic column layout is flexible - you can add as many columns as needed depending on your content needs. Column 1 Item Item Item Item Column 2 Item Item Column 3 Item Columns
Content for column 1
Content for column 2
Content for column 3
Previous [Title Bar Standardized title bar with plugin information and instance details](/framework/docs/3.1/title_bar) Next [Mashup Assemble multiple plugin views into a single interface](/framework/docs/3.1/mashup) --- # Mashup A Mashup arranges multiple plugin views within a single screen. The mashup modifier (e.g. `mashup--1Lx1R`, `mashup--2x2`) controls how the views are positioned, while each view's own modifier determines how much space it occupies. You don't specify the Mashup. When you configure multiple plugins on a single screen, the platform provides the appropriate Mashup container automatically. You provide the Mashup yourself. Include the `mashup` container with the appropriate layout class in your markup (e.g. `mashup--1Lx1R`, `mashup--2x2`).
...
...
...
...
...
...
### Mashup Layouts Mashup modifiers control how[View](/framework/docs/3.1/view) instances are arranged within the screen, while each view's own modifier determines how much space it occupies. The following layouts are available. #### 1 Left, 1 Right In the 1Lx1R layout, the first plugin occupies the left column while the second occupies the right column. Plugin A Plugin B
Plugin A
Plugin B
#### 1 Top, 1 Bottom In the 1Tx1B layout, one plugin spans the top row while the other occupies the bottom row. Plugin A Plugin B
Plugin A
Plugin B
#### 1 Left, 2 Right In the 1Lx2R layout, one plugin occupies the left column while two plugins stack in the right column. Plugin A Plugin B Plugin C
Plugin A
Plugin B
Plugin C
#### 2 Left, 1 Right The 2Lx1R layout stacks two plugins in the left column, with a single plugin in the right column. Plugin A Plugin B Plugin C
Plugin A
Plugin B
Plugin C
#### 2 Top, 1 Bottom In the 2Tx1B layout, two plugins are presented side by side in the top row, with a single plugin in the bottom row. Plugin A Plugin B Plugin C
Plugin A
Plugin B
Plugin C
#### 1 Top, 2 Bottom The 1Tx2B layout places one plugin in the top row, with two plugins side by side in the bottom row. Plugin A Plugin B Plugin C
Plugin A
Plugin B
Plugin C
#### 2 x 2 Grid The 2x2 grid arranges four plugins in a grid pattern. Plugin A Plugin B Plugin C Plugin D
Plugin A
Plugin B
Plugin C
Plugin D
Previous [Columns Implement zero-config column layouts for content organization](/framework/docs/3.1/columns) Next [Title Style headings with consistent typography](/framework/docs/3.1/title) --- # Title The Title system provides consistent text headings with different size variants. It helps maintain visual hierarchy and readability throughout the interface. ### Size Variations The Title system offers five size variants: small, base (default), large, xlarge, and xxlarge. Small TitleBase TitleLarge TitleExtra Large TitleXXL Title TitleSize Variations Small Title Base Title Base Title Large Title Extra Large Title XXL Title Small by default, base on large screens ### Responsive Titles The Title system supports responsive variants using breakpoint prefixes. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different sizes at different screen widths. Responsive TitleSmall by default, xlarge on lg screens TitleResponsive Responsive Title Base by default, xlarge on lg screens #### Orientation and Size+Orientation Title sizes can adapt to orientation with `portrait:` and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation VariantLarge by default, small in portrait. TitleOrientation Orientation Variant Large by default, small in portrait. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--title-font-family` | "BlockKie" | "BlockKie" | "Inter Variable", Inter | — | | `--title-font-size` | 26px | 26px | calc(21px * var(--ui-scale)) | — | | `--title-font-smoothing` | none | none | auto | — | | `--title-font-weight` | 400 | 400 | 400 | — | | `--title-line-height` | 1 | 1 | 1.2 | — | | Small | | | | | | `--title-small-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--title-small-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--title-small-font-smoothing` | none | none | auto | — | | `--title-small-font-weight` | 400 | 400 | 700 | — | | `--title-small-line-height` | 1 | 1 | 1.2 | — | | Large | | | | | | `--title-large-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--title-large-font-size` | 30px | — | calc(30px * var(--ui-scale)) | — | | `--title-large-font-smoothing` | auto | — | auto | — | | `--title-large-font-weight` | 425 | — | 425 | — | | `--title-large-line-height` | 1.2 | — | 1.2 | — | | Xlarge | | | | | | `--title-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--title-xlarge-font-size` | 35px | — | calc(35px * var(--ui-scale)) | — | | `--title-xlarge-font-smoothing` | auto | — | auto | — | | `--title-xlarge-font-weight` | 400 | — | 400 | — | | `--title-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--title-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--title-xxlarge-font-size` | 40px | — | calc(40px * var(--ui-scale)) | — | | `--title-xxlarge-font-smoothing` | auto | — | auto | — | | `--title-xxlarge-font-weight` | 375 | — | 375 | — | | `--title-xxlarge-line-height` | 1.2 | — | 1.2 | — | Previous [Mashup Assemble multiple plugin views into a single interface](/framework/docs/3.1/mashup) Next [Value Display data values with consistent formatting](/framework/docs/3.1/value) --- # Value The Value system provides consistent text styling for displaying numerical and textual values, with various size options and support for tabular numbers. It ensures readability and visual hierarchy across different contexts. ### Size Variants The Value system offers twelve size variants, from XXSmall to Peta. #### XXSmall The `value--xxsmall` class creates the smallest text size. Example48,206.62 ValueXXSmall Example 48,206.62 #### XSmall The `value--xsmall` class provides a size slightly larger than XXSmall. Example48,206.62 ValueXSmall Example 48,206.62 #### Small The `value--small` class creates a smaller text size. Example48,206.62 ValueSmall Example 48,206.62 #### Base The base `value` class without size modifiers and the `value--base` class both produce the same visual result. See the [Responsive Values](#responsive-values) section for examples. Example48,206.62 ValueBase Example 48,206.62 Example 48,206.62 #### Large The `value--large` class creates larger text. Example48,206.62 ValueLarge Example 48,206.62 #### XLarge The `value--xlarge` class provides larger text. Example48,206.62 ValueXLarge Example 48,206.62 #### XXLarge The `value--xxlarge` class creates very large text. Example48,206.62 ValueXXLarge Example 48,206.62 #### XXXLarge The `value--xxxlarge` class provides very large text. Example48,206.62 ValueXXXLarge Example 48,206.62 #### Mega The `value--mega` class creates extremely large text. 42 ValueMega 42 #### Giga The `value--giga` class provides massive text. 42 ValueGiga 42 #### Tera The `value--tera` class creates colossal text. 42 ValueTera 42 #### Peta The `value--peta` class provides the largest text. 42 ValuePeta 42 ### Numerical Display The Value system includes special formatting options for numerical values. #### Tabular Numbers Add the `value--tnums` modifier to enable tabular numbers. Regular: 48,206.62Tabular: 48,206.62 ValueTabular Numbers Regular: 48,206.62 Tabular: 48,206.62 ### Responsive Values The Value system supports responsive variants using breakpoint prefixes. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different sizes at different screen widths. Responsive Value1,234.56 ValueResponsive Responsive Value 1,234.56 Small by default, base on large screens #### Orientation and Size+Orientation Value sizes can adapt to orientation with `portrait:` and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation Variant42,000.00 ValueOrientation Orientation Variant 42,000.00 ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--value-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--value-font-size` | 38px | — | calc(38px * var(--ui-scale)) | — | | `--value-font-smoothing` | auto | — | auto | — | | `--value-font-weight` | 450 | — | 450 | — | | `--value-line-height` | 42px | — | calc(42px * var(--ui-scale)) | — | | Xxsmall | | | | | | `--value-xxsmall-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--value-xxsmall-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--value-xxsmall-font-smoothing` | none | none | auto | — | | `--value-xxsmall-font-weight` | 400 | 400 | 700 | — | | `--value-xxsmall-line-height` | 16px | 16px | calc(14px * var(--ui-scale)) | — | | Xsmall | | | | | | `--value-xsmall-font-size` | 20px | — | calc(20px * var(--ui-scale)) | — | | `--value-xsmall-font-weight` | 600 | — | 600 | — | | `--value-xsmall-line-height` | 24px | — | calc(24px * var(--ui-scale)) | — | | Small | | | | | | `--value-small-font-size` | 26px | — | calc(26px * var(--ui-scale)) | — | | `--value-small-font-weight` | 500 | — | 475 | — | | `--value-small-line-height` | 29px | — | calc(29px * var(--ui-scale)) | — | | Large | | | | | | `--value-large-font-size` | 58px | — | calc(58px * var(--ui-scale)) | — | | `--value-large-font-weight` | 400 | — | 400 | — | | `--value-large-line-height` | 70px | — | calc(70px * var(--ui-scale)) | — | | Xlarge | | | | | | `--value-xlarge-font-size` | 74px | — | calc(74px * var(--ui-scale)) | — | | `--value-xlarge-font-weight` | 375 | — | 375 | — | | `--value-xlarge-line-height` | 86px | — | calc(86px * var(--ui-scale)) | — | | Xxlarge | | | | | | `--value-xxlarge-font-size` | 96px | — | calc(96px * var(--ui-scale)) | — | | `--value-xxlarge-font-weight` | 350 | — | 350 | — | | `--value-xxlarge-line-height` | 108px | — | calc(108px * var(--ui-scale)) | — | | Xxxlarge | | | | | | `--value-xxxlarge-font-size` | 128px | — | calc(128px * var(--ui-scale)) | — | | `--value-xxxlarge-font-weight` | 300 | — | 300 | — | | `--value-xxxlarge-line-height` | 128px | — | calc(128px * var(--ui-scale)) | — | | Mega | | | | | | `--value-mega-font-size` | 170px | — | calc(170px * var(--ui-scale)) | — | | `--value-mega-font-weight` | 275 | — | 275 | — | | `--value-mega-line-height` | 180px | — | calc(180px * var(--ui-scale)) | — | | Giga | | | | | | `--value-giga-font-size` | 220px | — | calc(220px * var(--ui-scale)) | — | | `--value-giga-font-weight` | 250 | — | 250 | — | | `--value-giga-line-height` | 230px | — | calc(230px * var(--ui-scale)) | — | | Tera | | | | | | `--value-tera-font-size` | 290px | — | calc(290px * var(--ui-scale)) | — | | `--value-tera-font-weight` | 225 | — | 225 | — | | `--value-tera-line-height` | 300px | — | calc(300px * var(--ui-scale)) | — | | Peta | | | | | | `--value-peta-font-size` | 380px | — | calc(380px * var(--ui-scale)) | — | | `--value-peta-font-weight` | 200 | — | 200 | — | | `--value-peta-line-height` | 390px | — | calc(390px * var(--ui-scale)) | — | Previous [Title Style headings with consistent typography](/framework/docs/3.1/title) Next [Label Create clear labels for unified content identification](/framework/docs/3.1/label) --- # Label The Label system provides various styles for displaying text labels, with options for different visual treatments and sizes. The filled variant uses black (darkest) background; label--primary, label--success, etc. use semantic colors. Labels can be used to highlight text, show status, or create visual hierarchy in your interface. ### Size and Style Variants Labels come in several style variants to suit different use cases. Each variant provides a distinct visual style that can be combined with any size modifier. Small Base Large XLarge XXLarge Default Label Label Label Label Label Outline Label Label Label Label Label Underline Label Label Label Label Label Gray Label Label Label Label Label Filled Label Label Label Label Label LabelStyle Variants Default Label Outline Label Underline Label Gray Label Filled Primary Success Error Inverted (alias) Large Outline Label XLarge Filled Label #### Semantic variants Use `label--primary`, `label--success`, `label--error`, `label--warning` for intent-based colors. `label--filled` uses black (darkest). Success and warning use black text; warning uses yellow background. See [Colors](/framework/docs/3.1/colors) for the semantic mapping. FilledPrimarySuccessErrorWarning LabelSemantic Colors ### Text Overflow Behavior Labels can handle longer text content through natural wrapping or text clamping. Understanding how labels behave with overflow content helps ensure your interface remains readable and visually balanced. #### Multi-line Wrapping By default, labels will wrap to multiple lines when content exceeds the available width, maintaining readability for longer text. Small Base Large Default This longer label will wrap to multiple lines when it exceeds the width This longer label will wrap to multiple lines when it exceeds the width This longer label will wrap to multiple lines when it exceeds the width Underline This longer label will wrap to multiple lines when it exceeds the width This longer label will wrap to multiple lines when it exceeds the width This longer label will wrap to multiple lines when it exceeds the width Filled This longer label will wrap to multiple lines when it exceeds the width This longer label will wrap to multiple lines when it exceeds the width This longer label will wrap to multiple lines when it exceeds the width LabelMulti-line This longer label will wrap to multiple lines when it exceeds the width #### Text Clamping Use the framework's `data-clamp` attribute to limit labels to a specific number of lines with ellipsis overflow. Small Base Large 1-line This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow 2-line This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow Underline 1 This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow Underline 2 This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long label text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow LabelClamped This text will be clamped to one line This text will be clamped to exactly two lines with ellipsis ### Responsive Features Label components support all three responsive systems: size-based, orientation-based, and bit-depth variants. This enables precise control over label appearance across different device configurations. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different sizes and styles at different screen widths. Responsive LabelSmall by default, xlarge on lg screens LabelResponsive Responsive Label Small by default, xlarge on lg screens Small by default, base on medium+ screens Progressive Label Sizing #### Orientation and Size+Orientation Label sizes can adapt to orientation with `portrait:` and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation VariantLarge by default, small in portrait. LabelOrientation Orientation Variant Large by default, small in portrait. #### Bit-Depth Responsive Use bit-depth prefixes like `1bit:`, `2bit:`, and `4bit:` to optimize label appearance for different display color capabilities. Display OptimizedFilled (1bit) → Outline (2bit) → Underline (4bit) Selective StylingOutline (1bit) → Gray (4bit) LabelBit-Depth Responsive Display Optimized Selective Styling #### Combined Responsive Features Combine multiple responsive systems for highly targeted label styling. Use size, orientation, and bit-depth modifiers together following the pattern: `size:orientation:bit-depth:utility`. Advanced TargetingComplex responsive combinations Multi-ConditionMultiple responsive conditions LabelCombined Responsive Advanced Targeting Multi-Condition ### Backward Compatibility The gray-out label variant has been renamed from `label--gray-out` to `label--gray`. The legacy class name still works and maps to the same bit-depth responsive styling. Prefer the new name going forward. The inverted label has been renamed to `label--filled` (black background). Use `label--primary`, `label--success`, etc. for semantic colors. `label--inverted` remains as an alias for `label--filled`. Gray label (deprecated) Inverted (alias) Gray label (preferred) Filled label (preferred) ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--label-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--label-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--label-font-smoothing` | none | none | auto | — | | `--label-font-weight` | 400 | 400 | 500 | — | | `--label-line-height` | 1.25 | 1.25 | 1.25 | — | | Small | | | | | | `--label-small-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--label-small-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--label-small-font-smoothing` | none | none | auto | — | | `--label-small-font-weight` | 400 | 400 | 500 | — | | `--label-small-line-height` | 1 | 1 | 1 | — | | Large | | | | | | `--label-large-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--label-large-font-size` | 21px | — | calc(21px * var(--ui-scale)) | — | | `--label-large-font-smoothing` | auto | — | auto | — | | `--label-large-font-weight` | 500 | — | 500 | — | | `--label-large-line-height` | 1.2 | — | 1.2 | — | | Xlarge | | | | | | `--label-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--label-xlarge-font-size` | 26px | — | calc(26px * var(--ui-scale)) | — | | `--label-xlarge-font-smoothing` | auto | — | auto | — | | `--label-xlarge-font-weight` | 475 | — | 475 | — | | `--label-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--label-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--label-xxlarge-font-size` | 30px | — | calc(30px * var(--ui-scale)) | — | | `--label-xxlarge-font-smoothing` | auto | — | auto | — | | `--label-xxlarge-font-weight` | 450 | — | 450 | — | | `--label-xxlarge-line-height` | 1.2 | — | 1.2 | — | Previous [Value Display data values with consistent formatting](/framework/docs/3.1/value) Next [Description Format descriptive text with standardized styles](/framework/docs/3.1/description) --- # Description The Description component provides a standardized way to display descriptive text content with consistent styling. ### Size Variants Descriptions come in four size variants to suit different use cases. Each variant provides a distinct visual style that can be used for various content hierarchies. Base Large XLarge XXLarge Description Description Description Description DescriptionSize Variants Base Description Large Description XLarge Description XXLarge Description Large by default, base on medium+ screens ### Text Overflow Behavior Descriptions can handle longer text content through natural wrapping or text clamping. Understanding how descriptions behave with overflow content helps ensure your interface remains readable and visually balanced. #### Multi-line Wrapping By default, descriptions will wrap to multiple lines when content exceeds the available width, maintaining readability for longer text. Base Large XLarge XXLarge This longer description will wrap to multiple lines when it exceeds the available width This longer description will wrap to multiple lines when it exceeds the available width This longer description will wrap to multiple lines when it exceeds the available width This longer description will wrap to multiple lines when it exceeds the available width DescriptionMulti-line This longer description will wrap to multiple lines when it exceeds the width #### Text Clamping Use the framework's `data-clamp` attribute to limit descriptions to a specific number of lines with ellipsis overflow. Base Large XLarge XXLarge This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow This is a very long description text that would normally wrap to many lines but demonstrates how clamping behavior works with different variants and line limits to show ellipsis overflow DescriptionClamped This text will be clamped to one line This text will be clamped to exactly two lines with ellipsis This larger text will be clamped to three lines ### Responsive Features Description components support all three responsive systems: size-based, orientation-based, and bit-depth variants. This enables precise control over description appearance across different device configurations. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different sizes at different screen widths. Responsive DescriptionBase by default, xlarge on lg screens DescriptionResponsive Responsive Description Base by default, xlarge on lg screens Large by default, base on medium+ screens Progressive Description Sizing #### Orientation and Size+Orientation Description sizes can adapt to orientation with `portrait:` and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation VariantLarge by default, base in portrait. DescriptionOrientation Orientation Variant Large by default, base in portrait. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--description-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--description-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--description-font-smoothing` | none | none | auto | — | | `--description-font-weight` | 400 | 400 | 400 | — | | `--description-line-height` | 1 | 1 | 1.2 | — | | Large | | | | | | `--description-large-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--description-large-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--description-large-font-smoothing` | none | none | auto | — | | `--description-large-font-weight` | 400 | 400 | 700 | — | | `--description-large-line-height` | 1.25 | 1.25 | 1.2 | — | | Xlarge | | | | | | `--description-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--description-xlarge-font-size` | 21px | — | calc(21px * var(--ui-scale)) | — | | `--description-xlarge-font-smoothing` | auto | — | auto | — | | `--description-xlarge-font-weight` | 500 | — | 500 | — | | `--description-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--description-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--description-xxlarge-font-size` | 24px | — | calc(24px * var(--ui-scale)) | — | | `--description-xxlarge-font-smoothing` | auto | — | auto | — | | `--description-xxlarge-font-weight` | 475 | — | 475 | — | | `--description-xxlarge-line-height` | 1.2 | — | 1.2 | — | Previous [Label Create clear labels for unified content identification](/framework/docs/3.1/label) Next [Divider Create horizontal or vertical dividers between elements](/framework/docs/3.1/divider) --- # Divider The Divider element provides a simple, standalone way to create visual separations in your layouts. Dividers automatically adapt to their background color for optimal visibility across four background types: white, light, dark, and black. ### Usage Use `divider` or `divider--h` for horizontal dividers, and `divider--v` for vertical dividers. Dividers automatically detect their background and adjust their appearance for optimal contrast. #### Automatic Background Detection By default, dividers automatically detect whether they're on a white, light, dark, or black background and adjust their appearance accordingly. The system categorizes backgrounds into four types for optimal contrast: - **White:** Very light backgrounds (gray-70 to gray-75 and pure white) - **Light:** Light gray backgrounds (gray-50 to gray-65) - **Dark:** Dark gray backgrounds (gray-30 to gray-45) - **Black:** Very dark backgrounds (gray-10 to gray-25 and pure black) White Background Divider uses darkest style (level 7) Light Background (gray-70) Divider uses dark style (level 6) Dark Background (gray-30) Divider uses light style (level 3) Black Background Divider uses lightest style (level 1) Auto Background Detection
#### Manual Background Control You can manually specify the background type using `divider--on-white`, `divider--on-light`, `divider--on-dark`, or `divider--on-black` classes when automatic detection isn't suitable. All variants on white on-white (optimal) on-light on-dark on-black (poor contrast) All variants on black on-white (poor contrast) on-light on-dark on-black (optimal) Manual Background Control
#### Vertical Dividers Vertical dividers work the same way as horizontal dividers, with automatic background detection for all four background types. Left SideWhite background Right SideAuto-detected Left SideBlack background Right SideAuto-detected Vertical Dividers
#### Common Usage Patterns $1,234Revenue 42Orders $29.38AOV Section Separation
Previous [Description Format descriptive text with standardized styles](/framework/docs/3.1/description) Next [Rich Text Display formatted paragraphs with alignment and size variants](/framework/docs/3.1/rich_text) --- # Rich Text The Rich Text component provides a flexible container for displaying text content with consistent styling and layout options. It's commonly used for paragraphs, articles, and other formatted text content that needs to maintain readability and visual hierarchy. ### Understanding Richtext Components The richtext system consists of two key parts working together: the parent `.richtext` container and its natural child `.content`[Content Limiter](/framework/docs/3.1/content_limiter) component. The parent `.richtext` container is designed for flexibility and can hold any content. It controls the overall placement and spacing of the component within your layout. The `.content` component is where your actual text content lives. It provides additional styling and formatting options specific to text. Both components have separate alignment modifiers that serve different purposes. The table below summarizes each modifier: | Class | Modifiers | Applies To | Controls | Example | | --- | --- | --- | --- | --- | | `richtext` | `left`, `center`, `right` | Container (`.richtext`) | Aligns the richtext container within its parent | `richtext--center` | | `content` | `left`, `center`, `right` | Child content (`.content`) | Aligns the content block within the richtext container | `content--right` | | `text` | `left`, `center`, `right` | Text elements inside `.content` | Aligns inline text within the content block | `text--center` | This multi-level alignment system provides maximum flexibility for positioning both the component and its content independently. ### Rich Text Alignment The Rich Text component can be aligned in three different ways: left, center, or right. Each alignment option provides different text positioning to suit various design needs. #### Left Aligned Left alignment is the default and most readable format for longer text content, ideal for paragraphs and articles. This is an example of left-aligned rich text content. This alignment is generally best for readability with longer paragraphs of text. Multiple paragraphs will maintain the same alignment, making it easy to read through longer content while maintaining visual consistency. Rich TextLeft Aligned

This is an example of left-aligned rich text content.

Multiple paragraphs will maintain the same alignment.

#### Center Aligned Center alignment is ideal for headings, quotes, or shorter text that needs to be highlighted or visually balanced within the layout. This is an example of center-aligned rich text content. Centered text works well for quotes, headings, or highlighted information that needs visual emphasis. Rich TextCenter Aligned

This is an example of center-aligned rich text content.

Centered text works well for quotes or highlighted information.

#### Right Aligned Right alignment is less common but can be useful for specific design scenarios or to create visual tension in layouts. This is an example of right-aligned rich text content. Right alignment can be used for captions, sidebars, or to create visual interest through contrasting alignments. Rich TextRight Aligned

This is an example of right-aligned rich text content.

Right alignment can be used for captions or sidebars.

### Content Size Variants The Rich Text component offers six size variants: `small`, `base`, `large`, `xlarge`, `xxlarge`, and `xxxlarge`. The `content` class without size modifiers and the `content--base` class both produce the same visual result. Use `content--base` when you need to explicitly set the base size in responsive contexts. All size variants support responsive prefixes like `sm:`, `md:`, `lg:`, and `portrait:`. This is xxxlarge size rich text content. This is xxlarge size rich text content. This is xlarge size rich text content. This is large size rich text content. This is base size rich text content. This is small size rich text content. Rich TextSize Variants

This is xxxlarge size rich text content.

This is xxlarge size rich text content.

This is xlarge size rich text content.

This is large size rich text content.

This is base size rich text content.

This is small size rich text content.

Base by default, xxxlarge on large screens

### Controlling Width By default, the Rich Text content takes up as much space as it needs and is centered in the layout, expanding up to a maximum width. However, you can precisely control the width of content using Size utility classes[Size](/framework/docs/3.1/size) . This Rich Text content has a fixed width of 240 pixels using utility classes. Notice how the text is constrained to this specific width regardless of the container size. Rich TextFixed Width: 240px

This Rich Text content has a fixed width of 240 pixels using utility classes.

Notice how the text is constrained to this specific width regardless of the container size.

You can use any of the Size system's fixed sizes (`w--32`, `w--64`, etc.), arbitrary sizes (`w--[250px]`), or responsive sizes (`w--full`, `w--auto`). This flexibility lets you create perfectly sized text blocks for any layout need. ### Responsive Features The `content` component supports all three responsive systems: size-based, orientation-based, and bit-depth variants. This enables precise control over content text size across different device configurations. #### Breakpoint Prefixes Use breakpoint prefixes like `sm:`, `md:`, `lg:` to apply different content sizes at different screen widths. Responsive content Base by default, xxlarge on lg screens Rich TextResponsive

Responsive content

Small by default, base on medium+ screens

Progressive content sizing

#### Orientation and Size+Orientation Content sizes can adapt to orientation with `portrait:` and `landscape:`, and can be combined with size breakpoints (e.g., `md:portrait:`). Orientation variant content Large by default, small in portrait. Rich TextOrientation

Orientation variant content

Large by default, small in portrait.
#### Bit-Depth Responsive Use bit-depth prefixes like `1bit:`, `2bit:`, and `4bit:` to optimize content text size for different display color capabilities. Display optimized content Large (1bit) → XLarge (2bit) → XXLarge (4bit) Selective sizing Base (1bit) → XLarge (4bit) Rich TextBit-Depth Responsive

Display optimized content

Selective sizing

#### Combined Responsive Features Combine multiple responsive systems for highly targeted content sizing. Use size, orientation, and bit-depth modifiers together following the pattern: `size:orientation:bit-depth:content--size`. Advanced targeting Complex responsive combinations Multi-condition content Multiple responsive conditions Rich TextCombined Responsive

Advanced targeting

Multi-condition content

### Integration with Content Limiter The Rich Text component works seamlessly with the Content Limiter utility[Content Limiter](/framework/docs/3.1/content_limiter) to handle overflowing text. When combined, it automatically adjusts text size to fit the available space, which is particularly useful in constrained layouts. Simply add the `data-content-limiter="true"` attribute to your richtext content element. You can also specify a custom maximum height using the `data-content-max-height` attribute (e.g., `data-content-max-height="140"`). When `data-content-limiter="true"` is present, the limiter auto-measures the available height in the nearest container and adjusts text to fit. You can override the auto height by specifying `data-content-max-height` with a pixel value. Automatically resize text when content exceeds height limits [View Content Limiter Documentation](/framework/docs/3.1/content_limiter) The Rich Text component with Content Limiter will automatically adjust text size when content exceeds the auto-measured available height. This is particularly useful for views with limited vertical space such as quadrants or half-horizontal layouts. Notice how this text is rendered smaller to fit within the quadrant view. Without Content Limiter, this text would overflow the container. Rich TextWith Content Limiter

The Rich Text component with Content Limiter will automatically adjust text size when content exceeds the height threshold. This is particularly useful for views with limited vertical space.

### Integration with Pixel Perfect For optimal text rendering on ePaper displays, the Rich Text component can be enhanced with the Pixel Perfect utility[Pixel Perfect](/framework/docs/3.1/pixel_perfect) . This ensures text is rendered with crisp edges by aligning precisely to the pixel grid, preventing blurry or inconsistent text weight. Simply add the `data-pixel-perfect="true"` attribute to your richtext content element. Ensure crisp text rendering on 1-bit displays [View Pixel Perfect Documentation](/framework/docs/3.1/pixel_perfect) This text is rendered with pixel perfect alignment, ensuring that each character aligns precisely with the pixel grid. Notice how the text appears crisp and clear with consistent weight. Pixel Perfect is especially important for ePaper displays that use a 1-bit color space (just black and white), where anti-aliased gray pixels are forced to become either fully black or fully white. Rich TextWith Pixel Perfect

This text is rendered with pixel perfect alignment, ensuring that each character aligns precisely with the pixel grid. Notice how the text appears crisp and clear with consistent weight.

Pixel Perfect is especially important for ePaper displays that use a 1-bit color space (just black and white), where anti-aliased gray pixels are forced to become either fully black or fully white.

### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--richtext-content-max-width` | 640px | — | — | — | | `--richtext-font-family` | "NicoClean" | "NicoClean" | "Inter Variable", Inter | — | | `--richtext-font-size` | 16px | 16px | calc(16px * var(--ui-scale)) | — | | `--richtext-font-smoothing` | none | none | auto | — | | `--richtext-font-weight` | 400 | 400 | 500 | — | | `--richtext-line-height` | 22px | 22px | calc(22px * var(--ui-scale)) | — | | Small | | | | | | `--richtext-small-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--richtext-small-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--richtext-small-font-smoothing` | none | none | auto | — | | `--richtext-small-font-weight` | 400 | 400 | 500 | — | | `--richtext-small-line-height` | 16px | 16px | calc(18px * var(--ui-scale)) | — | | Large | | | | | | `--richtext-large-font-family` | "BlockKie" | "BlockKie" | "Inter Variable", Inter | — | | `--richtext-large-font-size` | 26px | 26px | calc(21px * var(--ui-scale)) | — | | `--richtext-large-font-smoothing` | none | none | auto | — | | `--richtext-large-font-weight` | 400 | 400 | 500 | — | | `--richtext-large-line-height` | 1 | 1 | 1.2 | — | | Xlarge | | | | | | `--richtext-xlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--richtext-xlarge-font-size` | 30px | — | calc(30px * var(--ui-scale)) | — | | `--richtext-xlarge-font-smoothing` | auto | — | auto | — | | `--richtext-xlarge-font-weight` | 425 | — | 425 | — | | `--richtext-xlarge-line-height` | 1.2 | — | 1.2 | — | | Xxlarge | | | | | | `--richtext-xxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--richtext-xxlarge-font-size` | 35px | — | calc(35px * var(--ui-scale)) | — | | `--richtext-xxlarge-font-smoothing` | auto | — | auto | — | | `--richtext-xxlarge-font-weight` | 400 | — | 400 | — | | `--richtext-xxlarge-line-height` | 1.2 | — | 1.2 | — | | Xxxlarge | | | | | | `--richtext-xxxlarge-font-family` | "Inter Variable", Inter | — | "Inter Variable", Inter | — | | `--richtext-xxxlarge-font-size` | 40px | — | calc(40px * var(--ui-scale)) | — | | `--richtext-xxxlarge-font-smoothing` | auto | — | auto | — | | `--richtext-xxxlarge-font-weight` | 375 | — | 375 | — | | `--richtext-xxxlarge-line-height` | 1.2 | — | 1.2 | — | Previous [Divider Create horizontal or vertical dividers between elements](/framework/docs/3.1/divider) Next [Item Build standardized list items and content blocks](/framework/docs/3.1/item) --- # Item The Item component provides a flexible container for displaying content with optional metadata and indexing. It's commonly used for lists, schedules, and other content that needs consistent formatting. ### Item Variants Items can be displayed in four variants: with meta and index, with meta only, with meta emphasis levels, or in a simple format. Each variant provides different levels of visual hierarchy and information density. #### With Meta This variant includes a meta section without an index, providing space for optional metadata while maintaining a clean appearance. Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant Code ReviewReview pull requests for Project Beta 3:30 PM - 4:30 PMHigh Priority Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant ItemWith Meta
Team Meeting Weekly team sync-up
9:00 AM - 10:00 AM Confirmed
#### With Meta Emphasis Apply `item--emphasis-1`, `item--emphasis-2` or `item--emphasis-3` to progressively darken the meta bar and draw attention. Level 1 is the default styling. Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant Code ReviewReview pull requests for Project Beta 3:30 PM - 4:30 PMHigh Priority Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant ItemEmphasis Levels
Team Meeting Weekly team sync-up
9:00 AM - 10:00 AM Confirmed
Client Presentation Quarterly review with XYZ Corp
2:00 PM - 3:30 PM Tentative
Project Deadline Submit final deliverables for Project Alpha
11:59 PM Important
#### With Meta and Index The most detailed variant includes both a meta section and an index number, useful for ordered lists or when additional context is needed. 1 Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed 2 Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative 3 Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant 4 Code ReviewReview pull requests for Project Beta 3:30 PM - 4:30 PMHigh Priority 1 Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed 2 Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative 3 Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant ItemWith Meta and Index
1
Team Meeting Weekly team sync-up
9:00 AM - 10:00 AM Confirmed
#### Simple The simplest variant focuses purely on content, ideal for basic lists or when metadata isn't needed. Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant Code ReviewReview pull requests for Project Beta 3:30 PM - 4:30 PMHigh Priority Team MeetingWeekly team sync-up 9:00 AM - 10:00 AMConfirmed Client PresentationQuarterly review with XYZ Corp 2:00 PM - 3:30 PMTentative Project DeadlineSubmit final deliverables for Project Alpha 11:59 PMImportant ItemSimple
Team Meeting Weekly team sync-up
9:00 AM - 10:00 AM Confirmed
#### With Icon Add an `icon` div between meta and content to display an icon alongside the item. 72°Temperature 12 mphWind Speed 6UV Index SunnyToday Partly CloudyTomorrow RainyWednesday ItemWith Icon
72° Temperature
### List component (deprecated) The `.list` class is deprecated. Prefer a column component, flex column, grid column, or a layout wrapper with a [Gap](/framework/docs/3.1/gap) utility instead. The [Overflow](/framework/docs/3.1/overflow) engine still supports legacy `.list` for backward compatibility. ### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--item-index-font-family` | "NicoPups" | "NicoPups" | "Inter Variable", Inter | — | | `--item-index-font-size` | 16px | 16px | calc(13px * var(--ui-scale)) | — | | `--item-index-font-smoothing` | none | none | auto | — | | `--item-index-font-weight` | 400 | 400 | 600 | — | | `--item-index-line-height` | 1 | 1 | 1 | — | | `--item-meta-width` | 10px | 10px | — | calc(10px * var(--ui-scale)) | Previous [Rich Text Display formatted paragraphs with alignment and size variants](/framework/docs/3.1/rich_text) Next [Table Create data tables optimized for 1-bit rendering](/framework/docs/3.1/table) --- # Table The Table system provides structured data presentation with consistent styling and various size options. It's designed to display information in a clear, scannable format while maintaining visual hierarchy. ### Base Structure Tables are built using standard HTML table elements with additional classes for styling. The base structure includes headers and data cells with consistent spacing and typography. #### Default Table The `table` class provides the standard table styling with comfortable spacing and clear visual hierarchy. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableRegular
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
#### Indexed Table Add an opt‑in index column by placing a meta block in the cells you want indexed: `td .meta > span.index`. Add `table--indexed` to dock the meta block to the left and add padding for that column. | | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | --- | | 1 | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | 2 | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | 3 | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | 4 | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | 5 | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | 6 | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | 7 | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | 8 | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | 9 | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | 10 | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | 11 | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | 12 | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | 13 | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | 14 | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | 15 | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | 16 | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | 17 | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | 18 | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | 19 | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | 20 | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | 21 | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | 22 | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | 23 | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | 24 | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | 25 | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | 26 | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | 27 | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | 28 | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | 29 | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | 30 | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableIndexed
Employee Role Pranks Sales Score Fun Fact
1 Pam Beesly Receptionist 3 $0 0.00 Loves drawing
### Size Variants Tables support five sizes: Base, Large, XLarge, Small, and XSmall. Use modifier classes to change row heights. #### Base The `table` class without size modifiers and the `table--base` class both produce the same visual result, providing the standard table styling with comfortable spacing and clear visual hierarchy. Use `table--base` when you need to explicitly set the base size in responsive contexts, such as `table--small lg:table--base`. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableBase
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
#### Large Use `table--large` to increase row heights for more spacious tables. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableLarge
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
#### XLarge Use `table--xlarge` for larger screens. Pairs well with larger font-sizes. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableXLarge
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
#### Small Use `table--small` for a compact table with reduced row heights. The older `table--condensed` class remains supported as a backward-compatible alias. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableSmall
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
#### XSmall Use `table--xsmall` for the most compact row heights. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableXSmall
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
### Overflow Engine Demonstrates the Overflow behavior[Table Overflow](/framework/docs/3.1/table_overflow) and trailing “and X more” row when content exceeds the height budget. | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableOverflow
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
### Clamp Engine Apply `data-clamp`[Clamp](/framework/docs/3.1/clamp) to each cell’s content to ensure consistent single-line truncation with ellipsis. This works with the Table Overflow behavior [Table Overflow](/framework/docs/3.1/table_overflow) . | Employee | Role | Pranks | Sales | Score | Fun Fact | | --- | --- | --- | --- | --- | --- | | Dwight Schrute | Assistant to the Regional Manager | 24 | 44 | 12.91 | Owns a beet farm | | Jim Halpert | Sales Rep | 42 | 21 | 8.69 | Dwight hates him | | Stanley Hudson | Sales Rep | 0 | 28 | 5.83 | Only smiles on Pretzel Day | | Phyllis Vance | Sales Rep | 0 | 18 | 3.79 | Married to Bob Vance | | Andy Bernard | Sales Rep | 2 | 14 | 3.18 | Cornell graduate | | Creed Bratton | Quality Assurance | ??? | ??? | ??? | ??? | | Karen Filippelli | Sales / Utica Manager | 0 | 12 | 2.57 | Jim’s ex from Stamford | | Michael Scott | Regional Manager | 15 | 0 | 1.65 | World’s Best Boss mug | | Todd Packer | Traveling Salesman | 0 | 6 | 1.34 | Terrible human being | | Ryan Howard | Temp / VP / Janitor | 1 | 2 | 0.63 | Pitched the Sabre Pyramid | | Pam Beesly | Receptionist / Office Admin | 3 | 0 | 0.43 | Art school dreamer | | Meredith Palmer | Supplier Relations | 0 | 1 | 0.32 | Exchanged paper for steak | | Holly Flax | HR (Nashua) | 2 | 0 | 0.32 | Michael’s soulmate | | Darryl Philbin | Warehouse Foreman | 1 | 0 | 0.22 | Started a band | | Kevin Malone | Accountant | 1 | 0 | 0.22 | Spilled the chili | | Erin Hannon | Receptionist | 1 | 0 | 0.22 | Dates Gabe, then Andy | | Kelly Kapoor | Customer Service | 0 | 0 | 0.00 | Obsessed with Ryan | | Angela Martin | Accountant | 0 | 0 | 0.00 | Owns 12 cats | | Oscar Martinez | Accountant | 0 | 0 | 0.00 | “Actually...” guy | | Roy Anderson | Warehouse | 0 | 0 | 0.00 | Pam’s ex-fiancé | | Toby Flenderson | HR | 0 | 0 | 0.00 | Michael hates him | | Jan Levinson | Corporate | 0 | 0 | 0.00 | Serenity by Jan | | David Wallace | CFO | 0 | 0 | 0.00 | Invented “Suck It” | | Robert California | CEO | 0 | 0 | 0.00 | The Lizard King | | Nellie Bertram | Special Projects Manager | 0 | 0 | 0.00 | Took Andy’s job | | Deangelo Vickers | Regional Manager | 0 | 0 | 0.00 | Juggled invisible balls | | Charles Miner | Corporate VP | 0 | 0 | 0.00 | Hated Jim’s pranks | | Gabe Lewis | Sabre Liaison | 0 | 0 | 0.00 | Tall, awkward, hates horror movies | | Clark Green | Sales | 0 | 0 | 0.00 | Mini Dwight | | Pete Miller | Sales | 0 | 0 | 0.00 | Nickname: Plop | TableClamp: 1 line
Employee Role Pranks Sales Score Fun Fact
Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--table-tbody-height` | 46px | — | — | — | | `--table-thead-height` | 36px | — | — | — | | Xsmall | | | | | | `--table-xsmall-tbody-height` | 22px | — | — | — | | `--table-xsmall-thead-height` | 18px | — | — | — | | Small | | | | | | `--table-small-tbody-height` | 31px | — | — | — | | `--table-small-thead-height` | 24px | — | — | — | | Large | | | | | | `--table-large-tbody-height` | 56px | — | — | — | | `--table-large-thead-height` | 44px | — | — | — | | Xlarge | | | | | | `--table-xlarge-tbody-height` | 72px | — | — | — | | `--table-xlarge-thead-height` | 56px | — | — | — | Previous [Item Build standardized list items and content blocks](/framework/docs/3.1/item) Next [Chart Visualize data optimized for 1-bit rendering](/framework/docs/3.1/chart) --- # Chart With careful, minimal styling choices, TRMNL can display a variety of numerical or time centric content as charts and graphs. ### Usage Any CDN-enabled JavaScript library may be used to develop charting interfaces, however the examples below leverage [Highcharts](https://highcharts.com) and [Chartkick](https://chartkick.com). If you set the `height: null` within your highchart's settings, the chart will automatically expand to fill the available space. Take care to disable animation effects, otherwise your chart may be only partially captured by TRMNL's screenshot rendering service. #### Line Chart Line charts effectively display trends over time. This example shows a simple line chart with customized styling to match the TRMNL aesthetic. 25,388Pageviews 4,771Visitors 2.23Mins on Page ![TRMNL Logo](https://trmnl.com/images/plugins/trmnl--render.svg)ChartsLine Chart
25,388 Pageviews
4,771 Visitors
2.23 Mins on Page
Simple Analytics Logo Simple Analytics trmnl.com
#### Multi-Series Line Chart For comparing data across multiple time periods or categories, multi-series line charts are ideal. This example demonstrates a comparison between current and previous period data with distinct styling for each series. $85,240Total Sales 32Pending Orders Jul 01 - Jul 15 Current $128AOV 665Fulfilled Orders Jun 15 - Jun 30 Previous ![TRMNL Logo](https://trmnl.com/images/plugins/trmnl--render.svg)ChartsMulti-Series Line Chart
$85,240 Total Sales
32 Pending Orders
Jul 01 - Jul 15
Current
$128 AOV
665 Fulfilled Orders
Jun 15 - Jun 30
Previous
Charts Multi-Series Line Chart
#### Bar Chart Bar charts are ideal for comparing discrete categories side by side. This example displays four different metrics across multiple time periods. $31,883Revenue $22,910Expenses $8,990Marketing $14,930Operations ![TRMNL Logo](https://trmnl.com/images/plugins/trmnl--render.svg)ChartsBar Chart
$31,883 Revenue
$22,910 Expenses
$8,990 Marketing
$14,930 Operations
#### Gauge Chart Gauge charts can effectively display single metrics or scores. This example shows multiple gauges in a row with a main summary gauge, perfect for displaying daily and weekly metrics like sleep quality scores. Monday Tuesday Wednesday Thursday Friday Saturday Sunday 18%REM Sleep 23%Deep Sleep 12mTime to Sleep 7h 32minSleep Duration 8Toss & Turns 0.5%Snoring ![TRMNL Logo](https://trmnl.com/images/plugins/trmnl--render.svg)ChartsGauge Chart
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
18% REM Sleep
23% Deep Sleep
12m Time to Sleep
7h 32min Sleep Duration
8 Toss & Turns
0.5% Snoring
Previous [Table Create data tables optimized for 1-bit rendering](/framework/docs/3.1/table) Next [Progress Display progress bars in different styles](/framework/docs/3.1/progress) --- # Progress The Progress component provides visual indicators for completion status and step-based processes. Optimized for ePaper displays with bitmap patterns for 1-bit displays and solid colors for 4-bit+ displays. ### Progress Bar Progress bars display continuous progress with a filled track. They support multiple sizes and emphasis levels for different visual weights and contexts. #### Sizes Progress bars come in five sizes: xsmall, small, base (default), regular (default, no modifier), and large. Use the `fill` element with inline width styling to set the progress percentage. The `progress-bar--base` modifier explicitly sets the default/regular size and is useful for responsive layouts. Xsmall Progress25% Small Progress25% Base Progress50% Regular Progress50% Large Progress75% ProgressBar Sizes
Xsmall Progress 25%
Small Progress 25%
Base Progress 50%
Regular Progress 50%
Large Progress 75%
#### Emphasis Progress bars support three emphasis levels: default, emphasis-2, and emphasis-3 for different visual weights. Default Emphasis60% Emphasis 260% Emphasis 360% ProgressBar Emphasis
Default Emphasis 60%
Emphasis 2 60%
Emphasis 3 60%
### Progress Dots Progress dots display discrete steps or stages in a process. They come in five sizes and show different states: filled (completed), current (active), and empty (upcoming). #### Sizes Progress dots come in five sizes: xsmall, small, base (default), regular (default, no modifier), and large. Each size maintains the same dot states and functionality. The `progress-dots--base` modifier explicitly sets the default/regular size and is useful for responsive layouts. Xsmall Progress Small Progress Base Progress Regular Progress Large Progress ProgressDots Sizes
### Related Tokens These tokens are automatically mapped to this page by token prefix. | Token | 1-bit | 2-bit | Density 2x | 4/8/16-bit | | --- | --- | --- | --- | --- | | Base | | | | | | `--progress-bar-height` | 24px | — | — | — | | `--progress-bar-height-large` | 32px | — | — | — | | `--progress-bar-height-small` | 12px | — | — | — | | `--progress-bar-height-xsmall` | 6px | — | — | — | | `--progress-dot-size` | 16px | — | — | — | | `--progress-dot-size-large` | 20px | — | — | — | | `--progress-dot-size-small` | 12px | — | — | — | | `--progress-dot-size-xsmall` | 8px | — | — | — | Previous [Chart Visualize data optimized for 1-bit rendering](/framework/docs/3.1/chart) --- # Alias Plugin Users who can generate screens themselves, can make use of our Alias plugin. As the name indicates, this plugin simply passes along your own (hosted) image to your TRMNL device. This may sound similar to our Image Display plugin, but with one key difference: Alias lets you encrypt + decrypt the content, making it impossible for TRMNL to know what's being displayed. # How it works TRMNL expects image content to be accessible over the internet / home-network. This image content may then be plugged into the TRMNL web application in either plain text or encrypted format. The diagram below explains how each of the Alias plugin strategies differ. # Getting started To set up an Alias plugin, first provide the absolute URL of your image. Make sure the image is 1 bit bitmap(bmp3), 1-bit PNG, or 2-bit PNG (OG model, firmware v1.6.x or higher), 800x480 pixels. [See here](https://docs.trmnl.com/go/imagemagick-guide) for a hint on generating this format with ImageMagick. **Type - Plain Text** When your TRMNL device requests content from your Alias plugin instance, TRMNL will respond with the data you provided in the TRMNL web application. For example, as per the screenshot above. **Type - Encrypted** For an alias with type Encrypted, encrypt your image_url using <**TBD**> algorithm. Next, provide a decryption key to your TRMNL device via the captive WiFi portal. You can instantiate WiFi pairing mode by holding the boot button on the back of your TRMNL for 5-6 seconds, then letting go, while the device is turned on. Your TRMNL device will store this decryption key onboard, and never share it over the internet. # Putting it all together The Alias plugin provides a secure way to generate and consume content without sharing any information with the TRMNL servers, while still leveraging the convenience and power of the TRMNL web application. # Troubleshooting **Request origins** Whatever the exact URL is that can be reached by your local network, use that in the URL field. If you're running a server at , provide that full URL. It does not have to be `https`. **Image metadata** Most issues with this plugin are due to the image formatting not matching [our specifications](https://docs.trmnl.com/go/imagemagick-guide) exactly. For example, the OG TRMNL device model expects 800x480 pixel images, not 1600x960 or some other equivalent ratio. **Image response headers** There have been reports that your image's endpoint must respond with a `content-length` header; the value should be an integer. This is a requirement we are [actively investigating](https://github.com/usetrmnl/firmware/issues/156) and intend to remove. If you are generating images on the fly, you may not have this value by default because the content is still streaming. In this case, use a dummy value. **Caching** At the bottom of your Alias plugin settings, there is a section called **Enable cache?**: If set to **no**, your device will always re-draw the image URL, even if its filename matches the last rendered filename (*e.g.*, the image is rewritten at the source using the same filename). --- # Is there a plugin for... Public Plugins: Community Recipes: List of all plugins: - [Exquisite Corpses](https://trmnl.com/recipes/241214) - [IndyCar Calendar](https://trmnl.com/recipes/244852) - [Mitglieder Deutscher Bundestag](https://trmnl.com/recipes/233024) - [1-bit Jaws](https://trmnl.com/recipes/57642) - [10 PRINT](https://trmnl.com/recipes/176517) - [2. Bundesliga Spieltag](https://trmnl.com/recipes/178650) - [72 Japanese Seasons](https://trmnl.com/recipes/103813) - [A-League Men Ladder](https://trmnl.com/recipes/260919) - [AA Daily Reflection](https://trmnl.com/recipes/260105) - [ABC NEWS Australia](https://trmnl.com/recipes/74581) - [ACTransit Stop](https://trmnl.com/recipes/183449) - [ADHDinos](https://trmnl.com/recipes/248040) - [AFL Ladder](https://trmnl.com/recipes/84638) - [AI Leaderboard](https://trmnl.com/recipes/258521) - [ATP/WTA Tennis Rankings](https://trmnl.com/recipes/194775) - [AZ511](https://trmnl.com/recipes/254461) - [Aare Guru](https://trmnl.com/recipes/118461) - [Actual Budget](https://trmnl.com/recipes/111710) - [Adafruit IO](https://trmnl.com/recipes/112643) - [AdafruitIO](https://trmnl.com/recipes/41159) - [Advanced RSS](https://trmnl.com/recipes/79118) - [Advice Slip](https://trmnl.com/recipes/147316) - [Air Quality](https://trmnl.com/recipes/193113) - [Air Quality Forecast](https://trmnl.com/recipes/23306) - [Air Quality Monitor](https://trmnl.com/recipes/62233) - [Alert.Swiss](https://trmnl.com/recipes/238152) - [Alias](https://trmnl.com/integrations/alias) - [All Your News](https://trmnl.com/recipes/182990) - [Alpenglow](https://trmnl.com/integrations/alpenglow) - [Amber Electric](https://trmnl.com/recipes/53435) - [Anagram Puzzle](https://trmnl.com/recipes/34845) - [Anime Character of the Day](https://trmnl.com/recipes/227462) - [Anime of the Day](https://trmnl.com/recipes/48032) - [Anon Opin](https://trmnl.com/recipes/147675) - [AppSumo Deals](https://trmnl.com/recipes/179048) - [Apple Calendar](https://trmnl.com/integrations/apple-calendar) - [Apple Music](https://trmnl.com/integrations/apple-music) - [Apple Note](https://trmnl.com/recipes/191420) - [Apple Note Board](https://trmnl.com/recipes/96659) - [Apple Photos](https://trmnl.com/integrations/apple-photos) - [Apple Reminders](https://trmnl.com/recipes/17237) - [Apple Shortcuts Reminders](https://trmnl.com/recipes/79716) - [Apple Steps](https://trmnl.com/recipes/32162) - [Architectural Styles](https://trmnl.com/recipes/228508) - [Argentina's Central Bank Reserves](https://trmnl.com/recipes/231257) - [Arlington Public School Status](https://trmnl.com/recipes/201396) - [Arlington Public Schools Menus](https://trmnl.com/recipes/198409) - [Art Store](https://trmnl.com/recipes/199611) - [Art of the Day](https://trmnl.com/recipes/16765) - [Asana](https://trmnl.com/integrations/asana) - [Asfalt Alert (Dutch Traffic)](https://trmnl.com/recipes/115011) - [Astronauts](https://trmnl.com/integrations/astronauts) - [Astronomy Picture of the Day](https://trmnl.com/recipes/54706) - [Atomic Habits Quotes](https://trmnl.com/integrations/atomic-habits-quotes) - [Austrian Train Departures (OEBB)](https://trmnl.com/recipes/28271) - [Autobahn Warnings](https://trmnl.com/recipes/232543) - [Autumn](https://trmnl.com/recipes/119587) - [Avalanche Forecast](https://trmnl.com/recipes/195454) - [Awair](https://trmnl.com/recipes/200956) - [Azure DevOps - Pull Requests](https://trmnl.com/recipes/160336) - [BART Commute](https://trmnl.com/recipes/181434) - [BART Departures](https://trmnl.com/recipes/180202) - [BARTstop](https://trmnl.com/recipes/180254) - [BBC News - Latest News](https://trmnl.com/recipes/206768) - [BBC World News](https://trmnl.com/recipes/79766) - [BCycle](https://trmnl.com/recipes/100262) - [BTC Price and Chart](https://trmnl.com/integrations/btc-price-and-chart) - [BVG - Berlin Public Transport](https://trmnl.com/recipes/39029) - [BVG departures](https://trmnl.com/recipes/113756) - [Bandcamp Album of the Day](https://trmnl.com/recipes/260387) - [Basecamp](https://trmnl.com/integrations/basecamp) - [Basecamp Assignments](https://trmnl.com/recipes/249612) - [Bauernkalender](https://trmnl.com/recipes/192909) - [Bay Wheels](https://trmnl.com/recipes/185947) - [Bears Football](https://trmnl.com/recipes/165579) - [Been There: World Map](https://trmnl.com/recipes/254631) - [Bergfex Snow Report](https://trmnl.com/recipes/219253) - [Better Battery](https://trmnl.com/recipes/217763) - [Beware of Toddler Comic](https://trmnl.com/recipes/131756) - [Bible Verse (ESV)](https://trmnl.com/recipes/96153) - [Bible Verses](https://trmnl.com/integrations/bible-verses) - [Bible in a Year](https://trmnl.com/recipes/244519) - [Bijbelvers Dagelijkswoord](https://trmnl.com/recipes/142718) - [Bike Stations](https://trmnl.com/recipes/25766) - [Binary Clock](https://trmnl.com/recipes/11153) - [Binary Clock](https://trmnl.com/recipes/135561) - [Biorhythms](https://trmnl.com/integrations/biorhythms) - [Birds 🐦](https://trmnl.com/recipes/248919) - [Birthday Calendar](https://trmnl.com/recipes/225941) - [Bitbucket Pull Requests](https://trmnl.com/recipes/219271) - [Bitcoin Halving Countdown](https://trmnl.com/recipes/264832) - [Bitcoin Market Overview](https://trmnl.com/recipes/242408) - [Bitcoin Mempool](https://trmnl.com/recipes/167030) - [Bitcoin Price Chart](https://trmnl.com/recipes/157962) - [Bitcoin Wallet](https://trmnl.com/integrations/bitcoin-wallet-balance) - [Bitpanda Portfolio](https://trmnl.com/integrations/bitpanda-portfolio) - [Bluesky Profile](https://trmnl.com/recipes/85582) - [BoardGameGeek - Your Collection](https://trmnl.com/recipes/25265) - [BoardGameGeek The Hotness &c.](https://trmnl.com/recipes/262487) - [Book Club Planner](https://trmnl.com/recipes/126964) - [Book Cover](https://trmnl.com/recipes/123042) - [Book by its Cover](https://trmnl.com/recipes/127048) - [Book of the day](https://trmnl.com/recipes/40821) - [Bored Activities](https://trmnl.com/recipes/186137) - [Bouldering Gym Capacity](https://trmnl.com/recipes/91950) - [Brainless Tales](https://trmnl.com/recipes/181181) - [Brandom](https://trmnl.com/recipes/96506) - [Brick Storage](https://trmnl.com/integrations/brick-storage) - [Brighton buses](https://trmnl.com/recipes/112752) - [Brisbane Bin Collection](https://trmnl.com/recipes/240601) - [Buienradar (Dutch Weather)](https://trmnl.com/recipes/21248) - [Buienradar image](https://trmnl.com/recipes/278318) - [Bundesliga Spieltag](https://trmnl.com/recipes/137683) - [Business Cat Comic](https://trmnl.com/recipes/172970) - [Busy Buddy](https://trmnl.com/integrations/busy-buddy) - [Börsihind.ee](https://trmnl.com/recipes/122983) - [CISA Directives and Alerts](https://trmnl.com/recipes/258980) - [CalDAV](https://trmnl.com/integrations/caldav) - [Calendar XL](https://trmnl.com/recipes/261373) - [Calvin and Hobbes Comics](https://trmnl.com/recipes/27184) - [CamelCamelCamel](https://trmnl.com/recipes/121902) - [Canadian Holidays](https://trmnl.com/recipes/176366) - [Canvas](https://trmnl.com/integrations/canvas) - [Carbon Proxy - Local LLM](https://trmnl.com/recipes/287762) - [Cat Facts](https://trmnl.com/recipes/2734) - [Cats 😸](https://trmnl.com/recipes/100133) - [Cats, Dogs and Cat Facts](https://trmnl.com/recipes/40326) - [Changelog](https://trmnl.com/integrations/changelog) - [Channel News Asia - Latest News](https://trmnl.com/recipes/43101) - [ChartMogul](https://trmnl.com/recipes/234571) - [ChatGPT](https://trmnl.com/integrations/chatgpt) - [Check X Order Queue](https://trmnl.com/recipes/260817) - [Chess.com Ratings Chart](https://trmnl.com/recipes/115874) - [Chess.com puzzle of the day](https://trmnl.com/recipes/109248) - [Chinese Proverbs (Translated)](https://trmnl.com/recipes/171219) - [Chuck Norris Facts](https://trmnl.com/recipes/19481) - [CitiBike](https://trmnl.com/recipes/97268) - [Clash Royale Statistics](https://trmnl.com/recipes/80608) - [Claude Code Dashboard](https://trmnl.com/recipes/277976) - [Claude Code Dashboard](https://trmnl.com/recipes/278858) - [Claude Usage](https://trmnl.com/recipes/263932) - [ClickHole](https://trmnl.com/recipes/31807) - [ClickUp Tasks](https://trmnl.com/recipes/48679) - [Clicky](https://trmnl.com/integrations/clicky) - [Climate Change: Global temperature anomalies](https://trmnl.com/recipes/238037) - [Clock](https://trmnl.com/integrations/clock) - [Close](https://trmnl.com/integrations/close) - [CodingLife](https://trmnl.com/recipes/91273) - [Coffee Recipe](https://trmnl.com/recipes/252053) - [CoinMarketCap](https://trmnl.com/integrations/coinmarketcap) - [CoinSpot AUD](https://trmnl.com/recipes/101850) - [Coinbase BTC Price](https://trmnl.com/recipes/13890) - [Comic Book Covers](https://trmnl.com/recipes/186430) - [Comic Library](https://trmnl.com/recipes/180935) - [Community: Conway's Game of Life](https://trmnl.com/recipes/114863) - [Compliments](https://trmnl.com/integrations/compliments) - [ConvertKit](https://trmnl.com/integrations/convertkit-analytics) - [Countdown](https://trmnl.com/recipes/16789) - [Countdown](https://trmnl.com/recipes/188160) - [Countdown with Icons](https://trmnl.com/recipes/152234) - [Counter Strike 2](https://trmnl.com/recipes/221410) - [Counter-Strike 2 Item Showcase](https://trmnl.com/recipes/180810) - [Cozy Scenes: Daily Comic Moments🐱](https://trmnl.com/recipes/284437) - [Cozy Scenes: Daily Vibes](https://trmnl.com/recipes/270926) - [Cozy Scenes: Inky Blobs](https://trmnl.com/recipes/285574) - [Cozy Scenes: Side Quests (Task List RPG)📜🗡️](https://trmnl.com/recipes/283303) - [Cozy Scenes: Waifu Weather 🌞](https://trmnl.com/recipes/280830) - [Cracow Public Transport](https://trmnl.com/recipes/47552) - [Crypto Fear & Greed Index](https://trmnl.com/recipes/19728) - [Crypto Market Status](https://trmnl.com/recipes/59188) - [Culture Series Ship Names](https://trmnl.com/recipes/44727) - [Currency Index](https://trmnl.com/recipes/273095) - [Custom Cheatsheet](https://trmnl.com/recipes/197356) - [Custom Next Holiday and Countdown](https://trmnl.com/recipes/257171) - [Custom Text](https://trmnl.com/integrations/custom-text) - [Custom Wisdom Tile](https://trmnl.com/recipes/110948) - [Cut/daily Quote of the Day](https://trmnl.com/recipes/248986) - [Cyanide and Happiness Comics](https://trmnl.com/recipes/30789) - [DC Bus Stop Schedule](https://trmnl.com/recipes/65327) - [DC Metro Station Schedule](https://trmnl.com/recipes/21537) - [DEFAS Departures](https://trmnl.com/recipes/137663) - [DIN Time 風](https://trmnl.com/recipes/131778) - [DOTA Team Rankings](https://trmnl.com/recipes/185654) - [DVB - Dresden Public Transport](https://trmnl.com/recipes/113464) - [DWD](https://trmnl.com/recipes/83018) - [DWD Bio Wetter](https://trmnl.com/recipes/32651) - [DWD Pollen](https://trmnl.com/recipes/32444) - [Dad Jokes](https://trmnl.com/recipes/619) - [Dad Jokes by GroanDeck.com](https://trmnl.com/recipes/285134) - [Dad Jokes via icanhazdadjoke](https://trmnl.com/recipes/168807) - [Dagens Nyheter Latest News](https://trmnl.com/recipes/176747) - [Daily AI News](https://trmnl.com/recipes/214031) - [Daily B.C. Comic](https://trmnl.com/recipes/248039) - [Daily Conway's Game of Life](https://trmnl.com/recipes/226511) - [Daily Corporate Bullshit](https://trmnl.com/recipes/226952) - [Daily Devotional](https://trmnl.com/recipes/244233) - [Daily Dewey](https://trmnl.com/recipes/127278) - [Daily Excuse](https://trmnl.com/recipes/286320) - [Daily Film and TV Trivia](https://trmnl.com/recipes/144919) - [Daily Finance Tip](https://trmnl.com/recipes/231754) - [Daily Form: Workout of the Day](https://trmnl.com/recipes/263676) - [Daily Horoscope](https://trmnl.com/recipes/30775) - [Daily Luann Comic](https://trmnl.com/recipes/248034) - [Daily New Yorker Cartoon](https://trmnl.com/recipes/131760) - [Daily Pokémon](https://trmnl.com/recipes/115891) - [Daily Quran](https://trmnl.com/recipes/76103) - [Daily Rituals](https://trmnl.com/recipes/47433) - [Daily Shell: Daily Unix Tips](https://trmnl.com/recipes/252227) - [Daily Tarot](https://trmnl.com/recipes/35833) - [Daily Tide Chart](https://trmnl.com/recipes/197206) - [Daily Torah Quote](https://trmnl.com/recipes/140237) - [Daily Torah Study](https://trmnl.com/recipes/151619) - [Daily Torah Topic](https://trmnl.com/recipes/186781) - [Daily Torah Wisdom](https://trmnl.com/recipes/195699) - [Daily Weather](https://trmnl.com/recipes/150460) - [Danish Power Prices](https://trmnl.com/recipes/240727) - [Data Breaches](https://trmnl.com/recipes/183668) - [Date + Weekday Indicator](https://trmnl.com/recipes/59988) - [Day Night Map](https://trmnl.com/recipes/127208) - [Day of the Week](https://trmnl.com/recipes/244484) - [DayGLANCE](https://trmnl.com/recipes/263917) - [Days Left This Year](https://trmnl.com/integrations/days-left-year) - [Days Left Until Retirement](https://trmnl.com/integrations/days-left-retirement) - [Days Left Until...](https://trmnl.com/integrations/days-left-until) - [Days Since...](https://trmnl.com/integrations/days-since) - [Dengonban](https://trmnl.com/recipes/42454) - [Desk Rhythm: Your Time-Management Companion](https://trmnl.com/recipes/263049) - [Deutsche Bahn Abfahrten](https://trmnl.com/integrations/deutsche-bahn-abfahrten) - [Deutsche Bahn Departures](https://trmnl.com/recipes/25192) - [Dev.to Articles](https://trmnl.com/recipes/184240) - [Developer Excuse](https://trmnl.com/recipes/53432) - [DeviantArt](https://trmnl.com/recipes/246026) - [Dexcom (Home Assistant)](https://trmnl.com/recipes/47133) - [Dharma Quotes](https://trmnl.com/recipes/122917) - [Die Losungen](https://trmnl.com/recipes/230750) - [Digital Clock](https://trmnl.com/recipes/73403) - [Digital Clock](https://trmnl.com/recipes/190574) - [DigitalOcean Status](https://trmnl.com/recipes/227481) - [Dinosaur Comics](https://trmnl.com/recipes/28743) - [Discogs](https://trmnl.com/recipes/255730) - [Discord Messages](https://trmnl.com/recipes/172622) - [Divvy](https://trmnl.com/recipes/21568) - [Docker Container Monitor](https://trmnl.com/recipes/162087) - [Dog Facts](https://trmnl.com/recipes/2554) - [Dogs 🐶](https://trmnl.com/recipes/149276) - [Dolar Rates in Argentina](https://trmnl.com/recipes/224161) - [Donations QR code](https://trmnl.com/recipes/278357) - [Dreamcast Title Screens](https://trmnl.com/recipes/222198) - [Dropbox Photo Album](https://trmnl.com/integrations/dropbox-photo-album) - [Duden Wort des Tages](https://trmnl.com/recipes/87629) - [Duolingo](https://trmnl.com/recipes/57294) - [Duolingo](https://trmnl.com/recipes/152659) - [Durham Waste Collection](https://trmnl.com/recipes/178528) - [Dutch Public Transit Upcoming Departures](https://trmnl.com/recipes/143803) - [Dynamic energy prices in Belgium](https://trmnl.com/recipes/148409) - [E-sports upcoming tournaments](https://trmnl.com/recipes/181530) - [EDHRec Top MTG Commanders](https://trmnl.com/recipes/131111) - [EPEX Nederland](https://trmnl.com/recipes/86393) - [EPL Fixtures](https://trmnl.com/recipes/251081) - [EPL Fixtures - My Team Only](https://trmnl.com/recipes/28435) - [EToro Portfolio Viewer](https://trmnl.com/recipes/253167) - [EVCC Dashboard](https://trmnl.com/recipes/240868) - [Earth Observatory Image of the Day](https://trmnl.com/recipes/280791) - [Earthquakes](https://trmnl.com/recipes/33312) - [Ecowitt Weather Station](https://trmnl.com/recipes/48926) - [Eight Sleep](https://trmnl.com/integrations/eight-sleep) - [Electric Sheep](https://trmnl.com/recipes/268491) - [Electricity prices (tado/awattar/smartENERGY)](https://trmnl.com/recipes/116993) - [Element of the Day](https://trmnl.com/recipes/226166) - [Elprisetjustnu](https://trmnl.com/recipes/28620) - [Email Messages (IMAP)](https://trmnl.com/recipes/198482) - [Email Meter](https://trmnl.com/integrations/email-meter) - [Environmental Sensor Graph](https://trmnl.com/recipes/256958) - [Eredivisie Standings](https://trmnl.com/recipes/124580) - [Esthio Schoolmenu](https://trmnl.com/recipes/250418) - [Ethereum Wallet](https://trmnl.com/integrations/ethereum-wallet-balance) - [Etsy](https://trmnl.com/integrations/etsy) - [Europa League Table](https://trmnl.com/recipes/156818) - [Evil Insults](https://trmnl.com/recipes/19335) - [Exchange Rates](https://trmnl.com/recipes/142269) - [Expecto (Pregnancy Guide)](https://trmnl.com/recipes/89136) - [Extended Weather Dashboard](https://trmnl.com/recipes/214872) - [Extra Fabulous Comics](https://trmnl.com/recipes/253160) - [F1AcademyCalendar.com](https://trmnl.com/recipes/52893) - [F2Calendar.com - Formula 2 Calendar](https://trmnl.com/recipes/52472) - [F3Calendar.com - Formula 3 Calendar](https://trmnl.com/recipes/52907) - [FOXNews](https://trmnl.com/recipes/190653) - [FPL top 5 standings](https://trmnl.com/recipes/184477) - [FSHBWL Aquarium](https://trmnl.com/recipes/136156) - [Far Side Comic](https://trmnl.com/recipes/122324) - [Farcaster](https://trmnl.com/recipes/93503) - [Fastmail Calendar](https://trmnl.com/integrations/fastmail-calendar) - [Fathom Analytics](https://trmnl.com/recipes/89064) - [Fera](https://trmnl.com/integrations/fera) - [Ferengi Rules of Acquisition](https://trmnl.com/integrations/ferengi-rules-of-acquisition) - [Fesshole](https://trmnl.com/recipes/32630) - [Financial Times (FT)](https://trmnl.com/recipes/133514) - [Fish Tank](https://trmnl.com/recipes/224093) - [FlashInvaders Status](https://trmnl.com/recipes/26983) - [Flight Tracker](https://trmnl.com/integrations/flight-tracker) - [Flip Clock](https://trmnl.com/recipes/174854) - [Focus Timer](https://trmnl.com/recipes/241966) - [Football - EFL Championship Standings](https://trmnl.com/recipes/35605) - [Football - EPL Fixtures](https://trmnl.com/recipes/13395) - [Football - EPL Results](https://trmnl.com/recipes/13442) - [Football - EPL Standings](https://trmnl.com/recipes/15887) - [Football - La Liga Standings](https://trmnl.com/recipes/275566) - [Football - MLS Standings](https://trmnl.com/recipes/25478) - [Formula 1 - Race and Standings](https://trmnl.com/recipes/251959) - [Formula 1 Calendar & Race Times](https://trmnl.com/recipes/20507) - [Formula 1 Constructor Standings](https://trmnl.com/recipes/30563) - [Formula 1 Driver Standings](https://trmnl.com/recipes/30547) - [Formula 1 Races](https://trmnl.com/recipes/11914) - [FormulaECal.com - Formula E Calendar](https://trmnl.com/recipes/52910) - [Fortnite Statistics](https://trmnl.com/recipes/186199) - [Fortune Cookie of the Day](https://trmnl.com/recipes/201904) - [Fox News - Political Cartoon of the Day](https://trmnl.com/recipes/162296) - [FoxTrot Classics Comic](https://trmnl.com/recipes/122369) - [Foxes in Love](https://trmnl.com/recipes/84949) - [Frank Energie](https://trmnl.com/recipes/245095) - [Fuel Prices Portugal](https://trmnl.com/recipes/181330) - [Fun Daily Holidays](https://trmnl.com/integrations/fun-daily-holidays) - [Fun Sort of Time](https://trmnl.com/recipes/182877) - [Fun With Flags](https://trmnl.com/recipes/176791) - [GG.Deals - Track your game discounts](https://trmnl.com/recipes/192524) - [GO Transit Schedule](https://trmnl.com/recipes/226758) - [GSOL](https://trmnl.com/recipes/43564) - [GTA VI Countdown](https://trmnl.com/recipes/219410) - [Gaffa](https://trmnl.com/integrations/gaffa) - [Game Boy Title Screens](https://trmnl.com/recipes/202774) - [Game Deals](https://trmnl.com/recipes/184803) - [GameCube Title Screens](https://trmnl.com/recipes/202626) - [Garfield (Comic)](https://trmnl.com/recipes/31447) - [Gemini](https://trmnl.com/recipes/50076) - [Generative Art: Trees, Boxes, Circles, and Lines](https://trmnl.com/recipes/198750) - [GeoGuess](https://trmnl.com/recipes/193220) - [Geovelo](https://trmnl.com/recipes/72208) - [German Film Guide](https://trmnl.com/recipes/130454) - [Ghibli Backgrounds](https://trmnl.com/recipes/210390) - [GitHub Commit Graph](https://trmnl.com/integrations/github-commit-graph) - [GitHub Contributors](https://trmnl.com/integrations/github-contributor-summary) - [GitHub Issues](https://trmnl.com/recipes/53612) - [GitHub Profile](https://trmnl.com/recipes/190117) - [GitHub Pull Requests](https://trmnl.com/recipes/221833) - [GitHub Trending Repos](https://trmnl.com/recipes/243710) - [GitHub Workflow Runs](https://trmnl.com/recipes/161967) - [GitLab Merge Requests](https://trmnl.com/recipes/76874) - [Gita Verse of the Day](https://trmnl.com/recipes/261336) - [Github Releases](https://trmnl.com/recipes/176802) - [Github Repo Insights](https://trmnl.com/recipes/158645) - [Github Review Requests](https://trmnl.com/recipes/105565) - [Global Stock Market](https://trmnl.com/recipes/45484) - [GoatCounter](https://trmnl.com/recipes/228787) - [Goodreads](https://trmnl.com/recipes/168273) - [Goodreads RSS](https://trmnl.com/recipes/112256) - [Google Analytics](https://trmnl.com/integrations/google-analytics) - [Google Calendar](https://trmnl.com/integrations/google-calendar) - [Google Doodle of the Day](https://trmnl.com/recipes/240618) - [Google Drive (photos)](https://trmnl.com/integrations/google-drive-photos) - [Google Photos](https://trmnl.com/recipes/227153) - [Google Photos](https://trmnl.com/integrations/google-photos) - [Google Photos Canvas](https://trmnl.com/recipes/230712) - [Google Sheet Quote](https://trmnl.com/recipes/121258) - [Google Sheets Table (Finance)](https://trmnl.com/recipes/247022) - [Google Tasks](https://trmnl.com/integrations/google-tasks) - [Google Trends](https://trmnl.com/recipes/149848) - [Government Alerts](https://trmnl.com/recipes/217791) - [Grafana Dashboard](https://trmnl.com/recipes/105532) - [Grafana Panel](https://trmnl.com/recipes/189121) - [Gumroad](https://trmnl.com/integrations/gumroad-analytics) - [HTM Tram/Bus Status](https://trmnl.com/recipes/148353) - [HTTP Cats](https://trmnl.com/recipes/235410) - [HTTP Dogs](https://trmnl.com/recipes/243773) - [HVV Transit](https://trmnl.com/recipes/187524) - [Habitica Tasks](https://trmnl.com/recipes/123494) - [Hacker News](https://trmnl.com/integrations/hacker-news) - [Haiku Weather](https://trmnl.com/recipes/252404) - [Hardcover](https://trmnl.com/recipes/63345) - [Hardcover - Reading Buddy](https://trmnl.com/recipes/122776) - [Harvest](https://trmnl.com/integrations/harvest) - [Healthchecks.io](https://trmnl.com/recipes/64170) - [Hello My Name Is](https://trmnl.com/recipes/36396) - [Heyokyay](https://trmnl.com/recipes/243525) - [Himawari Real-Time Image](https://trmnl.com/recipes/178288) - [Hogwarts Randomizer](https://trmnl.com/recipes/97572) - [Home Assistant Entity - Trend](https://trmnl.com/recipes/124881) - [Home Assistant Entity Cards](https://trmnl.com/recipes/286869) - [Home Assistant History Graph](https://trmnl.com/recipes/286742) - [Home Assistant Screenshot](https://trmnl.com/integrations/home-assistant-screenshot) - [Home Assistant To-Do List](https://trmnl.com/recipes/227107) - [Home Assistant Weather Station](https://trmnl.com/recipes/46862) - [Hong Kong Bus ETA](https://trmnl.com/recipes/14066) - [Hong Kong Tunnel Tolls](https://trmnl.com/recipes/227860) - [Horizontal World Clock](https://trmnl.com/recipes/153664) - [Hourly Weather Forecast](https://trmnl.com/recipes/35788) - [Hoymiles Solar Panel](https://trmnl.com/recipes/140588) - [HubSpot](https://trmnl.com/integrations/hubspot) - [Hurricane Outlook](https://trmnl.com/recipes/137775) - [Hydration Reminder](https://trmnl.com/recipes/220845) - [I Ching - Daily Divination](https://trmnl.com/recipes/49685) - [I'm in a Meeting!](https://trmnl.com/recipes/87731) - [ISS Location](https://trmnl.com/integrations/iss-location) - [ISS Tracker Map](https://trmnl.com/recipes/135652) - [Ice-Breaker Questions](https://trmnl.com/recipes/200507) - [Image Display](https://trmnl.com/integrations/image-display) - [Imgur Album](https://trmnl.com/recipes/87125) - [Indian Premier League](https://trmnl.com/recipes/271323) - [InfoQ Headlines](https://trmnl.com/recipes/77844) - [International Flip Date Clock](https://trmnl.com/recipes/247379) - [Is Mercury in Retrograde?](https://trmnl.com/recipes/221425) - [Is my metro commute screwed?](https://trmnl.com/integrations/is-my-metro-commute-screwed) - [Islamic Prayer Times](https://trmnl.com/recipes/246906) - [It's 5 O'Clock Somewhere](https://trmnl.com/recipes/253752) - [It's a Plane](https://trmnl.com/recipes/122422) - [JUDU - Vilnius Public Transport](https://trmnl.com/recipes/177713) - [Japanese word of the day (with furigana)](https://trmnl.com/recipes/117962) - [Jellyfin - Recent Activity](https://trmnl.com/recipes/16937) - [Jira Issue Counts](https://trmnl.com/recipes/221333) - [Jira Issues List](https://trmnl.com/recipes/216106) - [Journaling Prompt](https://trmnl.com/recipes/234315) - [KKD Standings](https://trmnl.com/recipes/136209) - [KSP Mission Outcome](https://trmnl.com/recipes/66356) - [Kagi News](https://trmnl.com/recipes/45842) - [Kat Walkers Stats](https://trmnl.com/recipes/30257) - [Kickstarter](https://trmnl.com/integrations/kickstarter) - [Kiosko Newspaper Frontpages](https://trmnl.com/recipes/291635) - [Kung Fu Panda Quotes](https://trmnl.com/recipes/240176) - [LCARS Schematics](https://trmnl.com/integrations/lcars-schematics) - [LDS Quotes](https://trmnl.com/recipes/23523) - [LLM Leaderboard](https://trmnl.com/recipes/101445) - [LOTR Fellowship Stills](https://trmnl.com/recipes/249533) - [LOTR Movie Quotes](https://trmnl.com/recipes/43498) - [LP Weather](https://trmnl.com/recipes/249564) - [Laatste nieuws - NOS - RTL - AD - Nu.nl](https://trmnl.com/recipes/206736) - [Language Learning](https://trmnl.com/integrations/language-learning) - [Language Learning: Sentences](https://trmnl.com/recipes/177718) - [Last 100 GitHub Workflows](https://trmnl.com/recipes/160135) - [LastFM](https://trmnl.com/recipes/25815) - [Latest Episodes from TWiT](https://trmnl.com/recipes/82367) - [Latest Hardcover Quote](https://trmnl.com/recipes/84359) - [Latest TRMNL recipes](https://trmnl.com/recipes/156769) - [Launchpad - Upcoming Rocket Launches](https://trmnl.com/recipes/189917) - [Lawn Mowing Direction](https://trmnl.com/recipes/92770) - [Lego Sets](https://trmnl.com/recipes/184371) - [Lexophile](https://trmnl.com/recipes/120897) - [Lichess Daily Puzzle](https://trmnl.com/recipes/30458) - [Lichess Puzzle](https://trmnl.com/recipes/63321) - [Linear](https://trmnl.com/recipes/187006) - [Literate Movies](https://trmnl.com/recipes/122948) - [Literature Clock](https://trmnl.com/recipes/11141) - [Litterbox Comics](https://trmnl.com/recipes/148700) - [Little Better: Daily Challenges for a Better You](https://trmnl.com/recipes/262723) - [Live CCTV Roulette](https://trmnl.com/recipes/269803) - [Live Earth View](https://trmnl.com/recipes/114409) - [Live Solar System](https://trmnl.com/recipes/207766) - [Lobby](https://trmnl.com/integrations/lobby) - [Lobsters](https://trmnl.com/recipes/212869) - [Lojong Slogans](https://trmnl.com/recipes/11364) - [Lokale bekendmakingen (NL: Local announcements)](https://trmnl.com/recipes/22748) - [Ludum Dare Games](https://trmnl.com/recipes/271931) - [Lumon Wellness](https://trmnl.com/recipes/102880) - [Lunar Calendar](https://trmnl.com/integrations/lunar-calendar) - [Lunch Money](https://trmnl.com/integrations/lunch-money) - [Lyrics](https://trmnl.com/recipes/226391) - [MBTA Alerts](https://trmnl.com/recipes/93149) - [MBTA Arrivals](https://trmnl.com/recipes/179242) - [MEE6 Discord Leaderboard](https://trmnl.com/recipes/48539) - [METAR](https://trmnl.com/recipes/21542) - [MLB My Team Tracker](https://trmnl.com/recipes/51132) - [MLB Season Standings](https://trmnl.com/recipes/273979) - [MLB Standings](https://trmnl.com/recipes/274231) - [MLB Team Dashboard](https://trmnl.com/recipes/272277) - [MTG Card of the Day](https://trmnl.com/recipes/127986) - [MTG Daily Scry](https://trmnl.com/recipes/147727) - [MTR Next Trains (Hong Kong)](https://trmnl.com/recipes/14045) - [MVG - Munich Public Transport](https://trmnl.com/recipes/37373) - [Magic 8 Ball](https://trmnl.com/recipes/22689) - [Magic Draw](https://trmnl.com/recipes/89345) - [Magic Plane of the Day](https://trmnl.com/recipes/129689) - [Magic Price Tracker](https://trmnl.com/recipes/261552) - [Mailchimp](https://trmnl.com/integrations/mailchimp-analytics) - [MakerWorld Trending Models](https://trmnl.com/recipes/272038) - [Manchester Bikes](https://trmnl.com/recipes/127062) - [Manga of the Day](https://trmnl.com/recipes/66601) - [March Madness Group Standings](https://trmnl.com/recipes/29987) - [Mario Kart Track](https://trmnl.com/recipes/101176) - [Marquee](https://trmnl.com/recipes/153733) - [Mastodon](https://trmnl.com/recipes/89880) - [Matomo Analytics](https://trmnl.com/recipes/97234) - [Matrix](https://trmnl.com/recipes/16382) - [Mawaqit](https://trmnl.com/recipes/105763) - [Mawaqit](https://trmnl.com/integrations/mawaqit) - [Max Payne Quotes](https://trmnl.com/recipes/224761) - [Maze](https://trmnl.com/recipes/131035) - [MealViewer (School Lunch)](https://trmnl.com/recipes/133879) - [Mealie Shopping List](https://trmnl.com/recipes/159793) - [Meesman Aandelen Wereldwijd (NL)](https://trmnl.com/recipes/188084) - [Meh Deals](https://trmnl.com/recipes/262732) - [Menu Pricelist](https://trmnl.com/recipes/135480) - [Met.no Forecast](https://trmnl.com/recipes/132448) - [Metal Price Tracker](https://trmnl.com/recipes/180898) - [Meteoblue Weather](https://trmnl.com/integrations/meteoblue-weather) - [Metro Nashville Police Dept. Active Dispatches](https://trmnl.com/recipes/90541) - [Metro North Departures](https://trmnl.com/recipes/97591) - [Metro North Trip Status](https://trmnl.com/recipes/225668) - [Metro Transit](https://trmnl.com/recipes/95919) - [MetroClock](https://trmnl.com/recipes/128829) - [MetroDate](https://trmnl.com/recipes/128895) - [Mijn afvalwijzer](https://trmnl.com/recipes/200788) - [Milestones - Event Countdown](https://trmnl.com/recipes/135996) - [Minecraft Server](https://trmnl.com/recipes/175847) - [Minecraft Server Statistics](https://trmnl.com/recipes/82497) - [Minecraft Splashes](https://trmnl.com/recipes/245667) - [Mini Muni San Francisco](https://trmnl.com/recipes/151924) - [Miniflux Feed](https://trmnl.com/recipes/33478) - [Mobiliteit.lu Checker](https://trmnl.com/recipes/129030) - [Modern Art of the Day](https://trmnl.com/recipes/184800) - [Modrinth](https://trmnl.com/recipes/46866) - [Mondrian](https://trmnl.com/integrations/mondrian) - [Monkey Island Quotes](https://trmnl.com/recipes/106632) - [MonkeyUser Comics](https://trmnl.com/recipes/230071) - [Monkeytype](https://trmnl.com/recipes/184019) - [Month Progress](https://trmnl.com/recipes/209701) - [Monument of the day](https://trmnl.com/recipes/192148) - [Motivational Quote](https://trmnl.com/integrations/motivational-quote) - [MotoGPCal.com - MotoGP Calendar](https://trmnl.com/recipes/55415) - [Movie Guesser](https://trmnl.com/recipes/178211) - [Movie Quotes](https://trmnl.com/recipes/189976) - [Movie Title Screens 🎬](https://trmnl.com/recipes/98891) - [Mt. Baker Ski Area weather data](https://trmnl.com/recipes/267028) - [My Slack Messages](https://trmnl.com/recipes/128030) - [Myers-Briggs Type](https://trmnl.com/recipes/272814) - [N64 Title Screens](https://trmnl.com/recipes/223120) - [NASA Image of the Day](https://trmnl.com/recipes/76645) - [NBA Standings](https://trmnl.com/recipes/194456) - [NBA Team Schedule](https://trmnl.com/recipes/224233) - [NBL Ladder](https://trmnl.com/recipes/260913) - [NES Title Screens 🕹️](https://trmnl.com/recipes/264734) - [NFL Fantasy Pickems](https://trmnl.com/recipes/143537) - [NFL Schedule](https://trmnl.com/recipes/142825) - [NFL Standings By Division](https://trmnl.com/recipes/187644) - [NFL Team Next Game](https://trmnl.com/recipes/161093) - [NFL Team Tracker](https://trmnl.com/recipes/139791) - [NHL - Next Edmonton Oilers Game](https://trmnl.com/recipes/32262) - [NHL - Team's Next Game](https://trmnl.com/recipes/30044) - [NHL Standings](https://trmnl.com/recipes/31670) - [NL Energy Day Ahead Prices](https://trmnl.com/recipes/70055) - [NMBS Departures](https://trmnl.com/recipes/22733) - [NOAA Tides](https://trmnl.com/recipes/125365) - [NOAA Tropical Weather Outlook (Hurricanes)](https://trmnl.com/recipes/178809) - [NOS News Dutch headlines](https://trmnl.com/recipes/46612) - [NPC for Open Legend RPG](https://trmnl.com/recipes/85403) - [NPM Packages](https://trmnl.com/recipes/168898) - [NRL Ladder](https://trmnl.com/recipes/260500) - [NS Departures](https://trmnl.com/recipes/264343) - [NS Disruptions](https://trmnl.com/recipes/55984) - [NS Station vertrektijden](https://trmnl.com/recipes/127104) - [NTS Radio: Live Now](https://trmnl.com/recipes/166082) - [NWPS River Gauges](https://trmnl.com/recipes/101750) - [NWS Radar (CONUS)](https://trmnl.com/recipes/123491) - [NWS Severe Weather Alerts (USA)](https://trmnl.com/recipes/46722) - [NYC 311](https://trmnl.com/recipes/87446) - [NYC Ferry Departures](https://trmnl.com/recipes/106754) - [NYC Metro Schedule](https://trmnl.com/integrations/nyc-metro-schedule) - [NYC Subway Alerts](https://trmnl.com/recipes/81483) - [NYC Subway Status](https://trmnl.com/recipes/130441) - [NYT Games: Connections](https://trmnl.com/recipes/159089) - [NYTimes Top Stories](https://trmnl.com/recipes/16262) - [Nano Banana Dashboard](https://trmnl.com/integrations/nano-banana-dashboard) - [Naolib - Départs à proximité](https://trmnl.com/recipes/256931) - [Nashville Electric Service (NES)](https://trmnl.com/recipes/112378) - [Nashville Fire Department Active Incidents](https://trmnl.com/recipes/89850) - [Nashville Pollen & Air Quality](https://trmnl.com/recipes/93912) - [Nashville Waste Collection](https://trmnl.com/recipes/240532) - [Nashville WeGo Public Transit](https://trmnl.com/recipes/91940) - [National Day Calendar](https://trmnl.com/recipes/9387) - [National Rail Departures](https://trmnl.com/recipes/203604) - [Naval Quotes](https://trmnl.com/recipes/250062) - [Neocities Sites](https://trmnl.com/recipes/290436) - [Netatmo Weather Station](https://trmnl.com/integrations/netatmo-weather-station) - [Netflix Soon: Release Radar](https://trmnl.com/recipes/264056) - [Network Devices v1.3](https://trmnl.com/recipes/189338) - [New York Times Best Sellers](https://trmnl.com/recipes/121924) - [Newgrounds](https://trmnl.com/recipes/29512) - [Next Calendar Event/Room Reservation](https://trmnl.com/recipes/186929) - [Next MLB Game](https://trmnl.com/recipes/57244) - [Next Rocket Launch](https://trmnl.com/recipes/28575) - [Nextcloud Calendar](https://trmnl.com/integrations/nextcloud-calendar) - [Nextcloud Photos](https://trmnl.com/recipes/285470) - [Night Sky](https://trmnl.com/recipes/290823) - [Nightscout Glucose Trend](https://trmnl.com/recipes/30521) - [Nightstand](https://trmnl.com/integrations/nightstand) - [Notable Local Bird Sightings (from eBird)](https://trmnl.com/recipes/124110) - [Notifiarr](https://trmnl.com/recipes/122270) - [Notion](https://trmnl.com/integrations/notion) - [OP3 Podcast Analytics](https://trmnl.com/recipes/153142) - [OPDS Bookshelf](https://trmnl.com/recipes/122018) - [ORF.at Newsfeed](https://trmnl.com/recipes/173288) - [OSRS GE Price Trend](https://trmnl.com/recipes/265824) - [OSRS Hiscores](https://trmnl.com/recipes/209134) - [Oblique Strategies](https://trmnl.com/recipes/17691) - [Octopus Agile](https://trmnl.com/recipes/28219) - [Off The Mark Comic](https://trmnl.com/recipes/183496) - [Ollama](https://trmnl.com/recipes/193453) - [Ollama AI Model Releases](https://trmnl.com/recipes/88231) - [Olympics 2026: Medals per country](https://trmnl.com/recipes/241160) - [Olympics 2026: Schedule](https://trmnl.com/recipes/241893) - [One Album A Day](https://trmnl.com/recipes/154887) - [One Forgotten Default](https://trmnl.com/recipes/227463) - [One Headline News](https://trmnl.com/recipes/243967) - [Open Library](https://trmnl.com/recipes/124200) - [Open Trivia DB](https://trmnl.com/recipes/137500) - [Open-Meteo Hourly Weather Forecast v1.6.0](https://trmnl.com/recipes/112422) - [OpenWeatherMap Forecast](https://trmnl.com/recipes/41990) - [Opening hours](https://trmnl.com/integrations/opening-hours) - [Openrouter](https://trmnl.com/recipes/162051) - [Outlook Calendar](https://trmnl.com/integrations/outlook-calendar) - [PBS Kids](https://trmnl.com/recipes/110655) - [PGA Leaderboard](https://trmnl.com/recipes/279404) - [POTUS Tracker](https://trmnl.com/recipes/29218) - [PS2 Title Screens](https://trmnl.com/recipes/130437) - [PSTMRK](https://trmnl.com/recipes/74331) - [PagerDuty Incidents](https://trmnl.com/recipes/256937) - [PagerDuty: On Call Today](https://trmnl.com/recipes/158498) - [Paperboy - Hundreds of newspaper frontpages](https://trmnl.com/recipes/152705) - [Parashat HaShavua (Torah Portion)](https://trmnl.com/recipes/110498) - [Parcel](https://trmnl.com/integrations/parcel) - [Park Buddies](https://trmnl.com/recipes/144856) - [Parks Wait Time Display](https://trmnl.com/recipes/66418) - [Paul's Recipe recipe](https://trmnl.com/recipes/236317) - [Paymo Report](https://trmnl.com/recipes/77353) - [Peanuts Comics](https://trmnl.com/recipes/28496) - [Pebble Appstore](https://trmnl.com/recipes/281436) - [Pentagon Pizza Index](https://trmnl.com/recipes/218742) - [Personal Steam Deals](https://trmnl.com/recipes/246965) - [Pi-hole](https://trmnl.com/recipes/220520) - [Pickles Comic](https://trmnl.com/recipes/122316) - [Pitchfork - Best New Albums](https://trmnl.com/recipes/84502) - [Pitchfork - New Album Reviews](https://trmnl.com/recipes/231856) - [Pitt Hour Clock](https://trmnl.com/recipes/250833) - [Pixel Weather](https://trmnl.com/recipes/189394) - [Pixelfed](https://trmnl.com/recipes/103958) - [Pixelfed profile](https://trmnl.com/recipes/106505) - [Place Facts](https://trmnl.com/integrations/place-facts) - [Plant watering Schedule](https://trmnl.com/recipes/181032) - [Plausible](https://trmnl.com/recipes/158263) - [PlayStation Title Screens](https://trmnl.com/recipes/222275) - [Plex (Recently Played)](https://trmnl.com/recipes/107012) - [Plex - Now Playing](https://trmnl.com/recipes/76059) - [Poetry Today](https://trmnl.com/integrations/poetry-today) - [Pokemon Go Updates](https://trmnl.com/recipes/211473) - [Pollen Forecast (Europe)](https://trmnl.com/recipes/56803) - [Pollen Forecast (US)](https://trmnl.com/integrations/pollen-forecast-us) - [Pollen Information Austria](https://trmnl.com/recipes/255979) - [Pollenradar (The Netherlands)](https://trmnl.com/recipes/31139) - [Polymarket](https://trmnl.com/recipes/186483) - [Polymarket](https://trmnl.com/integrations/polymarket) - [Poorly Drawn Lines comic](https://trmnl.com/recipes/72396) - [PostSecret](https://trmnl.com/recipes/31635) - [Posthog Growth accounting](https://trmnl.com/recipes/112268) - [Postmark](https://trmnl.com/integrations/postmark-analytics) - [Potter Quotes](https://trmnl.com/recipes/258242) - [Powerwall Energy Usage](https://trmnl.com/integrations/powerwall-energy-usage) - [Pragmatic Programmer Tips](https://trmnl.com/recipes/145923) - [Pregnancy Tracker](https://trmnl.com/recipes/249841) - [Pretty Calendar](https://trmnl.com/recipes/210616) - [Price Tags](https://trmnl.com/recipes/209398) - [Private Plugin](https://trmnl.com/integrations/private-plugin) - [Prochain Metro RER Paris](https://trmnl.com/recipes/38247) - [Product Hunt](https://trmnl.com/integrations/product-hunt) - [ProfitWell](https://trmnl.com/integrations/profit-well) - [PromptInk](https://trmnl.com/recipes/221038) - [Public Recipe Stats](https://trmnl.com/recipes/170970) - [Puget Sound Transit Nearby](https://trmnl.com/recipes/225183) - [PurpleAir](https://trmnl.com/integrations/purpleair) - [QR Code](https://trmnl.com/recipes/14428) - [Quarter Progress](https://trmnl.com/recipes/212805) - [Queensland Stop Timetable (Translink)](https://trmnl.com/recipes/233040) - [Quoterism Random Quotes](https://trmnl.com/recipes/188221) - [REENIO - booking system](https://trmnl.com/recipes/162137) - [RMV Departures](https://trmnl.com/recipes/196089) - [RSS Feed](https://trmnl.com/integrations/rss-feed) - [RabbitAssets](https://trmnl.com/recipes/181797) - [RabbitForex](https://trmnl.com/recipes/182766) - [Rain Radar NL](https://trmnl.com/recipes/119118) - [Raindrop - Hourly Rain Forecast (USA)](https://trmnl.com/recipes/35332) - [Random Advice](https://trmnl.com/recipes/19328) - [Random Board Game](https://trmnl.com/recipes/31666) - [Random Coffee Cup](https://trmnl.com/recipes/82685) - [Random Computer](https://trmnl.com/recipes/258479) - [Random Cooking Recipes](https://trmnl.com/recipes/105468) - [Random D&D Monster](https://trmnl.com/recipes/209012) - [Random Duck](https://trmnl.com/recipes/58123) - [Random Emoji](https://trmnl.com/recipes/84361) - [Random Google Fonts](https://trmnl.com/recipes/163790) - [Random Hardcover Quotes](https://trmnl.com/recipes/90802) - [Random Image (Unsplash)](https://trmnl.com/recipes/23195) - [Random Joke](https://trmnl.com/recipes/227561) - [Random MDN Article](https://trmnl.com/recipes/153731) - [Random Meal](https://trmnl.com/recipes/42499) - [Random Meme](https://trmnl.com/recipes/236194) - [Random Riddle](https://trmnl.com/recipes/22707) - [Random SCP (broken)](https://trmnl.com/recipes/91883) - [Random Toki Pona Word](https://trmnl.com/recipes/137228) - [Random Unicode](https://trmnl.com/recipes/151050) - [Random Zi](https://trmnl.com/recipes/156095) - [Read It Later](https://trmnl.com/recipes/266399) - [Readwise](https://trmnl.com/integrations/readwise) - [Recent Earthquakes](https://trmnl.com/recipes/133626) - [Recipe Leaderboard](https://trmnl.com/recipes/211002) - [Recurring Items](https://trmnl.com/recipes/260208) - [Recycle! - Belgium](https://trmnl.com/recipes/152152) - [Reddit](https://trmnl.com/integrations/reddit) - [Reddit Comics](https://trmnl.com/recipes/100794) - [Reddit r/place](https://trmnl.com/recipes/182274) - [Redirect](https://trmnl.com/integrations/redirect) - [Relay for St Jude](https://trmnl.com/recipes/87177) - [RetroAchievements: Recent Games](https://trmnl.com/recipes/176378) - [Rick and Morty](https://trmnl.com/recipes/227088) - [Ride With GPS](https://trmnl.com/recipes/96060) - [Robot Reflections](https://trmnl.com/recipes/178565) - [Ruter / Entur Norway](https://trmnl.com/recipes/20164) - [SAT GUS](https://trmnl.com/recipes/115686) - [SBB Swiss Stationboard](https://trmnl.com/recipes/166116) - [SBB/CFF Timetable](https://trmnl.com/recipes/32374) - [SEQ Dam Levels](https://trmnl.com/recipes/263572) - [SL - Stockholm Public Transport](https://trmnl.com/recipes/80473) - [STRMNL](https://trmnl.com/integrations/strmnl) - [SWU – Ulm/Neu-Ulm Public Transport](https://trmnl.com/recipes/52882) - [Safely Endangered](https://trmnl.com/recipes/277420) - [Salah Times](https://trmnl.com/integrations/salah-times) - [Salesforce](https://trmnl.com/integrations/salesforce) - [Sandwitches](https://trmnl.com/recipes/247547) - [School Calendar for Zermelo](https://trmnl.com/recipes/238244) - [Scooter finder](https://trmnl.com/recipes/136058) - [Screensaver](https://trmnl.com/integrations/screensaver) - [Screenshot](https://trmnl.com/integrations/screenshot) - [SeatGeek](https://trmnl.com/recipes/120686) - [Selected Nuclear Power Plant Outages](https://trmnl.com/recipes/280780) - [Semaphore Flags](https://trmnl.com/recipes/249467) - [Sentry](https://trmnl.com/recipes/160968) - [Sentry Issues](https://trmnl.com/recipes/159081) - [Servarr Dashboard](https://trmnl.com/recipes/190336) - [Shabbat Times](https://trmnl.com/recipes/115974) - [Shan Shui Chinese Landscape](https://trmnl.com/recipes/14789) - [Shopify](https://trmnl.com/integrations/shopify) - [Shopping List](https://trmnl.com/integrations/shopping-list) - [Should I Deploy Today?](https://trmnl.com/recipes/29090) - [Simple Analytics](https://trmnl.com/integrations/simple-analytics) - [Simple Calendar](https://trmnl.com/recipes/11023) - [Simple Date](https://trmnl.com/recipes/40143) - [Simple Open Signage](https://trmnl.com/recipes/242183) - [Simple To-do](https://trmnl.com/recipes/233168) - [Simplenote Published Note](https://trmnl.com/recipes/236882) - [Skeletor](https://trmnl.com/recipes/99970) - [SkyWatch](https://trmnl.com/recipes/286469) - [Sleeper One-on-One Matchups](https://trmnl.com/recipes/140718) - [Sleeper: Matchups](https://trmnl.com/recipes/140922) - [Sleeper: Standings](https://trmnl.com/recipes/141171) - [Sleepy Time!](https://trmnl.com/recipes/190819) - [Slickdeals Frontpage](https://trmnl.com/recipes/184875) - [Small Seasons](https://trmnl.com/recipes/18876) - [Snooker Scores](https://trmnl.com/recipes/198010) - [Snow Report](https://trmnl.com/recipes/160226) - [So Very British](https://trmnl.com/recipes/147175) - [Soccer Matches](https://trmnl.com/recipes/275621) - [Soccer Standings](https://trmnl.com/recipes/194354) - [Social Media Followers](https://trmnl.com/recipes/199996) - [Solana Wallet](https://trmnl.com/recipes/229540) - [Solar Weather Activity](https://trmnl.com/recipes/190280) - [Song of the Day - Random Song](https://trmnl.com/recipes/184682) - [Sonos - Now Playing](https://trmnl.com/recipes/261848) - [Sound Transit Link Light Rail](https://trmnl.com/recipes/48113) - [Sound Transit Link Light Rail Dashboard](https://trmnl.com/recipes/179733) - [Space Weather Prediction Center Alerts](https://trmnl.com/recipes/191916) - [Spaceballs](https://trmnl.com/recipes/59980) - [SparkFun: News](https://trmnl.com/recipes/179498) - [Spelling Bee](https://trmnl.com/recipes/201905) - [Spotify - Listening Stats](https://trmnl.com/recipes/164583) - [Spotify Dashboard](https://trmnl.com/recipes/247528) - [Spotify New Releases](https://trmnl.com/recipes/243523) - [Spotify Stats](https://trmnl.com/recipes/245783) - [Spotify Top Tracks](https://trmnl.com/integrations/spotify-top-tracks) - [Spotify Weekly Rotation](https://trmnl.com/integrations/spotify-weekly-rotation) - [Square POS](https://trmnl.com/integrations/square-pos) - [St Albans Refuse Collections](https://trmnl.com/recipes/203404) - [Stardew Valley Villagers](https://trmnl.com/recipes/160532) - [Statuslanes](https://trmnl.com/recipes/181944) - [Statuspage](https://trmnl.com/integrations/statuspage) - [Steam Deals of the Day](https://trmnl.com/recipes/18131) - [Steam Player Count](https://trmnl.com/recipes/181252) - [Steam recent Games](https://trmnl.com/recipes/234204) - [Stock Portfolio](https://trmnl.com/recipes/237580) - [Stock Price](https://trmnl.com/recipes/225680) - [Stock Price](https://trmnl.com/integrations/stock-price) - [Stoic Quotes](https://trmnl.com/recipes/19343) - [Stripe](https://trmnl.com/recipes/89809) - [Stu Fractals](https://trmnl.com/recipes/15005) - [Subbly](https://trmnl.com/recipes/5083) - [Sudoku of the day](https://trmnl.com/recipes/42282) - [SunClock](https://trmnl.com/recipes/53263) - [Sundial - 24hr clock](https://trmnl.com/recipes/202560) - [Sunny 16](https://trmnl.com/recipes/23645) - [Sunrise & Sunset](https://trmnl.com/recipes/217936) - [Sunrise and Sunset](https://trmnl.com/recipes/29557) - [Sunrise and Sunset Chart](https://trmnl.com/recipes/195274) - [Supercars Drivers](https://trmnl.com/recipes/260558) - [Supercars Teams](https://trmnl.com/recipes/260533) - [Sweep clock](https://trmnl.com/recipes/187386) - [Swiss Transport Schedule](https://trmnl.com/recipes/26436) - [Sydsvenskan Latest News](https://trmnl.com/recipes/176724) - [TFL - Underground Line Status](https://trmnl.com/recipes/15955) - [TMB Alerts](https://trmnl.com/recipes/247650) - [TPLFVG](https://trmnl.com/recipes/53945) - [TRMNL Battery](https://trmnl.com/recipes/190136) - [TRMNL Battery Status](https://trmnl.com/recipes/29604) - [TRMNL Bounties](https://trmnl.com/recipes/18156) - [TRMNL Device Status](https://trmnl.com/recipes/195722) - [TRMNL Framework Updates](https://trmnl.com/recipes/277426) - [TRMNL Plugin Analytics](https://trmnl.com/recipes/286220) - [TTC Arrivals](https://trmnl.com/recipes/84336) - [TV Guide](https://trmnl.com/recipes/182680) - [TV Quotes](https://trmnl.com/recipes/214847) - [Tailscale](https://trmnl.com/recipes/108109) - [Taiwan Power Generation](https://trmnl.com/recipes/107839) - [Taiwan Reservoir Level](https://trmnl.com/recipes/94903) - [Tatort](https://trmnl.com/recipes/261844) - [Tautulli Now Playing](https://trmnl.com/recipes/117218) - [Tear-off Calendar](https://trmnl.com/recipes/92229) - [Tear-off Weather](https://trmnl.com/recipes/217346) - [Techmeme Headlines](https://trmnl.com/recipes/18207) - [Temperature Graph](https://trmnl.com/recipes/141444) - [Tempest Weather Station](https://trmnl.com/integrations/tempest-weather-station) - [Tesla (via Teslamate)](https://trmnl.com/recipes/41258) - [Tessie](https://trmnl.com/recipes/59557) - [TfL Bus Stops - Realtime Bus Arrivals](https://trmnl.com/recipes/234101) - [TfNSW Departures](https://trmnl.com/recipes/23278) - [The Daily Sarcasm](https://trmnl.com/recipes/139729) - [The New York Times](https://trmnl.com/recipes/64780) - [The Office](https://trmnl.com/integrations/the-office) - [The Onion - American Voices](https://trmnl.com/recipes/29537) - [The Onion - Editorial Cartoon](https://trmnl.com/recipes/32609) - [The Onion - Headlines](https://trmnl.com/recipes/29531) - [The Rickies](https://trmnl.com/recipes/97896) - [The Yetee Shirts of the Day](https://trmnl.com/recipes/83869) - [This Day in History](https://trmnl.com/recipes/9917) - [Three Word Clock](https://trmnl.com/recipes/25196) - [Tibber](https://trmnl.com/recipes/101453) - [Tibber Dashboard For NL, DE, SE & NOR](https://trmnl.com/recipes/188591) - [Tibber Norway](https://trmnl.com/recipes/135286) - [TickTick](https://trmnl.com/integrations/ticktick) - [Time Off Radar: Your Next Real Break](https://trmnl.com/recipes/266204) - [Tiny Planets](https://trmnl.com/recipes/189748) - [Today in geek history](https://trmnl.com/recipes/184719) - [Today's History in Panels](https://trmnl.com/recipes/184771) - [Today's Mealie Meal Plan](https://trmnl.com/recipes/34578) - [Today's Special](https://trmnl.com/recipes/95280) - [Today: Beautiful Date & Countdown](https://trmnl.com/recipes/250879) - [Today: Beautiful Weather](https://trmnl.com/recipes/270447) - [Todo List](https://trmnl.com/integrations/todo-list) - [Todoist](https://trmnl.com/integrations/todoist) - [Token FOMO](https://trmnl.com/recipes/192381) - [Toki Pona Word of the Day](https://trmnl.com/recipes/36529) - [Tokyo Metro Status](https://trmnl.com/recipes/176440) - [Top Android Apps](https://trmnl.com/recipes/176578) - [Top Cyber Threats Today](https://trmnl.com/recipes/259939) - [Top Hacker News Story](https://trmnl.com/recipes/17494) - [Top Severity CVEs Today](https://trmnl.com/recipes/153627) - [Top iOS Apps](https://trmnl.com/recipes/176265) - [Toronto Waste Collection](https://trmnl.com/recipes/102103) - [Trading212 Portfolio Viewer](https://trmnl.com/recipes/254858) - [TradingView](https://trmnl.com/recipes/157210) - [Trakt.tv](https://trmnl.com/recipes/245666) - [TraktWastedTime](https://trmnl.com/recipes/252905) - [Translink SEQ Network Status](https://trmnl.com/recipes/276612) - [Travel Time](https://trmnl.com/recipes/63494) - [Trellis Dashboard](https://trmnl.com/recipes/133373) - [Trello for Public Boards](https://trmnl.com/recipes/96319) - [Trending Movie Trailers](https://trmnl.com/recipes/198072) - [Trending Movies](https://trmnl.com/recipes/30888) - [Trending Recipes](https://trmnl.com/recipes/241645) - [Trending TV](https://trmnl.com/recipes/30916) - [Triangles - Generated Art](https://trmnl.com/recipes/220720) - [True or False?](https://trmnl.com/recipes/91747) - [TrueNAS recommended versions](https://trmnl.com/recipes/261592) - [Trump Quote](https://trmnl.com/recipes/113468) - [Tsukuba Express Status](https://trmnl.com/recipes/179047) - [Tweakers.net Dutch headlines unofficial (UPDATED)](https://trmnl.com/recipes/114675) - [Twitch Dashboard](https://trmnl.com/recipes/243915) - [Twonks Comics](https://trmnl.com/recipes/272414) - [UK Bus Departures (TransportAPI)](https://trmnl.com/recipes/221735) - [UK Bus Departures (bustimes.org)](https://trmnl.com/recipes/222328) - [UK Parliament Petitions](https://trmnl.com/recipes/228576) - [UK River Levels: Live](https://trmnl.com/recipes/228203) - [UK Tide Times](https://trmnl.com/recipes/113385) - [UK Train Departures (via Realtime Trains)](https://trmnl.com/recipes/38770) - [UK Trains](https://trmnl.com/recipes/232789) - [US Debt](https://trmnl.com/recipes/56095) - [US Stream Gauges](https://trmnl.com/recipes/198194) - [US Weather Maps](https://trmnl.com/recipes/154046) - [USGS Elevated Volcanoes](https://trmnl.com/recipes/178876) - [USPTO New Trademarks](https://trmnl.com/recipes/264269) - [Ultrahuman Ring](https://trmnl.com/recipes/260137) - [Un-stats](https://trmnl.com/recipes/246002) - [Unifi Site Manager](https://trmnl.com/recipes/102852) - [Unsplash](https://trmnl.com/integrations/unsplash) - [Upcoming Holidays (for specific country)](https://trmnl.com/recipes/91928) - [Upcoming Holidays Worldwide](https://trmnl.com/recipes/13875) - [Upcoming Movies](https://trmnl.com/integrations/upcoming-movies) - [Upcoming Tides (NOAA)](https://trmnl.com/recipes/98475) - [Updown.io](https://trmnl.com/recipes/94944) - [Uptime Kuma Statuspage](https://trmnl.com/recipes/159393) - [UptimeRobot](https://trmnl.com/recipes/98765) - [Urban Dictionary](https://trmnl.com/recipes/142466) - [Urban Dictionary Top Word](https://trmnl.com/recipes/90146) - [Useless Facts](https://trmnl.com/recipes/28722) - [Velo Station Info Antwerpen](https://trmnl.com/recipes/151444) - [Vienna Metro Departures (Wiener Linien)](https://trmnl.com/recipes/98534) - [Vienna Public Transport Disruptions](https://trmnl.com/recipes/250757) - [Visual Calendar](https://trmnl.com/recipes/218751) - [Visual Child Clock](https://trmnl.com/recipes/90656) - [Vlaamse Moppen](https://trmnl.com/recipes/281174) - [Vocaloid Lyrics Quote](https://trmnl.com/recipes/150439) - [Vox](https://trmnl.com/integrations/vox) - [Waifus](https://trmnl.com/recipes/246126) - [WakaTime Dashboard](https://trmnl.com/recipes/123718) - [WakaTime Stats](https://trmnl.com/recipes/241536) - [War and Peas Comic](https://trmnl.com/recipes/213996) - [Washington DC OPM Status](https://trmnl.com/recipes/203231) - [Washington Real-time Travel Times, Cameras, Alerts](https://trmnl.com/recipes/227171) - [Watchtower - Generated landscapes](https://trmnl.com/recipes/189444) - [Weather](https://trmnl.com/integrations/weather) - [Weather BYOS (Open Meteo)](https://trmnl.com/recipes/221669) - [Weather Chum](https://trmnl.com/recipes/49610) - [Weather Dashboard](https://trmnl.com/recipes/191027) - [Weather Glance](https://trmnl.com/recipes/181200) - [Weather Underground](https://trmnl.com/integrations/weather-underground) - [Weather XL](https://trmnl.com/recipes/194006) - [Webhook Image](https://trmnl.com/integrations/webhook-image) - [Webmentions](https://trmnl.com/recipes/203096) - [Week Number](https://trmnl.com/recipes/216687) - [Week Number](https://trmnl.com/integrations/week-number) - [Weekend Countdown](https://trmnl.com/recipes/191614) - [Weekly Calestenics](https://trmnl.com/recipes/113536) - [Weekly Meal Planner](https://trmnl.com/recipes/30089) - [Weeks of the Year](https://trmnl.com/recipes/209377) - [Weird Patents](https://trmnl.com/recipes/232425) - [What Country?](https://trmnl.com/recipes/234834) - [What to Wear Weather Forecast](https://trmnl.com/recipes/182853) - [WhatPulse](https://trmnl.com/recipes/218535) - [When is the next Apple Keynote?](https://trmnl.com/recipes/287989) - [Who's That Pokémon?](https://trmnl.com/recipes/27251) - [Wi-Fi/Wifi QR Code](https://trmnl.com/recipes/202908) - [Wifi Signal Strength](https://trmnl.com/recipes/191294) - [Wikipedia Articles](https://trmnl.com/integrations/wiki-random-article) - [Wikipedia Feeds](https://trmnl.com/recipes/148054) - [WildFires Tracker](https://trmnl.com/recipes/210345) - [Wildlife Observations by iNaturalist](https://trmnl.com/recipes/290545) - [Wildlife Sightings by Africam](https://trmnl.com/recipes/184141) - [Wingspan](https://trmnl.com/recipes/239594) - [Winnie the Pooh Quotes](https://trmnl.com/recipes/23139) - [Wisdom Project Quotes](https://trmnl.com/recipes/100671) - [Wisdom from the Bible](https://trmnl.com/recipes/19339) - [Withings](https://trmnl.com/integrations/withings) - [Word Clock](https://trmnl.com/recipes/19925) - [Word Clock Deutsch](https://trmnl.com/recipes/25205) - [Word Clock English](https://trmnl.com/recipes/25190) - [Word Clock Français](https://trmnl.com/recipes/25204) - [Word Clock 中文](https://trmnl.com/recipes/187631) - [Word Clock 日本語](https://trmnl.com/recipes/187661) - [Word Search](https://trmnl.com/recipes/189996) - [Word of the Day (Eng)](https://trmnl.com/recipes/33259) - [Word of the Day (Intl)](https://trmnl.com/recipes/33136) - [Work Chronicles Comic](https://trmnl.com/recipes/33461) - [Work Nudge](https://trmnl.com/recipes/160878) - [Workflowy](https://trmnl.com/integrations/workflowy) - [World Clock](https://trmnl.com/recipes/27940) - [World Clock](https://trmnl.com/recipes/66588) - [World Heritage Site of the Day](https://trmnl.com/recipes/260498) - [World Population](https://trmnl.com/recipes/239154) - [Wttr.in Weather](https://trmnl.com/recipes/149512) - [XKCD](https://trmnl.com/recipes/18267) - [XKCD Comic (with hover text)](https://trmnl.com/recipes/230540) - [XWeather](https://trmnl.com/recipes/138269) - [Year of the Day](https://trmnl.com/recipes/148248) - [Yearly Cycle Clock](https://trmnl.com/recipes/219958) - [Ynet Breaking News](https://trmnl.com/recipes/55524) - [Yoda Says](https://trmnl.com/recipes/165968) - [YouTube](https://trmnl.com/integrations/youtube-analytics) - [YouTube Channel's Latest Video](https://trmnl.com/recipes/149148) - [YouTube Creator Award](https://trmnl.com/recipes/198498) - [Your Life in Weeks](https://trmnl.com/recipes/22415) - [Youtube - Recent Uploads](https://trmnl.com/recipes/30043) - [Zen Quotes](https://trmnl.com/recipes/19353) - [Zmanim](https://trmnl.com/recipes/122757) - [air-Q](https://trmnl.com/integrations/air-q) - [badapple](https://trmnl.com/recipes/60000) - [beehiiv](https://trmnl.com/integrations/beehiiv) - [info.sh](https://trmnl.com/recipes/45647) - [oghunt](https://trmnl.com/integrations/oghunt) - [pISS Tank Watch](https://trmnl.com/recipes/215157) - [poste](https://trmnl.com/integrations/poste) - [ÜSTRA Status: Aufzüge & Rolltreppen](https://trmnl.com/recipes/221170) --- # Device / WiFi troubleshooting Below is a work-in-progress collection of potential issues + fixes to ensure your TRMNL device remains connected. # Your time is valuable If the steps below are confusing or not aligned with your experience, [find your MAC address](https://help.trmnl.com/en/articles/10614205-finding-your-trmnl-mac-address) and send us a live chat (click the bottom right icon). You can also try our [Find My Device](https://trmnl.com/find-my-device) tool to resolve issues without waiting on our team. # Initial Setup Issues If you've just unboxed your TRMNL, see the following tips. ### WiFi portal closing unexpectedly On your phone or computer, click "forget network" (or similar feature), then restart your device and re-connect to TRMNL WiFi. **Using a VPN?** This should be fine, but we suggest disabling your VPN during initial device setup. ### WiFi network not listed Like many IoT devices, TRMNL requires a 2.4ghz network. If you only have 5ghz enabled on your router, you can usually enable dual band with no changes required for your existing devices. It's ideal to have a separate, dedicated 2.4ghz network versus a "dual band" network, because routers with dual-band enabled will often instruct devices to use the 5ghz band, which is incompatible with most IoT devices. ### My network has a captive portal If you're setting up TRMNL in a building with "hotel style" WiFi, whereby you must input credentials inside a captive portal popup following network connection, TRMNL must be accompanied by a middleware device. [Check out our article on setting up your TRMNL through a captive portal with the help of a travel router](https://intercom.help/trmnl/en/articles/11663377-setting-up-a-trmnl-on-tricky-wi-fi-situations). Universities may offer a device network if you register the MAC address first. For example, [Carnegie-Mellon University](https://www.cmu.edu/computing/services/endpoint/network-access/wireless/how-to/cmudevice.html). **WiFi network connects, then portal appears again** If you're able to connect to the TRMNL network, input your local network SSID + Password, then click "Connect," but the screen just refreshes and goes back into pairing mode, email your device's MAC to or start a live chat with us for further debugging. You can find your MAC [here](https://help.trmnl.com/en/articles/10614205-finding-your-trmnl-mac-address). ### Wifi network not connecting If you're inputting your home or office network's WiFi credentials into the [TRMNL WiFi portal](https://intercom.help/trmnl/en/articles/9416306-how-to-set-up-a-new-device), but the device is not connecting or showing an error screen, check out the security settings on your router. While TRMNL should work with WPA, WPA2, and WPA3, some users have reported that only WPA3 (or WPA3 Personal) connections were successful for their device. We have seen issues with "WPA2/WPA3 Personal." Another thing to check is whether loads on your local network, incognito. Just use your phone (on WiFi) or computer and visit the URL. If it doesn't load, your router or ISP is blocking the endpoint that our Firmware pings for screen content. **Note: do not use ** to operate your device. It will not work. In this case, you can simply modify the firmware to ping our actual URL, , by choosing "Custom Server" from the WiFi setup step and inputting "". To reproduce this setup step, hold the circular button on the back of your TRMNL for 5-7 seconds, then let go. You may also need to turn the device off, wait 5 seconds, then turn it back on (after performing a hold-reset). **I have T-Mobile internet / phone** For some reason, we recently learned T-Mobile might block `tmrnl.app` from device access. If you have access to a VPN, try running your router traffic through it temporarily to see if that brings your TRMNL online. This is not a long-term solution, but will help us debug further and offer alternative solutions when you send us a live chat or email. **Channels and Bandwidth** The ESP32 is most stable on channels **1–11** in the 2.4 GHz band. Unusual bandwidth settings (like 40 MHz or dynamic channel selection) can cause erratic behavior. **AP (Client) Isolation** Some routers enable “AP Isolation” or “Client Isolation,” which prevents devices on the same Wi-Fi network from communicating with each other. **Disable AP Isolation in your router’s advanced wireless settings.** **Static IP Address** As of firmware 1.7.5, you can now set a [static IP address](https://help.trmnl.com/en/articles/13673576-device-static-ips) for your device. This can help if your DHCP server has re-used an IP with firewall restrictions/customization that is interfering with your device's access. # Configuring Your Access Point or Router Using your phone's hotspot feature is a way to isolate the device from your home network and confirm it is operating properly, because hotspots are much less complex than home or commercial access points or routers. First, we'll perform a **soft reset** to erase all saved SSIDs/networks. - Hold the button on the back of the TRMNL for 5-7 seconds to re-enter WiFi pairing mode - Connect to TRMNL network and in the captive portal click ADVANCED > Soft Reset (this clears all SSIDs from memory) If you are unable to get the device responding properly after trying multiple times to re-enter WiFi pairing mode, you can also do a [firmware flash](https://help.trmnl.com/en/articles/10271569-manually-flash-firmware) (cable required) which we consider a **hard reset**. - Reconnect to TRMNL network and this time, instead of connecting to your regular WiFi, put your cell phone in hotspot mode and connect it to that. Here are some instructions: - For Android: - Make sure your hotspot is configured in 2.4Ghz mode! Android Settings > network > hotpsot > wifi hotspot > extend compatibility - For iOS: - Make sure you use "Maximum Compatibility Mode"! If everything works fine, congratulations, you've done the hardest troubleshooting step, you've isolated the problem! ## Using AI to Configure Your Local Network With a few exceptions, this guide focuses on broad issues that affect multiple hardware network devices. To configure **your specific hardware router or access point**, you can leverage your favorite LLM (AI) using the following phrase: "*How do I configure my BRAND AND MODEL NAME to be compatible with IoT devices using only 2.4Ghz*." Insert your network device's name/model, such as `ASUS WRT600N`. # Ongoing Issues If you've already set up your TRMNL but are experiencing new issues, see the following tips. We will update this as often as we detect a new pattern. For live help, open the chat widget in the bottom right corner and ping us in real time. Be ready to share your router brand / model or other home networking settings to help us help you. ### Firmware update looping / slow Our firmware packages are only around 1.4mb, however it may take a few minutes for the package to be installed on your device following a download. We do not recommend clicking the boot button to 'skip' this update, or turning your device off/on. If you prefer to disable firmware updates, simply visit [Devices > Edit](https://trmnl.com/devices) > scroll down and uncheck the "OTA Updates" feture. Note that this requires the Developer edition perk, but if you don't have that enabled then you can send us a live chat and we'll do it on your behalf. ### Intermittent content vs error screen If your TRMNL sometimes displays content as expected, but at other times shows an error screen, it's possible the buffer needs to be reset. Simply turn off your device for 5-7 seconds (this length of time is necessary), then turn it back on. ### Error screen: "wifi not connected" or "content malformed" Suggestion: If you have ad blockers installed at the router-level, try whitelisting the domain "". This is the endpoint where the device firmware fetches new content. Suggestion: Set your Device's refresh rate (visit [Devices > Edit](https://trmnl.com/devices) > *Battery & Sleep* section) to 5 minutes, then click the button on the back of your device 1x. This should sync your new refresh rate. Then, wait 5 minutes for the device to perform a full refresh cycle (sleep, wake, request content). Suggestion: To help us confirm if this is a local network issue (vs device / firmware related), try connecting your TRMNL to your phone's hotspot. If you're using an iPhone > Personal HotSpot, make sure to toggle on "Maximize Compatibility" (beneath your editable WiFi Password field). ### Device error log - wifi connection failed, current WL Status: 6 If you have Developer Edition unlocked, you can view device logs via > *Developer Perks* section > scroll down to Logs. If the error code is "WL Status: 6", check out your router's Channel settings. One user reported that their 2.4ghz band was set to Channel 9 (auto), but changing to Channel 1 resolved their intermittent disconnections. ### WPA2/WPA3 Encryption When using the TRMNL (OG) model, if your router configuration offers *WPA2/WPA3-PSK and SAE encryption*, you will need to switch to ***WPA2-PSK only***. ### Maybe it's a DNS Problem? See our [DNS article](https://help.trmnl.com/en/articles/12386067-dns-and-network-requests-for-firewall-whitelists). ## TP-LINK user? Even after setting up distinct 2.4Ghz and 5Ghz SSIDs, if you still struggle to connect your TRMNL device use the "guest network" feature on the router with "WPA2/WPA3-Personal" security. ## Eero mesh customer? Disable connection steering for your TRMNL to prevent 5ghz or faraway access point connections upon device wakeup. You can also [temporarily hide the 5Ghz band](https://support.eero.com/hc/en-us/articles/360049983772-How-Do-I-Temporarily-Hide-the-5GHz-Band-on-My-eero-Network) during setup. ## Orbi mesh customer? Your standard setup may include a dual-band 2.4 / 5ghz network. However some users have reported that they needed to enable a 2.4ghz-only band for their TRMNL connection to remain stable. ## Fritz! Fritzbox (Fritz!OS 8)? In this update, `PMF` (Protected Management Frames) was automatically enabled, causing issues with TRMNL connectivity. Disabling this feature on the 2.4Ghz band can resolve the issue. There may also been some additional features set at factory based on [this article](https://fritz.com/en/apps/knowledge-base/FRITZ-Box-7520/3481_Does-the-FRITZ-Box-support-WPA3). ## Ubiquiti / Unifi customer? Our friends at HostiFi wrote [this article](https://support.hostifi.com/en/articles/7019454-unifi-recommend-settings-for-iot-devices) with tips to ensure iOT devices like TRMNL play nicely with your network. Without a proper setup, your router's default feature "Band Steering" will direct TRMNL devices to leverage the 5ghz band which will not work, even if you connected to 2.4ghz during setup. Below are some router settings from 2 users who fixed their connectivity issues. They have a 2.4ghz-only SSID with multiple IoT devices. Setup A: ​Setup B: If some of the above parameters don't work, try setting just your router's "Minimum Data Rate Control" setting to a minimum of 12mbps on the 2.4ghz network. If you've tried everything above, and are using WPA2, try changing to WPA3 Personal. ### Intrusion Prevention Detection One user had 4 OG units, and Unifi chose to single out one unit that caused failure after WiFi setup (no further connectivity to trmnl.app): "Unifi flagged the device as a threat, via its Intrusion Prevention Detection ([IPS](https://help.ui.com/hc/en-us/articles/360006893234-UniFi-Gateway-Intrusion-Detection-and-Prevention-IDS-IPS)). It allowed the device to connect to the network but dropped all packets." ## Congestion and Compatibility Two things to look at: - Ensure you are not heavily overlapping with a neighbour's channel, which can cause issues if it's massively congested. - Also, in the *Settings* -> *WiFi,* if you click on your network, you can see there is an *advanced* section below where you can change the compatibility. Unify likes to up this so it's not as compatible with low-speed devices like the ESP's. You can manually alter this to **low density**. You can make a 2.4Ghz only network and also an IOT network where the settings are lower density, so it will go as low as 1 Mbps. ## Does the device support WiFi feature XXX? TRMNL (OG) uses an [ESP32-C3's wifi module](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-guides/wifi-security.html). # What else? In general, device connectivity issues are a mix of local networking / router quirks and some ongoing compatibility improvements being made to our open source firmware: So if your device is having issues, rest assured that we can probably fix it with a little bit of back and forth debugging, and your hardware is in good condition otherwise. (*As of February 2026 we've shipped 30,000+ devices. Only 1x had a broken WiFi receiver on the board, which we immediately replaced at no cost to the customer.*) --- ***Unhappy with this article?*** *Let us know what you were searching for and why this article missed the mark for you. Email [support@trmnl.com](mailto:support@trmnl.com?subject=Device%20WiFi%20troubleshooting%20Article%20Feedback) and let us know (use the link to automatically include the article title)!* --- # International Customs / Duty Fees TRMNL holds [several EC licenses](https://help.trmnl.com/en/articles/11327036-hardware-specs-for-custom-authorities) and is permitted to sell in many countries around the world. All devices are assembled + shipped from our warehouses in Atlanta, Georgia (USA) and Berlin, Germany. # Estimating duty fees Orders to Europe are sent from our Berlin warehouse, and EU residents enjoy $0 Duty fees as well as free shipping. In addition to EU residents, a handful of other regions do not charge Duty fees: - Australia - European Union - Hong Kong - New Zealand - Philippines - (+ several others) But residents in *some* countries are not so lucky. Here are estimated fees (USD) in a handful of popular TRMNL customer locations. Please note that these fees are mostly for single OG devices. We are collecting more data now that we are shipping the new X device across the globe. - Canada, $13.50 pre April 2025 (May 2025: $33.51 CAD in Ontario; $38.85 CAD in Québec; $59 CAD in Calgary; $70 CAD in Montreal) - United Kingdom, from [$0](https://www.reddit.com/r/trmnl/comments/1k0j76z/comment/mnf32wg/) to 41 GBP (for OG); we are currently seeing higher fees reported, possibly due to [new legislation in the UK](https://www.reddit.com/r/smallbusiness/comments/1r8yez4/changes_to_duty_selling_from_uk_to_eu_how_are_you/) - Israel, 145 ILS ($45) - India, 7800₹ ($82 for an OG) - United Arab Emirates (UAE): Imports are subject to local charges upon delivery (typically 5% VAT and around 5% customs duty on electronics). The recipient is responsible as the Importer of Record, and these fees are collected by the courier. The exact amount paid at arrival time may be lower or higher pending the quantity of devices you ordered and if you included add-ons. For reference, TRMNL devices ship with description "[Case Color] E-ink display," category "Accessory - with battery," and HS Code 85171400. Finally, when you receive a notice from your local courier with a commercial invoice and "Declared value," note that this declared value is the total of the goods -- not the Duty fee. # Optimizing duty fees We don't include the value of digital products in your order. For example, if you purchase the Developer Edition add-on, this is a software upgrade and will reduce your Customs value. # Paying duty fees Some customers may prefer to pre-pay their Duty fee to avoid the hassle of making a separate payment in-person at a local post office. But this is not always an optimal decision, as some couriers charge a large "courier handling fee" of $7-28 for this pre-pay privilege. Other customers prefer to pre-pay due to their in-office job or travel schedule. In our experience, post offices typically hold packages for days or weeks before returning them to TRMNL. Additionally, some local couriers allow Duty fees to be paid online, or they include a payment slip during delivery and allow the recipient to pay fees later. --- # Serverless We welcome advanced community plugins. But sometimes they require authors to host or maintain their own middleman services. Using TRMNL Serverless you can achieve the flexibility of 3rd party tools (e.g. Cloudflare Workers) without spinning up services or paying for subscriptions. *The legacy version of this feature is called [Transformer](https://help.trmnl.com/en/articles/12996946-parsing-plugins-with-the-sandbox-runtime), and it will be replaced entirely by Serverless in the near future.* ## Quickstart 1. Visit [Plugins > Private Plugins > New](https://trmnl.com/plugin_settings/new?keyname=private_plugin), provide a name and click Save 2. Click Edit Markup, then click the Serverless tab 3. Select a programming language and build whatever you want* **network access is included!* Code provided in the Serverless text editor must include a `run` function that returns a hash / dictionary / JSON friendly object. For example... **Ruby** Includes `httparty` gem for network requests. Must `require` in your code. require 'httparty' def quotes HTTParty.get('https://dummyjson.com/quotes?limit=10')['quotes'] end def run(input) { input:, quotes: } end **Node** Includes `fetch()` module for network requests. async function run(input) { const res = await fetch("https://dummyjson.com/quotes?limit=1"); const data = await res.json(); return { quote: data.quotes[0] }; } ​**PHP** Includes standard curl extension for network requests.​ ​ function run($input) { $ch = curl_init("https://dummyjson.com/quotes?limit=1"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true); unset($ch); return ["quote" => $data["quotes"][0]]; } ​**Python** Includes `Requests` library for network request. Must `import` in your code. ​ import requests def run(input): data = requests.get("https://dummyjson.com/quotes?limit=1").json() return {"quote": data["quotes"][0]} ## Accessing existing variables Global variables like `#{{ trmnl }}` , as well as [custom form field](https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder) values, are available inside the parameter of your `run` function. We suggest `input` but it may be whatever you prefer. ## Frequently Asked Questions *Can I refactor my existing (published) plugins to use this feature?* Please don't, at least not until April 2026. Because this feature is in private beta, downstream installations won't have access to the runtime. *Can I install additional packages into this VM?* Yes, but it's currently a manual process. Send us a live chat or email with a particular package / gem / library you might need, and we can add it to the default image for your preferred language. For example, the Ruby image already has `HTTParty` included for simple network requests. *What are the specs on this runtime?* You get 128mb and 5 seconds to do your business. ## Troubleshooting We strongly encourage you to enable [Debug Logs](https://help.trmnl.com/en/articles/11586187-debugging-private-plugins) from your private plugin settings page while developing Serverless plugins. This will return native errors, for example: let data = await fetch('https://dummyjson.com/quotes?limit=5') ^^^^^ SyntaxError: await is only valid in async functions and the top level bodies of modules --- # Connect your Device to Terminus (BYOS) ## Step 0 - What is Terminus [Terminus](https://github.com/usetrmnl/byos_hanami) is the TRMNL-maintained [Bring Your Own Server](https://docs.trmnl.com/go/diy/byos) (BYOS) solution for self-hosting. It offers a convenient one-line script for installing with Docker in your environment as well as other options outlined in the [GitHub Repository](https://github.com/usetrmnl/byos_hanami). You can follow these steps with any BYOS solution. ## Step 1 - Turn your Device On Flip the power switch to ON from the back of your TRMNL device (TRMNL OG) or attach the charging dock to the back of the unit (TRMNL X). After it wakes up and blinks a few times, your screen might look something like this: If nothing happens when you turn on the device, the battery may be drained. In this case, plug TRMNL in with the included USB-C cable (if part of your order). Restart your device using the OFF/ON switch on the back (TRMNL OG). ## Step 2 - Connect to WiFi As the device screen says, use your phone or a nearby computer to find and connect to the WiFi network which includes the name "TRMNL" which will broadcast from your device. After connecting, a WiFi portal will appear (your WiFi credentials are [never](https://docs.trmnl.com/go/how-it-works#opinionated-device-less-than-greater-than-server-relationship) sent to our servers). Click **Advanced** > **Custom Server** > **Yes** to expose the URI for your BYOS server. Add the full path to your server as set up and displayed in the Terminus Dashboard. Do not include a trailing slash. # bad http://192.68.0.2:2300/ # good http://192.168.0.2:2300 Once entered, click **Back to Wi-Fi**, select your SSID, and enter your network password. Finally, click **Connect** to complete setup by sending these credentials to your device. **Troubleshooting** For suggestions to fix a few rare issues, see our [troubleshooting guide](https://help.trmnl.com/en/articles/10193157-device-wifi-troubleshooting). ## Step 3 - Device Registration If all information is correct, your device will register with Terminus and display the following message, or something similar, on the screen: Congratulations, you're ready to configure the device in Terminus! ## Switching From BYOS to TRMNL core If you have configured your device as above, but want to switch back to trmnl.com as your server, you will need to follow a few steps. 1. [Enter WiFi Pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode) again. 2. Reconnect to the TRMNL SSID and, under **Advanced**, perform a *Soft Reset.* 3. When the device restarts, follow the normal [setup guide](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device). **Note:** If the portal still shows an API Server URL, you should clear that field completely before clicking **Connect**. When blank, it defaults to TRMNL's server. ### Note about Ngrok About 1 year ago Ngrok added a splash page to the ~1st HTTP request sent to their server (per Origin IP). so it's necessary to follow the instructions to add a skip header or click through on the page themselves in a web browser, then retry the API requests. --- # Replacement parts We agree with iFixIt - **you have the right to repair**. Below are a few ways we can help you maintain a functional device, forever. **Quick Links:** - [TRMNL OG](#h_7a9101347e) (7.5") - [TRMNL X](#h_075d03a2aa) (10.3") - [Other Accessories](#h_c93a1ccd53) # TRMNL OG ## EPD (screen) We'll sell you one for $50 (including shipping), or you can get [this waveshare one](https://amazon.com/dp/B075R69T93/) in US Store or this [UK Version](https://www.amazon.co.uk/dp/B0BD5NQPH4). Email and see our [disassembly](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl) + [assembly](https://help.trmnl.com/en/articles/10003236-how-to-assemble-your-trmnl) guides. ## Case enclosure Have access to a 3D printer? Check out the open source designs [here](https://github.com/usetrmnl/mounts). Alternatively we'll send you a native case for $10 + shipping. Email and see our [disassembly](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl) + [assembly](https://help.trmnl.com/en/articles/10003236-how-to-assemble-your-trmnl) guides. ## Battery TRMNL (OG) maximum space for the battery is **62x31x10mm** and uses a JST-GH (1.25mm pin) configuration. Unfortunately we can't send "loose" batteries, but [this one](https://www.amazon.com/dp/B0BHQWMKVN/) (1000mAh) will fit. Alternatively [this option](https://www.amazon.com/dp/B0DXSJLHRB) provides 3x the power (3000mAh), but does not fit in the native TRMNL enclosure. If you prefer a longer battery life, consider pairing the larger battery with one of our many [3D printable case designs](https://github.com/usetrmnl/mounts). Watch [this assembly video](https://www.youtube.com/watch?v=Z64FDqIaKpg) for installation steps. ### Additional EU Battery Options This 1200mAh option would be fit and work properly: **** This large capacity version would be a very tight fit, it already has a proper connector, a large capacity, and an integrated protection board **** ### Additional US Battery Options This 2000mA option would be fit and work properly: **** ## PCB (main board) We'll send you one for $10 (plus shipping). Email and watch [this assembly video](https://www.youtube.com/watch?v=Z64FDqIaKpg) for installation steps. We will also flash the latest firmware on your behalf, or you can do so in 1 click [here](https://trmnl.com/flash). ## Kickstand We'll send you a new one for free, plus a little help for postage. Email and [follow these steps](https://help.trmnl.com/en/articles/11360753-how-to-replace-your-kickstand) to replace it. ## PCB Screws Our OG model uses `M3-0.5 x 4mm` screws to attach the PCB to the back enclosure. Get more with the same specs [here](https://www.amazon.com/dp/B0C1SGKGWZ). # TRMNL X ## EPD We can sell you a replacement screen for $70. We currently prefer for the device to be shipped back to us since we don't have re-assembly guides available yet. ## PCB Screws All screws are torx, stainless steel, magnetic - **Case**: `M2x8` conical head x12 - **Charging Dock**: `M2x5` conical head x4 - **Kickstand**: `M2.5x10` flat-head x2 - **PCB** (internal): `M2.5x4` flat-head x5 ***Note: The above image has imperfectly aligned screws which can lead to dock connectivity issues.*** # Other accessories **USB-C** Any cable with power + data should work, but we can sell you one for $5: ​ **Microfiber cloth / Anti-glare film** We'll send you a new one for free, plus a little help for postage. Email . --- # Portrait mode With the release of the TRMNL X, we officially support portrait mode for our native plugins and require compatibility for all community recipes released since January 17, 2026, however hundreds of plugins published prior were updated to support the layout. To change your device's orientation, go to your **Device Settings** > **Color & Rotation** > **Screen Rotation** ([quick link for your current device](https://trmnl.com/devices/current/palette/edit)) Change the setting to **Portrait** and *save*. Once changed, the backend screen generator still needs to generate screens of your playlist items in the new orientation. This will happen automatically based on the plugin's refresh rate, or for private plugins and installed recipes you can use the **Force Refresh** feature on the plugin's settings page to expedite the process. ### I Don't See the Screen Rotation Option If it is not present, your device does not currently support portrait mode. This is most common with smaller screen sizes, like the TRMNL OG or smaller. ## Using CSS to mimic portrait mode This section is being kept for reference only and is **NOT** recommended. First, build a plugin. This can be a Recipe, private plugin, or third party instance. Learn more about each type [here](https://help.trmnl.com/en/articles/10546870-compare-custom-plugin-types). Next, inside your markup, simply add the `transform` CSS declaration like so: transform: rotate(90deg); Here's some working sample code:
First Time Around

First Time Around is a one-disk DVD by Randy Bachman and Burton Cummings recorded in 2006 at CBC Studios in Toronto, Canada, by CBC. It was originally shown on CBC in April 2006, but was later released as a DVD with extended footage of the concert. The concert has 20 tracks of songs by Bachman-Turner Overdrive, Burton Cummings, The Guess Who and cover versions of artists such as Sting and Jimi Hendrix. It was originally shown on CBC in April 2006, but was later released as a DVD with extended footage of the concert. The concert has 20 tracks of songs by Bachman-Turner Overdrive, Burton Cummings, The Guess Who and cover versions of artists such as Sting and Jimi Hendrix Sting and Jimi Hendrix. It was originally shown on CBC in April 2006, but was later released as a DVD with extended footage of the concert. The concert has 20 tracks of songs by Bachman-Turner Overdrive, Burton Cummings, The Guess Who and cover versions of artists such as Sting and Jimi Hendrix Sting and Jimi Hendrix. It was originally shown on CBC in April 2006, but was later released as a DVD with extended footage of the concert. The concert has 20 tracks of songs by Bachman-Turner Overdrive, Burton Cummings, The Guess Who and cover versions of artists such as Sting and Jimi Hendrix.

Which looks like this in TRMNL's live preview markup editor: And here's the completed render: --- # How to set up a new device [Deutsch](https://help.trmnl.com/en/articles/12593804-so-richtest-du-dein-neues-gerat-ein) Congrats on getting a TRMNL! We're stoked to have you. ***Your time is valuable*** If the steps below are confusing or not aligned with your experience, [find your MAC address](https://help.trmnl.com/en/articles/10614205-finding-your-trmnl-mac-address) and send us a live chat (click the bottom right icon). # Step 1 - Set up your device Click on your model below for detailed instructions. **Quick Links:** - [TRMNL OG (7.5")](#h_5fee15b4bf) - [TRMNL X (10.3")](#h_d8288edec7) ## TRMNL OG (7.5" screen) Welcome aboard! TRMNL's OG device is our bestseller. Some may even call it a classic. If your box includes a clear film accessory, [see here](https://intercom.help/trmnl/en/articles/10217443-applying-the-screen-anti-glare-protector). Otherwise, let's begin. ### Turn on TRMNL Flip the power switch to ON from the back of your TRMNL device. After it wakes up and blinks a few times, your screen might look something like this: If nothing happens when you turn on the device, the battery may be drained. In this case, plug TRMNL in with the included USB-C cable (if part of your order) and restart your device using the OFF/ON switch on the back. ### Connect to WiFi As the device screen says, use your phone or a nearby computer to find and connect to the WiFi network named "TRMNL," which will broadcast from your turned on device. After connecting, a WiFi portal will appear like the middle screenshot above. Wait a moment while TRMNL scans for available networks, or provide credentials into the SSID/Password box directly if your home/office network doesn't appear or is hidden. Finally, click Connect to share these credentials with your TRMNL device. (*Your WiFi credentials are [never](https://docs.trmnl.com/go/how-it-works#opinionated-device-less-than-greater-than-server-relationship) sent to our servers*) Having trouble on corporate or enterprise networks? [See this guide](https://intercom.help/trmnl/en/articles/11663377-setting-up-a-trmnl-on-tricky-wi-fi-situations). **Troubleshooting** For suggestions to fix a few rare issues, see our [troubleshooting guide](https://help.trmnl.com/en/articles/10193157-device-wifi-troubleshooting). ### Register your Device After adding your WiFi credentials, TRMNL will restart and show a setup message. If your screen is blurry or you can't read the 6 digit ID, use [Find My Device](https://trmnl.com/find-my-device) to recover it instantly. If you *already have* a TRMNL account, [Add a New Device](https://trmnl.com/devices/new) with this Friendly ID. If you *don't* have a TRMNL account (different from your store profile), register at and provide this Friendly ID in the required field. After signing up, your device should begin working within moments. TRMNL adds a default plugin to all new accounts, which you're welcome to delete from your Plugins tab inside the web application. --- ## TRMNL X (10.3" screen) Welcome aboard! The X is our latest release, incorporating feature requests and key learnings from thousands of OG model owners. Let's begin. ### Turn on TRMNL Your device should have a welcome screen, likely prettier than this one. Remove the circular charging dock from your box and attach it to the back of your display. It has a strong magnet and should snap on without effort. Next, plug a USB-C cable into the dock. Plug the other end of your cable into any power source. You may also attach the kickstand, in which case the USB port is behind the kickstand's hinge. Within a few seconds your device should boot up. If nothing happens, try rotating your dock a few degrees (spin it like a disk) and wait a few more seconds. ### Connect to WiFi Your device should be showing a screen like this. As it reads, use your phone or a nearby computer to find and connect to the WiFi network named "TRMNL." After connecting to the TRMNL network you'll see a sequence of screens like this. Wait a moment while TRMNL scans for available networks, or provide credentials into the SSID/Password box directly if your home/office network doesn't appear or is hidden. Click Connect to share these credentials with your TRMNL device. (*Your WiFi credentials are [never](https://docs.trmnl.com/go/how-it-works#opinionated-device-less-than-greater-than-server-relationship) sent to our servers*) Having trouble on corporate or enterprise networks? [See this guide](https://intercom.help/trmnl/en/articles/11663377-setting-up-a-trmnl-on-tricky-wi-fi-situations). **Troubleshooting** For suggestions to fix a few rare issues, see our [troubleshooting guide](https://help.trmnl.com/en/articles/10193157-device-wifi-troubleshooting). ### Register your Device After adding your WiFi credentials, TRMNL will restart and show a setup message. If your screen is blurry or you can't read the 6 digit ID, use [Find My Device](https://trmnl.com/find-my-device) to recover it instantly. If you *already have* a TRMNL account, [Add a New Device](https://trmnl.com/devices/new) with this Friendly ID. If you *don't* have a TRMNL account (different from your store profile), register at and provide this Friendly ID in the required field. After signing up, your device should begin working within moments. TRMNL adds a default plugin to all new accounts, which you're welcome to delete from your Plugins tab inside the web application. # Step 2 - Connect Plugins While logged into TRMNL, visit the [Plugin directory](https://trmnl.com/plugins) to add content to your device. Your TRMNL will rotate through these plugins based on your [Playlist Schedule](https://help.trmnl.com/en/articles/11663305-playlist-scheduler) and [refresh intervals](https://help.trmnl.com/en/articles/10113695-how-refresh-rates-work). You can also force-skip to new screens at any time. How to force skip: - TRMNL OG - click the button on the back of your device - TRMNL X - tap anywhere on the touch bar (best results in the middle) Additional device button features can be configured as [special functions](https://help.trmnl.com/en/articles/9672080-special-functions). # Step 3 - Enjoy! TRMNL keeps you focused but informed, aware but not distracted, and in sync, async. Feel free to modify the refresh rate, enable sleep mode, and more from the [Devices > Edit](https://trmnl.com/devices/current/edit) page in your TRMNL account. If you have questions about the setup process, send us a live chat (bottom right corner of this page). --- ***Unhappy with this article?*** *Let us know what you were searching for and why this article missed the mark for you. Email [support@trmnl.com](mailto:support@trmnl.com?subject=How%20to%20set%up%20a%20new%20device%20Article%20Feedback) and let us know (use the link to automatically include the article title)!* --- # Enable WiFi Pairing Mode TRMNL supports multiple WiFi credential pairs, all of which is stored locally on your device and never sent to the cloud or your [BYOS server clients](https://docs.trmnl.com/go/diy/byos). If you change network settings or moved your TRMNL between locations, you might need to update the WiFi configuration. Select your device for instructions to reset WiFi. Quick Links: - [TRMNL OG](#h_26e0b8775e) - [TRMNL X](#h_c231afff3a) # TRMNL OG (7.5" device) Resetting WiFi on the OG is a simple process. ## Manually Enter Pairing Mode With your TRMNL device already turned on, hold the boot button on the back of your TRMNL for 6-8 seconds, then let go and it should enter WiFi pairing mode. You can also try holding the button for 15-20 seconds if this shorter value doesn't work. ## Set a Macro for Pairing Mode If you anticipate frequent WiFi or location changes for your TRMNL, consider setting a [special function](https://help.trmnl.com/en/articles/9672080-special-functions) so make your device enters WiFi pairing whenever you medium-press (1-2s hold) on the boot button. ## Default approach to Pairing Mode If your device has become unresponsive, turn it off for 10 seconds to reset the buffer. Then turn it back on. If the network SSID is not found, it should re-enter pairing mode. --- # TRMNL X (10.3" device) See this video or follow the steps below it. ## Manually Enter Pairing Mode [https://www.youtube.com/embed/aXiA_liuCvI?rel=0](https://www.youtube.com/embed/aXiA_liuCvI?rel=0) ### Steps from the video Hold both the left and the right side of the touchbar until the screen blinks: Tap the middle to cancel, or hold the middle to enter pairing mode: --- # Put your TRMNL in "flashing mode" For a TRMNL PCB to accept data transfer via a USB-C cable, it needs to be in "boot mode," also sometimes called flashing mode. **Quick Links:** - [TRMNL OG](#h_152eb7260b) - [TRMNL X](#h_96e07a072e) - [Seeed Studio](#h_61ce847adc) # TRMNL OG If you have a 7.5" native TRMNL device... ​ ## **Basic method** 1. Plug a data enabled USB-C cable into your TRMNL. 2. Turn off the TRMNL. 3. While pressing down on the circular ("boot") button below the power switch, turn the device back on. 4. Let go of the boot button. ## **Stubborn method** (if Basic does not work) 1. Turn off the device. 2. Hold down on the circular ("boot") button below the power switch. 3. While pressing down, plug a data enabled USB-C cable into your TRMNL. 4. Keep pressing the boot button and turn the device back on. 5. Let go of the boot button. If the above methods both fail, turn off your device for 10-15 minutes and try again. There may be a static charge or heat buildup that needs to dissipate. --- # TRMNL X This model has 2x modes that bypass the firmware, "[reset](https://help.trmnl.com/en/articles/12407673-troubleshooting-an-unresponsive-device#h_ee5e7cb9ad)" and "flashing." ## Flashing / Boot Mode Rotate your charging dock so the flat (back) side is facing the back of your TRMNL. Then, "hover" the dock from the bottom left corner to the middle, and plug it back in. A USB-C cable should also be attached if you are flashing new firmware from or your own development environment. Here's a couple angles in action. First, from above: [https://www.youtube.com/embed/D7p5Yk-fOuA?rel=0](https://www.youtube.com/embed/D7p5Yk-fOuA?rel=0) Next, from the side: [https://www.youtube.com/embed/fF1LuqHe9t4?rel=0](https://www.youtube.com/embed/fF1LuqHe9t4?rel=0) Complete this motion at a 2-3 second speed. If you do it too quickly, each of the 2x reset points may not recognize the magnet inside your dock. **Reminder**: You will then need to connect your USB-C cable to the dock and the dock to the back of your device so that the pins connect to the TRMNL X so it can be [flashed](https://trmnl.com/flash) from your computer. If the device is not recognized (`USB JTAG/serial...`) when you click **Connect** on the flashing page, it is *not* in Boot Mode or does not have a good connection through the dock. - **We have a guide for [how to get a good connection](https://help.trmnl.com/en/articles/12407673-troubleshooting-an-unresponsive-device#h_d6c1eff6f6).** - **Soft Reset / Restart device:** [Perform a soft reset](https://help.trmnl.com/en/articles/12407673-troubleshooting-an-unresponsive-device#h_ee5e7cb9adhttps://help.trmnl.com/en/articles/12407673-troubleshooting-an-unresponsive-device#h_ee5e7cb9ad) and then repeat the boot mode steps. --- # Seeed Studio Check out [our Seeed guide](https://help.trmnl.com/en/articles/12408721-seeed-studio-devices-xiao-diy-kit-and-reterminal) for steps to understand the buttons and how to enter flashing mode on your DIY Kit, E1001, E1002, and other devices. --- # Troubleshooting an Unresponsive Device Quick Links: - [TRMNL OG](#h_1267585565) - [TRMNL X](#h_2efeaad9b6) # TRMNL OG ## Charge the Device Though it may seem silly, the best starting point is to make sure the device is **fully charged**: Plugged in until the green light on the back (TRMNL OG) turns off. It's also a good practice to slide it into the **OFF** position during this charging cycle. Slide the power switch to ON once it is charged. For the rest of this article, if you reach a satisfactory result, please rate the article at the bottom accordingly. If you do not, please continue with the next step(s) till you've exhausted all options. ### Only Works When Plugged In If the device is responsive when plugged in, but it either: - fails to respond to button presses when unplugged - the green light never turns off This indicates a problem with the internal battery. If you recently opened the unit, please double-check it is properly plugged into the PCB. If not, look for any signs of battery swelling or burn marks on the battery itself. **Do not keep a faulty battery plugged in continuously!** [Contact support](#h_686ff24ec9) for further follow-up. ## Performing a Soft Reset An unresponsive device may require a soft reset. There are two ways to trigger the reset: ### WiFi Captive Portal Button - Hold the button on the back for 5-7 seconds, then release. This should enter the WiFi Captive Portal mode, like during [initial setup](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device), broadcasting a network `TRMNL` to connect to via WiFi. - Sign-in to the `TRMNL` SSID Network to display the captive portal. - Press the **Soft Reset** button. ### Long Press to Soft Reset Hold the button down on the back for 15-20 seconds to perform the soft reset. ## Reflashing the Device's Firmware [Reflashing the device](https://help.trmnl.com/en/articles/11936721-put-your-trmnl-in-flashing-mode) overwrites the existing firmware via a USB-C cable and our [web flashing utility](https://trmnl.com/flash), which contains the necessary instructions. Start with the most current firmware, and then step backwards and retest device responsiveness. If your device was purchased before August 2025 and was working properly before suddenly becoming unresponsive after an update or charging, please refer to our [fw1.6.2 troubleshooting guide](https://help.trmnl.com/en/articles/11934315-fw1-6-2-troubleshooting) and follow up with support. --- # TRMNL X ## Soft Reset If the dock is attached, remove it and invert it so the pins are pointing up. Place the inverted dock near the bottom-left corner of the **back** of the device and make a small circular motion (no need to touch, the magnet will do the work). The front screen of the device should automatically change to mostly blank with the loading image in the bottom-right, indicating the reset worked, before loading a screen. **You can repeat these steps with no negative impact on the device.** This soft reset will work to take the device out of boot loader mode, flashing mode, or during normal operation, as long as the device has battery charge. ## Charge the Device If the device does not respond to a *soft reset*, it will need to be charged. To do so, attach the dock with pins down to the center of the TRMNL logo glyph. It will be pulled into place by the magnets. Attach a USB-C cable to the dock and to a power source. Your device should start charging immediately. *If the device is online, you will see the charge state within your trmnl.com account, in the top-left. In v1.8.2+ of TRMNL X firmware, there will be a battery charging indicator visible on the bottom of the screen.* The dock pins should align properly with the PCB inside the device, regardless of dock orientation. However, if not, see below. ## Getting a Good Connection If charging is not working, there are two orientations that are successful. Thoughtfully adjusting the rotation of the dock can ensure a good connection for your unit. If there is an alignment issue of the PCB mounted inside your unit, then one of these two orientations will work, but the other may not. - Arrow pointing vertically or horizontally, dock screws in an `x` pattern - Arrow pointing at a 45° angle, dock screws in a `+` pattern This rotation can continue all the way around to 90, 135, 180, etc. to find what works for your device without having to open the case and reseat the PCB. If you are using a power+data USB-C cable (included with Clarity Kit), connecting the cable to a PC and using our [flashing guide](https://help.trmnl.com/en/articles/11936721-put-your-trmnl-in-flashing-mode) is another way to verify connectivity with the device showing up as `USB JTAG/serial...` on the list. **This method requires the battery to have charge.** ## Reconnecting to WiFi If you need to connect to a new WiFi network, you can trigger the WiFi captive portal by simultaneously pressing the left and right side of the touch bar. Then, hold the middle button to confirm on the next screen. See our [guide with video](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode). ## Manually Updating the Firmware Please see our guide for putting the TRMNL X in [flashing mode](https://help.trmnl.com/en/articles/11936721-put-your-trmnl-in-flashing-mode). *The unit must have some battery charge to trigger the mode change.* # Contact Support If the unit continues to be unresponsive, please contact support using the online chat icon in the bottom-right of any page on trmnl.com or by emailing [support@trmnl.com](mailto:support@trmnl.com?subject=Unresponsive%20Device%20Troubleshooting). --- # Creating Inline Images for Plugins The [Framework](https://trmnl.com/framework) [Title Bar](https://trmnl.com/framework/title_bar) component commonly includes a small icon to represent the plugin, especially on smaller mashup views. This is the most common place SVG image files are used to display simple graphics or iconography in plugins, but the following steps can be used for other SVGs in your plugin. To see a clear example of converting an SVG network call (...) to an inline SVG within a plugin, [this commit](https://github.com/stephenyeargin/trmnl-adafruit-io/commit/a555db36633a718ffe0f178b3c8023ca0992d45c) by community member Stephen Yeargin is invaluable. # Converting an Image File to SVG If you aren't already using an SVG file for your icon, some tools can help you do the conversion. For PNG, [pngtosvg.com](https://www.pngtosvg.com/) is a great option. Depending on your image, you'll want to adjust the *Colors* and *Simplify* values to get the best results, but here are some examples: - Complex full color image: *Colors* to `1` and *Simplify* to `10` - Simple, one-color (non-black) image: *Colors* to `2` and *Simplify* to `0` For JPG, you may need to do more preparation of your JPG, but there are numerous converters available. # Converting an SVG to Inline Code The tool [svg-2-code](https://nikitahl.github.io/svg-2-code/) by nikitahl is a good option. The output `` code will need the ` width="400" height="400"` values altered (`24` is a good size for the Title Bar component), but otherwise, you've now created an inline SVG! # Liquid Plugin Code In your plugin, you will want to use the Liquid **[capture](https://shopify.dev/docs/api/liquid/tags/capture)**[variable](https://shopify.dev/docs/api/liquid/tags/capture) in your **Shared** plugin view to easily use the inline SVG code and make it available to all your **Views**: {%- capture svg_logo %} ... {%- endcapture %} Save your **Shared** view, then we can update the Title Bar in your **View**(s) using proper `src` nomenclature, the captured variable (`svg_logo` in the example) and the Liquid **[base64_encode](https://shopify.dev/docs/api/liquid/filters/base64_encode)**[filter](https://shopify.dev/docs/api/liquid/filters/base64_encode):
...
Congratulations on making a more robust plugin! # PNG and Base64 Encoding While its important to properly size your PNG before converting to Base64, it's another option if you don't or can't go the SVG route. Make sure you're PNG is optimized! Use a site like [Squoosh](https://squoosh.app/), setting it to `OxiPNG` compression to ensure the file, and subsequent base64 encoding, is as small as possible! Using a site like Base64.guru or [others](https://www.base64-image.de/) allows you to upload a PNG image like this: Set the **Output Format** to `Data URI`, then click to *Encode*, generating code like this: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB/klEQVRIie1Vu6riUBRdeZhEEEXUKkEUA2nEIq2gYJFGwZ+wFMTKQqyudoKF/2ClWCpW+QE70coXtulEFDHZt7rOSKJzbyEDw2zYzdqPddZmn3MYIiK80dh3Nv83CHjHcd5K4KnANE3IsgxZlmHb9ssGyWQSsixjOp16xnkv0OfzIRKJAABWqxWCwSDi8fhDzuFwwPF4RDgcxu12gyAI3iewbZteuSAIZBiGCy+Xy8RxHJ3P55f1ngp2ux2GwyEAwHEc7Pd7dLvdh5z1eg0iQq/XA8/zKJfLUFX1ewomkwmxLPtt5ziOxuOxpwIXwWg0okwmQyzLUrVaJdM0KZFIuJp2Oh0yTZNEUSSWZSmdTtNgMHg+osvlAtu2sd1usVgsAACKokDXdUiS5FKeSqWg6zoYhgEALJdLbDYbnE4ncBz3q+aLqVgs/mgsr7xQKNwVuO6BqqrI5XLeK/fCstksNE1z4S4CwzDQaDR+TFCv11EqlVy455r+bv1+Hyz7/Mmq1Wq43W5P438kqFQq4HnvNCJCvV5/Wc/Ytk0AYFkWLpcLAoEAJEmCZVn4+PjAbDa7JzebTUiShFardcfy+Tza7TYikQiu1yuOxyNEUUQsFntUEI1GH5gVRYGmaZjP5/j69Px+P0RRvL9TAKBpGhRFucdDoZC3gncZ8/9P/usEn7TmdRqwWqbRAAAAAElFTkSuQmCC Put that inside an `` tag, or use the `{% capture png_logo %}` method above, and you're all set: --- # Visual Editor (IDE) Our private plugin [markup editor](https://help.trmnl.com/en/articles/9510536-private-plugins#h_9de5a95e77) featuring [version control](https://help.trmnl.com/en/articles/14136474-plugin-markup-version-control) and [debugging tools](https://help.trmnl.com/en/articles/11586187-debugging-private-plugins) has gotten us to 1,000 published plugins in the marketplace. But it also left users wanting more. So much that we built [trmnlp](https://github.com/usetrmnl/trmnlp), a local development toolkit and CLI for developing plugins outside the TRMNL interface. Now we're bringing these concepts together with the **Visual Editor**. ## Access the Visual Editor Navigate to any private plugin you've created or forked, then click Edit Markup. You'll land inside the legacy Markup Editor. Now click "Visual Editor" on the right side of the page. ## Using the Visual Editor Upon entering the IDE you'll be greeted with helpful tips and shortcuts. The visual editor is constructed of panes. Each may be resized or hidden with the toolbar across the top right of your screen. ### **Left sidebar** Nested tree of your markup elements. May be re-arranged via drag and drop. ### **AI Agent** Bottom left corner, chat with AI to build plugins from scratch or make tweaks. ### **Live Preview** Zoom and pan between all possible layouts, or click a layout in the top bar to jump directly into its markup. ### **Code editor** Located in the bottom center, here you can write HTML, CSS, and JavaScript just like in the legacy markup editor. ### **Shared/Serverless** (advanced) These tabs in the Code Editor unlock [shared markup](https://docs.trmnl.com/go/reusing-markup) and [serverless](https://help.trmnl.com/en/articles/14130649-serverless) capabilities. ### **Floating bar** Inject merge variables, drag/drop components, or templates to kickstart a new design ### **Right sidebar** Point-and-click style editor; update CSS, swap out components, read docs, and customize how elements look on different devices and orientations. ### Layout Use the icons along the top to rearrange the code editor to a left/right split (vs top/bottom) and show or hide other panes you may not need to access. ## Power Users Find a list of helpful keyboard shortcuts with `shift+?` (`shift + /`) , or refresh the page for the full welcome menu. Click on elements inside the preview area, then continue double-clicking on them, to drill into that element's markup inside the Code Editor. Drag and drop nested elements from the left sidebar to rearrange layouts without copy/pasting code blocks. --- # Webhook Image NOTE: this plugin was developed for the Home Assistant integration, so the API may change if necessary to suit that use case. ## Step 1 - Add the Plugin Inside TRMNL, navigate to Plugins > Webhook Image. Click "Add to my plugins" and give it a name. ## Step 2 - Copy Your Webhook URL After saving, you'll see a unique Webhook URL in your plugin settings. This URL is private to you. Treat it like a password. Anyone with this URL can update your display. ## Step 3 - Send an Image POST your image file directly to the webhook URL. Here's an example using cURL: `curl -X POST -H "Content-Type: image/png" --data-binary @my-image.png "YOUR_WEBHOOK_URL"` Replace YOUR_WEBHOOK_URL with the URL from Step 2, and my-image.png with your file. Supported formats: PNG, JPEG, BMP Maximum size: 5 MB Rate limit: 12 uploads per hour ## Step 4 - You're Done! Based on your device's refresh settings, you'll see your image on the display very soon. *Note: TRMNL OG displays are 800x480 pixels. For best results, size your image to match. This plugin only does passthrough image storage with no processing, so images may not display on your device unless they meet the devices needs.* ## Troubleshooting **Getting a 422 error?** Your image may be too large (over 5 MB), wrong format, or corrupted. Try a simple PNG file first. **Getting a 429 error?** You've hit the rate limit. Wait a bit before sending more images. **Display not updating?** Check that your POST request returned a 200 response. You can also use "Force Refresh" in plugin settings. **Image not rendering on your device?** Please validate that the format and pre-processing setup is valid for your device. Stay focused. --- # TRMNL Companion for iOS Read more about this feature in [the announcement](https://trmnl.com/blog/companion-ios-app). ## Get the App Download the app on your iPhone or iPad here: ## Feature Overview 1. Calendar Syncing 2. Apple Health 3. Apple Reminders 4. Apple Home 5. Shortcuts Let's explore each one. ## Calendar Syncing To make calendar sharing secure, Companion uses the EventKit library to sync events from multiple calendars (Apple, Outlook, etc) to your TRMNL account. (*If you use Google Calendar, check out our [native Google Calendar plugin](https://help.trmnl.com/en/articles/9483539-google-calendar)*) ### Step 1 - create a Calendar plugin Inside TRMNL, navigate to Plugins and create (or update) an existing Calendar* instance, setting the "Data Provider" value to "iPhone App" at the top of the settings form. `* Apple, Outlook, Fastmail, Nextcloud, CalDAV` ### Step 2 - Sync your calendars Inside the TRMNL Companion iOS App, pull down on the "Plugins" tab to refresh the list. Select the Calendar plugin instance(s) you set up in Step 1 and sync as many calendars as you'd like. It's OK to mix and match providers. For example you could sync a personal Google Calendar and a work-only Outlook calendar. The calendars themselves will not be shared -- rather, their events will be pulled independently and synced with your TRMNL account. Going forward, calendar events should automatically sync in the background. ## Apple Health / Reminders / Home Syncing Apple data to TRMNL is easy. ### Step 1 - Create a Plugin Inside the TRMNL web app, navigate to Plugins > [Private Plugin > New](https://trmnl.com/plugin_settings/new?keyname=private_plugin) and create a plugin with the "Webhook" strategy. ### Step 2 - Connect Data Back inside TRMNL Companion, pull down from the top of your Plugins tab to refresh the list and select the plugin you just created. Each category of data will appear in a dropdown menu, and you must enable permissions to share that type of data with TRMNL. Then you can toggle on/off data sources individually. This data will appear inside "nodes" of your TRMNL plugin. For example all Reminder data will appear inside a `#{{ reminders }}` block and so on. Learn how to use this data [here](https://help.trmnl.com/en/articles/9510536-private-plugins) and [here](https://help.trmnl.com/en/articles/10671186-liquid-101). ## Apple Shortcuts Shortcuts is a powerful "no code" utility built and maintained by Apple. This lets Apple users coordinate complex sequences of triggers and actions spanning many pieces of technology. For example, with Shortcuts you can make a smart light turn on when you arrive home. Or draft a Note with the current timestamp whenever your back door is opened. The possibilities are endless. With TRMNL Companion, these possibilities include creating and syncing custom plugins for your device. ### Step 1 - Create a Plugin Inside the TRMNL web app, navigate to Plugins > [Private Plugin > New](https://trmnl.com/plugin_settings/new?keyname=private_plugin) and create a plugin with the "Webhook" strategy. ### Step 2 - Create a Shortcut Inside your Apple product > Shortcuts, create a Shortcut with whatever inputs or triggers you'd like. Learn how to use Shortcuts generally [here](https://support.apple.com/guide/shortcuts/welcome/ios). When you're ready to push that data to TRMNL, create a new step, search for TRMNL Companion, then choose "Send Data to Plugin." Map your desired data inside the row labeled "JSON Data," then (optionally) customize the Key value "data" to something else. After saving your Shortcut, tap the ▶ (play) icon to test it. After seeing a success message, head back to your plugin inside the TRMNL web app. Click "Edit Markup" in the top right, then find your data inside the "Your Variables" section below the markup preview area. ## Customize your sync experience By default, TRMNL Companion will sync new calendar automatically in the background. But this may functionality may be paused if you "force close" the application. If you'd like to increase the sync frequency, set up an Apple Shortcut as follows. ### Step 1 - Create a Shortcut Open the Shortcuts app on your iPhone. Click the "+" symbol to add a new Shortcut. Search for "TRMNL" and select the TRMNL Companion app. ​ ### Step 2 - Enable the Shortcut After selecting the TRMNL Companion app, tap the Sync to TRMNL option. On the next screen we suggest disabling the "Show When Run" toggle. If you'd like to test this out, tap the ▶ (play) icon in the bottom right corner. ### Step 2 - Automate the Shortcut Tap the back button in the top left of your Shortcuts app to return to your home screen. Next, tap Automation in the bottom menu. Here you can set up a schedule, combining a time of day with the Sync Shortcut we created a moment ago. Click the image below to see it expanded. Note our recommendation to set "Run immediately" and disable "Notify when run," which will otherwise create a banner notification on your home screen. # Troubleshooting Please send feedback to . # Security TRMNL Companion uses native "Kit" modules by Apple. Each kit's data must be explicitly permitted by you. Downloading TRMNL Companion does not grant TRMNL personal information unless you login into your web account, approve permissions, then consent to additional data sharing. Regarding calendar specifically, Companion uses the EventKit library to fetch **only the details need to populate a calendar** -- an event's title, description, start/end time, status, and calendar name. hackers are welcome to Man-In-The-Middle the network request: { "end" => "18:00", "start" => "16:00", "status" => "accepted", "all_day" => false, "calname" => "ryan@usetrmnl", "summary" => "Support chats", "end_full" => "2025-10-02T18:00:00.000Z", "date_time" => "2025-10-02T16:00:00.000Z", "start_full" => "2025-10-02T16:00:00.000Z", "description" => "Respond to customers from my walking standup desk" } TRMNL Companion also only retrieves events from **the past 7 days, to the next 30 days**. --- # How to unpair / re-pair a device If you're rocking a native TRMNL device (not BYOD, not Virtual), you can unlink it in 1 click. Then you need to perform a 'soft reset' on the device itself. Here's how. ## **Step 1 - Visit the device settings** Inside TRMNL, hover the top-right dropdown and click the gear icon beside the native device you want to unpair. Scroll down to the Unlink Device section and click "Unlink." After confirming the scary confirmation box you should see a success message: ## **Step 2 - Soft reset the device** Now that the web application has said farewell, we need to instruct your physical device to consider itself reborn. **## TRMNL OG** Assuming your TRMNL is already on, hold the button on the back for 5-7 seconds, then let go. This should re-enter [WiFi pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode): **## TRMNL X** Pres the left and right side of the touchbar for ~1 second, letting go when the screen flashes to enter [WiFi pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode). You will confirm be holding in the center of the touchbar. Next, connect to the WiFi network named TRMNL as the screen suggests. You can do this on a phone or computer. But wait! Don't choose a WiFi network. We're resetting it aren't we? Ignore the network list at the top, and tap the "Soft Reset" button near the bottom. If you don't see this button, tap "Advanced" and then "Soft Reset" on the next page. Tap "yes" when it asks you to confirm. ## **Step 3 - Turning the Device Off** **## TRMNL OG** Within a few seconds, your TRMNL should re-enter WiFi pairing mode for a 2nd time. But this time, it will be as if it's a brand new device. You can now turn the device OFF with the slider on the back (slide it down). **## TRMNL X** Within a few seconds, your TRMNL will display the WiFi pairing screen again: To turn off the device, repeat the touchbar press on the left and right sides, holding for ~1 second, letting go when it starts flashing. You will see a confirmation screen: Hold the middle of the touch bar to confirm, letting it go when it starts flashing. You may see a *Flashing Modem Firmware...* screen for ~1 minute: After that completes it will show the Welcome screen: ## **Step 4 - You're done!** Your previous user connection, API key, Friendly ID (6 digit code), mirroring settings, name, refresh rate, sleep settings, and so on, will all be cleared. You may now [set up this device like usual](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device), or turn it off and give it to someone else. --- # Turning Off the TRMNL X Currently, turning off the device requires resetting WiFi. To turn off the TRMNL X, you need to put it in [WiFi pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode). Once you are on the WiFi Pairing Screen, you can go through the procedure to [turn the device off](https://help.trmnl.com/en/articles/11546838-how-to-unpair-re-pair-a-device?q=turn+off#h_8e3490343a). --- # Device battery FAQ While logged into TRMNL you can see WiFi + battery strength from your device picker or [all devices](https://trmnl.com/devices) view: Calculating remaining battery power is a simple equation, but it becomes less reliable as energy is drained and playlist refresh rates change. ## **Low battery notification** When your TRMNL device has < 10% energy remaining, you'll be notified by the following screen on your display: In the future you may be able to customize this screen. ;) ## **Charging your TRMNL** Select your device... **## OG** The OG model has 2 battery options, 1800mAh (default) and 2500mAh (upgraded). Assuming 0% energy: - 1800mAh - 3.5 hours to full charge - 2500mAh - 5 hours to full charge While charging, an LED on your device (above the USB-C port) will be green. When the device finishes charging, the LED will turn off. ​ **## Model X** The X model has 2 battery options, 6000mAh (default) and 12000mAh (upgraded). Assuming 0% energy: - 6000mAh - 5 hours to full charge - 12000mAh - 10 hours to full charge While charging, a "now charging" indicator will appear in the web interface. ## **Percent charged versus voltage** Select your device... **## OG** The `X%` charge value in your TRMNL account is approximated as follows: def percent_charged self.battery_voltage ||= 100 pct_charged = ((battery_voltage - 3) / 0.012).round(2) case pct_charged when 88..1000000 # 4.08v follows a full charge 100.0 when 85..88 95.0 when 83..85 90.0 when 10..83 pct_charged.round(2) when -1000000..10 1.0 end end If you instead prefer to know your device's last reported voltage, just hover the battery icon(s) for a read out: **## Model X** The Model X has a "gas gauge" chip built into the device, which reports charge levels down to 1/10th of 1%. This value, e.g. "42.4" or "95.3," is reported to TRMNL's web app whenever your device wakes up to request new content. Examine your precise battery charge amount by visiting your device's battery settings page: ## **Understanding power consumption** One refresh cycle takes 32.8mA over a period of 24s. In this time the device will ping the TRMNL server, download an image, and refresh the display. If there is no need to download and refresh the display with new image content -- for example if an image is not returned, or the returned image matches what is currently displayed -- power consumption will be around 20% of the above calculation. For more comprehensive details on TRMNL device power consumption, see here: ## **Determining your battery type** TRMNL devices offer multiple battery size options. If you are not sure which one came in your device, or simply want to verify: **## OG** ​[Disassemble your device](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl) (unless it is a clear case) 1. Compare your battery's shape and size to the photo below 2. For further confirmation, detach the battery (will require new tape) and look on the under side, which will show the mAh value Top: 1800mAh Bottom: 2500mAh **## Model X** Your Model X has 1 or 2 batteries, both of the same size and shape and power (6000mAh). If you have a clear enclosure, count them from looking through the back. Otherwise, you are welcome to disassemble your TRMNL with the included T6 screwdriver, or ask our support team to check the battery count being reported to our database by your device's firmware. --- # TRMNL X in Portrait Mode Shows Landscape Screen On April 9, 2026 we changed the way we structure the orientation change on the backend. While most plugins and screens were unaffected, if you have a plugin that is not showing the proper orientation, follow these steps to get things back in sync again. ## Force Refresh Make sure that the plugin or recipe has generated a new screen recently. If available, use the *Force Refresh* link on the plugin settings page. If not available, please wait till your next scheduled refresh has occurred. ## Save Orientation Again Under your TRMNL X [Device Settings > Color Palette > Orientation](https://trmnl.com/devices/current/palette/edit), change the orientation to the *opposite* option and click **Save**. Then, change it back to your desired option and click **Save** again. ## Examples of TRMNL X Screen Orientations Landscape (default): Portrait: --- # Why is BYOD needed for Kindle? We launched the Kindle Jailbreak in April 2025: ​ ## Alpha release The alpha version worked like this: 1. Our [KUAL application](https://github.com/usetrmnl/trmnl-kindle/tree/main/zip_example) fetched content from the `/api/display` endpoint ([docs](https://docs.trmnl.com/go/private-api/fetch-screen-content)). This image was for a native TRMNL device with 800x480 dimensions 2. Returned image was sent back to TRMNL, to a proxy endpoint, and transformed on-the-fly to match the Kindle's hardware specs With this strategy it was possible to mirror a native TRMNL device on a Kindle, without any extra licenses or costs. ## Beta release (current) On-the-fly image manipulation is difficult to scale, so we upgraded the stack: 1. TRMNL web interface has a "Device Model" dropdown picker ([docs](https://help.trmnl.com/en/articles/11547008-device-switcher-faq)) 2. Plugin images are generated based on these parameters 3. Our KUAL application fetches content the same way native devices do, without any proxying or on-the-fly manipulation required To access these improvements, this strategy requires that a Kindle making requests to our API is recognized as a Kindle device by TRMNL's screen generation pipeline. ## Official release (in progress) As our [Device Model docs](https://help.trmnl.com/en/articles/11547008-device-models) explain, DIY devices have several attributes beyond height/width dimensions. These include different levels of grayscale, color support, and degrees of rotation. When Framework 2.0 is released (est July 2025), TRMNL's screen generation pipeline will take these parameters into account as well, creating perfectly optimized images for every device (Kindle, Kobo, Android, etc) that has been articulated in TRMNL's Device Model database. To contribute to the Device Model database, see [the docs](https://help.trmnl.com/en/articles/11547008-device-models). ## TRMNL <> Kindle FAQ **Can I use connect multiple Kindles to 1 BYOD license?** Yes and no. [The spirit](https://help.trmnl.com/en/articles/11629486-calculating-byod-and-dev-edition-add-ons) of TRMNL is 1 device <> 1 license, but we respect those who simply want a few mirroring instances around their home. A couple things to consider: - Do your Kindles share the same specs? If not, images will be optimized for only some devices based on your [chosen device model](https://help.trmnl.com/en/articles/11547008-device-models). - Do you intend to add more than 2-3 devices? If yes, you may hit our `/api/display` endpoint's [rate limit](https://help.trmnl.com/en/articles/11652861-429-rate-limit). --- # Device Models TRMNL has native devices and DIY/ BYOD devices. Native devices are sold on our website, while DIY / BYOD devices are made up of components found elsewhere. Third party products like [Raspberry Pi](https://trmnl.com/blog/rpi-trmnl), [Kobo Readers](https://github.com/usetrmnl/trmnl-kobo), [Kindle](https://github.com/usetrmnl/trmnl-kindle), and [Android tablet](https://github.com/usetrmnl/trmnl-android) have something in common -- they can render a TRMNL dashboard, either from our native web application or an open source [BYOS server client](https://docs.trmnl.com/go/diy/byos). Here are some things to know as you set up a BYOD device. ## **Device models are WIP** To support a new device, we need a handful of attributes about its hardware. These are used as parameters inside the screen generation engine to ensure a perfect fit that leverages the best resolution and other capabilities. (integer) "width" # in pixels (integer) "height" # in pixels (integer) "color_depth" (string) "format" # png, bmp (integer) "colors" (integer) "scale_factor", default: 1 (integer) "offset_x", default: 0 (integer) "offset_y", default: 0 (integer) "rotate", default: 0 (integer) "device_type" # brand family, e.g. Kindle (string) "name" # friendly identifier For example, here's our Kindle 6th gen Paperwhite adapter: { id: 5, keyname: 'amazon_kindle_paperwhite_6th_gen', name: 'Amazon Kindle PW 6th Gen', width: 1024, height: 768, colour_depth: 8, colours: 256, scale_factor: 1, rotate: 90, offset_x: 0, offset_y: 0, format: 'png', device_type: :kindle } If you don't see a compatible dropdown option for your hardware, just send us a live chat or email and we'll help you figure it out. ## **BYOD device support is a labor of love** We are committed to open sourcing as much of our platform as we can. Learn more about this commitment [here](https://trmnl.com/blog/the-unbrickable-pledge/), and see a summary of our ecosystem [here](https://trmnl.com/developers). That said, supporting a growing library of 3rd party hardware has its challenges, and we ask that the community help us stay efficient + productive by: - posting Issues on the various [GitHub repos](https://github.com/usetrmnl) - joining our developer-only Discord > #byod / #byos channels - volunteering to test out your used hardware on new builds In exchange we can offer community recognition, complimentary TRMNL devices, and other perks. --- # Keeping your TRMNL plugged in If you have an OG TRMNL, an LED on the back lights up when the device is charging. This visual cue is by design, and makes it easy to unplug when charging is complete. However, some users prefer to keep their device always plugged in. For you, we have a suggestion to **prevent green LED bleed, or damage to the screen (pixel bleed)**. First, acquire a reflective or thick material such as aluminum tape: Next, [safely open your TRMNL enclosure](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl) and apply this material to the ~1x1 inch area where the LED sits behind the screen. You can plug in your device to compel the LED to turn on and identify a proper location. If you wish to acquire a TRMNL device with this material already applied, email and we will arrange this for you at no charge. --- # Custom plugin filters Below is a growing list of custom (fancy) mutations available to Private Plugins, powered by the open source [Liquid library](https://github.com/Shopify/liquid). Each filter is itself open-sourced in the trmnl-liquid repository: # Getting started with Liquid To get started quickly with `#{{ liquid }}` flavor variable notation, create a Private Plugin inside TRMNL, visit the Markup Editor, then follow along our Liquid 101 guide: To understand how Liquid words under the hood, check out the official docs: To parse and re-format Liquid-injected data, see the "Filters" section of this cheat sheet: # TRMNL custom filters Given our web application is built in Ruby/Rails, we're able to extend the default Liquid library with custom filters using the following strategy: We currently offer the following extensions. Email or hop inside our Developer-only Discord > #ideas channel to request more. ## **data** ​ # convert a variable to JSON #{{ my_var | json }} => {"foo":"bar"} # parse a nested json string # let data = {"data" => {"foo" => "bar", "stuff" => "{\"num\":5}"}} {% assign parsed = data.stuff | parse_json %} #{{ parsed_stuff.num }} # => 5 ## **collections** Requires an array of data, accessible from the Merge Variables dropdown. # let people = [{ "name": "Ryan", "age": 35 }, { "name": "Sara", "age": 29 }, { "name": "Jimbob", "age": 29 }] # group_by #{{ people | group_by: "age" }} # => {35 => [{"name" => "Ryan", "age" => 35}], 29 => [{"name" => "Sara", "age" => 29}, {"name" => "Jimbob", "age" => 29}]} # find_by , #{{ people | find_by: "name", "Ryan" }} # => {"age" => 35, "name" => "Ryan"} # find_by , , #{{ people | find_by: "name", "Toby", "Not Found" }} # where_exp # let numbers = [1, 2, 3, 4, 5] #{{ numbers | where_exp: "n", "n >= 3" }} # => [3, 4, 5] # let people = [{name: "Ryan", age: 35}, {name: "Liz", age: 25}] #{{ people | where_exp: "person", "person.age == 'Ryan' or person.age < 26" }} Manipulate a comma-separated value string into an array, then **sample** a single value randomly. Useful for providing a list of values in a Form Field and selecting one randomly to use in the Polling URL. # let isbns = "9781473205321,9781473205338,9781473200203, 9781473200104" #{{ isbns | split: ',' | sample }} # removing whitespace # let isbns = "9781473205321, 9781473205338, 9781473200203, 9781473200104" #{{ isbns | split: ',' | sample | strip }} ## **numbers** #{{ 1234 | number_with_delimiter }} # => 1,234 # custom delimiter (default is comma) #{{ 1234 | number_with_delimiter: '.' }} # => 1.234 #{{ 1000000 | number_with_delimiter: ' ' }} # => 1 000 000 # optional 2nd arg separates whole number from fractional value #{{ 1234.57 | number_with_delimiter: ' ', ',' }} # => 1 234,57 # currency handling #{{ 10420 | number_to_currency }} # => $10,420.00 #{{ 152350.69 | number_to_currency: '£' }} # => £152,350.69 # optional 2nd + 3rd args delimit + separate #{{ 1234.57 | number_to_currency: '£', '.', ',' }} # => £1.234,57 # currency handling with locale instead of currency unit #{{ 1234.57 | number_to_currency: 'sv' }} # => 1234.57 kr #{{ 1234.57 | number_to_currency: 'sv', '.', ',' }} # => 1.234,57 kr #{{ 1234.57 | number_to_currency: 'en-GB' }} # => £1234.57 Note that named parameters are not supported by Liquid, so you must supply all preceding arguments in the case that you want to override a 2nd+ parameter default. ## **string, markup, HTML** # pluralize + inflect/humanize #{{ "book" | pluralize: 1 }} # => 1 book #{{ "book" | pluralize: 2 }} # => 2 books #{{ "octopus" | pluralize: 3 }} # => 3 octopi #{{ "person" | pluralize: 4 }} # => 4 people # convert markdown into HTML {% assign markdown = "This is *crazy* and [here's a link](https://somewhere.com)." %} #{{ markdown | markdown_to_html }} # => This is crazy and here's a link. # optional - pass strip_html to convert to plain text #{{ markdown | markdown_to_html | strip_html }} # => This is crazy and here's a link. ## **dates** For general tips to manipulate dates with native Liquid, see our [Advanced Liquid](https://help.trmnl.com/en/articles/10693981-advanced-liquid) guide. The custom date filters below may be combined built-in Liquid filters like `plus` and `minus` to add or subtract minutes, hours, time zone offsets, and beyond. ​ #{{ 7 | days_ago }} => yyyy-mm-dd representing 7 days before now # oridinalize a day, e.g. 1 => 1st, 30 => 30th # accepts a date string with strftime argument that includes <>, which will be substituted by the ordinal itself #{{ "2025-10-02" | ordinalize: "%A, %B <>, %Y" }} # => Thursday, October 2nd, 2025 #{{ "2025-12-31 16:50:38 -0400" | ordinalize: "%A, %b <>" }} # => Wednesday, Dec 31st ## **uniqueness** The `append_random` filter ensures that multiple, linked HTML elements (like a `
` and JS object) don't override another instance of this plugin within a mashup layout. ​ {% assign chart_id = "chart-" | append_random %} # => "chart-q7x1" Within your layout's context, any usage of `#{{ chart_id }}` will yield the same generated value. This is useful for binding elements such as:
Highcharts.chart("#{{ chart_id }}") ## **localization** First, `l_date` localizes a date (object or string) to a friendlier format with [strftime syntax](https://www.foragoodstrftime.com/). #{{ '2025-01-11' | l_date: '%y %b' }} # => 25 Jan # with a specific locale, ex: Korean #{{ '2025-01-11' | l_date: '%y %b', 'ko' }} # => 25 1월 # with the user's locale; good for published Plugin Recipes #{{ '2025-01-11' | l_date: '%y %b', trmnl.user.locale }} # => 25 1월 Next, `l_word` translates common words to another language. Contribute more [here](https://github.com/usetrmnl/localizations).

#{{ "today" | l_word: 'es-ES' }}

# => hoy {% assign day = "tomorrow" %}

#{{ day | l_word: trmnl.user.locale }}

# => 내일 ## **QR codes** Convert a string (or URL) to a scannable QR code that automatically scales inside its container: ​ #{{ "Hello World" | qr_code }} #{{ "https://some-long-url.com/help.asp-net" | qr_code }} For backwards compatibility, using a fixed width/height. To enable this functionality: # A larger module_size, lower error correction "level", and fixed size #{{ "https://somewhere.com" | qr_code: 18, "", fixed }} Want to style your QR code? TRMNL automatically adds class `qr-code`. Here's an example that puts your QR code in the top right corner of the screen: #{{ "https://trmnl.ink" | qr_code: 3 }} Note: the `qr_code` filter relies on the RQRCode library: ​[https://github.com/whomwah/rqrcode](https://github.com/whomwah/rqrcode?tab=readme-ov-file) # Troubleshooting Combining filters with our Framework handlers, for example [Value Formatting](https://trmnl.com/framework/format_value), may produce unexpected results. For example: #{{ 10420.00 | number_to_currency }} Will yield $10,420 without `.00` trailing decimal places, because `value--tnums` attempts to simplify whole numbers. # Adding more custom filters Want to write code? Submit a pull request to [trmnl-liquid](https://github.com/usetrmnl/trmnl-liquid/), putting your work [here](https://github.com/usetrmnl/trmnl-liquid/blob/main/lib/trmnl/liquid/filters.rb) and a simple test case [here](https://github.com/usetrmnl/trmnl-liquid/blob/main/spec/trmnl/liquid/filters_spec.rb). Don't want to write code? Email or hop inside our Developer-only Discord > #Plugins channel to request additional filters or arguments. --- # How to Change Screens # TRMNL (OG) The button on the back of the TRMNL (OG) device can serve many functions, but the most prominent is to force the device to refresh and *advance to the next screen in your [playlist](https://help.trmnl.com/en/articles/11663305-playlist-scheduler)*. Pressing the button wakes up your device and instructs it to connect to its server to fetch the next screen update. This button has additional functions outlined in our [special functions](https://help.trmnl.com/en/articles/9672080-special-functions) article. # TRMNL X TRMNL X may be designed as an ambient dashboard device, but that doesn't mean you can't give it a little push (literally!) if you want to advance to the next screen in your [playlist](https://help.trmnl.com/en/articles/11663305-playlist-scheduler). To get the next image, tap the center of the touchbar on the bottom: ## Touchbar Extra Functionality TRMNL X will store recent screens in memory, allowing you to go "back" and "forward" through *saved* screens, no network request required. ### Touchbar Mode By default, TRMNL X devices ship in **Tap** mode. However, in your Device Settings > Firmware you can change this gesture from tap to **Swipe**. ### Touchbar Tap Touching the left-side of the touchbar will go back to the previous screen. Touching the right-side of the touchbar will go forward through saved screens. ### Touchbar Swipe You can swipe left to go back and swipe right to go forward. --- # Testing your Alias or Redirect plugin Since TRMNL forwards your Alias (Image URL) and Redirect (JSON URL) to your device, it is not possible to "preview" resulting content inside the TRMNL web application. ## Alias plugin testing 1. Set your Image URL, e.g. 1. https://my-blog.com/img.png or http://192.168.0.1/img.png 2. Navigate to Playlists, then find or add the Alias instance you just created via the Add a Plugin dropdown 3. Drag and drop the Alias instance to be directly below whatever item is now being shown, indicated by a green badge with "Displayed now" ​ 4. Click the button on the back of your TRMNL device to force-skip to the next item, your Alias instance 5. If you see your image, great. If you just see the TRMNL logo, your image formatting may not be exactly to spec. [See here](https://docs.trmnl.com/go/imagemagick-guide). ## **Redirect plugin testing** 1. Set your JSON endpoint Web Address, noting that **this must be publicly accessible on the web**. A `192.168` style address won't work because our web server needs to GET this endpoint and share its [response parameters](https://help.trmnl.com/en/articles/11035846-redirect-plugin) to your physical device. The response's `url` parameter can* be a local network destination however. 2. Navigate to Playlists, then find or add the Redirect instance you just created via the Add a Plugin dropdown 3. Drag and drop the Redirect instance to be directly below whatever item is now being shown, indicated by a green badge with "Displayed now" ​ 4. Click the button on the back of your TRMNL device to force-skip to the next item, your Redirect instance 5. If you see your image, great. If you just see the TRMNL logo, your JSON response body or image formatting may not be exactly to spec. To confirm your JSON is returning and formatted correctly: 1. Create a Private Plugin and input your Redirect instance's Web Address into the Polling URL field. Click Save, then click "Force Refresh" on the right side. 2. Open the Markup Editor and click to expand the Your Variables accordion. 3. Check if your 3x parameters are present. If not, double check that your JSON content is returned with header `content-type` set to `application/json` . TRMNL does not explicitly request a content-type. To confirm your image formatting is correct, [see here](https://docs.trmnl.com/go/imagemagick-guide). --- # How to Assemble your TRMNL **## TRMNL OG** This video walks through putting your OG device back together in under 1 minute. We recommend first watching the entire video, then trying it yourself. You may also get tips from our other guide: [how to disassemble your TRMNL](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl). ### Full TRMNL OG assembly from scratch [https://www.youtube.com/embed/Z64FDqIaKpg?rel=0](https://www.youtube.com/embed/Z64FDqIaKpg?rel=0) ## Partial TRMNL OG assembly [https://www.youtube.com/embed/t6HDGpmyN2I?rel=0](https://www.youtube.com/embed/t6HDGpmyN2I?rel=0) **## TRMNL X** Please see our full assembly guide video below. [https://www.youtube.com/embed/RdfMNQTyQQ8?rel=0](https://www.youtube.com/embed/RdfMNQTyQQ8?rel=0) If you have any issues, [get in touch](https://help.trmnl.com/en/articles/12410454-how-to-contact-support). --- # How to Preload WiFi Credentials Whether gifting a TRMNL to a friend or relative (we like you already), there are many reasons you may want to set up the WiFi credentials on your TRMNL device beforehand: - The recipient did not setup their WiFi - The recipient has no phone, laptop, or other device to complete setup - The recipient has restricted or limited movement No matter the reason, here's our recommend trick for getting things set up, because your TRMNL device can store multiple credentials at once! # Using your Phone's Mobile Hotspot Feature You will need to know at least the SSID and Password of your recipient's network. Ideally, you would also know the protocol used (*e.g.,* WPA2). First, configure your phone's hotspot to match the recipients network SSID, password, and settings. This varies by phone and carrier, but make sure that if you are setting up a TRMNL (OG), you are using the "Maximum Compatibility" mode on iPhones and setting Samsung's hotspot to "2.4Ghz." Second, [activate the captive portal](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode)on your TRMNL device: - **TRMNL (OG):** Hold the button on the back for 5-7 seconds, then release. - **TRMNL X**: Press the left and right side of the bottom touchbar at the same time, hold middle button to confirm. Finally, connect to the TRMNL network broadcast by your device and follow the normal [setup procedure](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device) to enter the credentials, pressing CONNECT to send them to the device. If everything was correct, your device will automatically display your next playlist item. You can now disable your hotspot. --- # TRMNL X Not Broadcasting TRMNL Network The first ~75 TRMNL X devices we shipped were loaded with firmware v1.7.5, which should automatically update to the current version once they are connected to internet. However, due to two reasons, it may not be able to do so through the normal flow. Here's how to fix it. ## Entering Flashing Mode Please see our [guide](https://help.trmnl.com/en/articles/11936721-put-your-trmnl-in-flashing-mode). If it doesn't update to the current version (1.7.6+) after following the steps on the flashing page, you may need to **exit bootloader mode by trigger a *soft reset*.** ## Get Out of Bootloader Mode Please perform a *soft reset* as described in our [troubleshooting guide](https://help.trmnl.com/en/articles/12407673-troubleshooting-an-unresponsive-device). --- Once the firmware is updated, the TRMNL-XXXX network will broadcast and allow you to complete setup normally. --- # Nano Banana Dashboard Nano Banana Dashboard uses Google's Gemini AI to transform your existing TRMNL plugin data into imaginative scenes rendered directly on your e-ink display. ## **Step 1 - Get a Gemini API Key** Ccreate an API key from Google AI Studio and add your billing details. Without billing setup, the image generation will fail as google does not have a free tier any longer. Visit [aistudio.google.com/apikey](https://aistudio.google.com/apikey) to generate one. Copy and keep it handy for Step 3. ## **Step 2 - Add Data Source Plugins** Nano Banana Dashboard visualizes data from your other TRMNL plugins. Before configuring the dashboard, make sure the plugins you want to display are already added to your playlist. For example, if you want your dashboard to show today's weather and upcoming calendar events, ensure the Weather and Google Calendar plugins are already set up in your account. ## **Step 3 - Configure the Dashboard** Inside TRMNL, navigate to Plugins > Nano Banana Dashboard and fill in the following: ## **Required Settings** - **API Key**: Paste the Gemini API key from Step 1. This is stored encrypted and never shared. - **Model**: Choose between **Flash** (faster, lower cost) or **Pro** (higher quality output). Flash is a great starting point. - **Plugin 1, 2, 3**: Select up to 3 of your existing plugin instances as data sources. The dashboard will pull live data from these plugins and weave it into the scene. ## **Optional Settings** - **Custom Text**: Text that will always appear prominently on every generated dashboard. Great for a name, motto, or daily reminder. - **Data Instructions**: Tell the AI which data to feature or skip. For example: *"Show today's calendar items only and highlight the weather forecast."* - **Style / Theme**: Choose a visual scene theme that defines how your data is presented. Options include: - **None** — Minimal framing, data-focused - **Scenic Landscape** — Data integrated into a natural scene - **Daily Briefing** — Classic newspaper-style layout - **Retro Comic** — Comic book panel aesthetic - **Technical Blueprint** — Engineering schematic style - **Vintage Poster** — 1930s art deco design - **Cozy Cafe** — Chalkboard menu style - **Space Mission** — Mission control aesthetic - **Japanese Ink** — Sumi-e brush painting style - **Library Study** — Scholarly, book-lined backdrop - **Steampunk Workshop** — Gears and brass instruments - **Ocean Depths** — Underwater observatory theme - **Minimalist Grid** — Clean, structured grid layout - **Random** — Surprise me! Picks a different theme each time. - **Style Customizations**: Additional instructions for the visual style. Be as imaginative as you like. For example: *"Make it look like a hand-drawn sketch"* or *"Use a dark moody atmosphere."* - **Aspect Ratio**: Choose the image dimensions (16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 1:1, or 21:9). - **Color Palette**: Controls the colors in the generated image. Options range from **Black & White (2 shades)** up to **Full 24-bit color**. Any palette works since the TRMNL image pipeline will optimize it for your device. ## **Example Configuration** Model: Flash Plugin 1: Google Calendar Plugin 2: Weather Custom Text: Good morning! Data Instructions: Show today's calendar items only as well as the weather Style: Cozy Cafe Style Customizations: Warm and inviting with a hand-lettered feel Color Palette: 4 shades grayscale This would generate a chalkboard-style cafe scene showing your day's calendar events and weather forecast, with your custom text displayed prominently. ## **How Caching Works** To avoid unnecessary API calls, Nano Banana Dashboard caches your generated image. A new image is only generated when your settings or plugin data change. If nothing has changed since the last render, a new image is not generated. ## **Tips & Best Practices** - Start with **Flash** model while experimenting. It's faster and costs less. Switch to **Pro** once you've dialed in your preferred style. - Use **Data Instructions** to focus the output. Without guidance, the AI will try to include everything from your selected plugins. You can even instruct the creation of different images based on your data. - The **Random** theme is a fun way to keep your display fresh. It picks a different visual style on each generation. - **4 shades grayscale** is the sweet spot for 2-bit OG TRMNL devices, but feel free to experiment as the image pipeline handles the conversion so all formats work. - If generation fails, the dashboard will continue showing your last successfully generated image. ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing AI-generated scenes on your TRMNL very soon. Stay focused (and artistic)! --- # Home Assistant Screenshot This plugin is a companion to the TRMNL Home Assistant [add-on](https://github.com/usetrmnl/trmnl-home-assistant) that captures screenshots of your dashboards, applies dithering algorithms optimized for ePaper screens, and sends them to TRMNL. ## Step 1 - Add the Plugin Inside TRMNL, navigate to Plugins > Home Assistant Screenshot. ## Step 2 - Copy Your Webhook URL After saving, you'll see a unique Webhook URL in your plugin settings. This URL is private to you. Treat it like a password. Anyone with this URL can update your display. ## Step 3 - Installing TRMNL HA ### OPTION 1 - Add-on store Navigate to the TRMNL Home Assistant [add-on](https://github.com/usetrmnl/trmnl-home-assistant) and click: [](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fusetrmnl%2Ftrmnl-home-assistant) OR ​ In Home Assistant, go to Settings > Add-ons > Add-on Store. Click the three-dot menu, select "Repositories", and add: Then find "TRMNL HA" in the list and click Install. ### OPTION 2 - Stand-alone If you run Home Assistant in Docker (HA Container), you don't have the add-on store. Run TRMNL HA as a separate container. See step 4 for details on your the HOME_ASSISTANT_URL and ACCES_TOKEN values. docker run -d --name trmnl-ha \ -e HOME_ASSISTANT_URL=http://YOUR_IP:8123 \ -e ACCESS_TOKEN=your_token \ -p 10000:10000 \ ghcr.io/usetrmnl/trmnl-ha-amd64:latest Note: For ARM64 (Pi 4/5, Apple Silicon), use this image url: `ghcr.io/usetrmnl/trmnl-ha-aarch64:latest` Access the Web UI at `http://your-server:10000` Full Docker setup: ## Step 4 - Configure Go to Settings > Add-ons > TRMNL HA > Configuration. Create a Long-Lived Access Token (Profile > Long-Lived Access Tokens) and paste it in the "access_token" field. Leave home_assistant_url as the default `http://homeassistant:8123` unless you have connection issues. If it fails, use your device's IP address (e.g., `http://192.168.1.100:8123`) as the default won't work from inside containers. Save and start the add-on. Add HA to the sidebar by using the dedicated toggle. ## Step 5 - Configure Open "TRMNL HA" from the sidebar, create a schedule, pick your device preset using the preview to confirm it looks good on device, and paste your Webhook URL into the allocated field. ## Step 6 - You're Done! Based on your schedule and device refresh settings, you'll see your dashboard on the display very soon. Note: Use "Send Now" to test immediately. Device presets auto-configure the right size and dithering for your display. ## Troubleshooting **Display not updating?** Check that your schedule is enabled and the webhook URL is correct. Click "Send Now" to test. Also check add-on logs at Settings > Add-ons > TRMNL HA > Log **Screenshot looks wrong?** Try increasing the "Wait" time for complex dashboards. Use a high-contrast theme like "Graphite". Fine-tune the image generation. **HA connection failing?** Verify your access token and home assistant url is correct. The Web UI shows connection status with details. **Need more help?** Full docs: --- # Display Calibration for BYOD Devices Every e-ink panel is a little different. What looks perfect on one display might feel cramped or oversized on another. # Where to find this feature? Navigate to **Device > Edit > Device Model** (only visible for BYOD devices): # Pixel Ratio (0.5–3.0) Zooms the entire image. Like pinching to zoom on your phone. - Below 1.0 = zoomed out, more content fits - Above 1.0 = zoomed in, content is larger # UI Scale (0.5–2.0) Adjusts text and element sizes without changing the overall image dimensions. - Below 1.0 = smaller text, tighter spacing - Above 1.0 = larger text, more breathing room # Which one should I use? - **Everything looks too small** → Increase Pixel Ratio - **Text is hard to read** → Increase UI Scale - **Layout feels cramped** → Increase UI Scale - **Image doesn't fit the screen** → Adjust Pixel Ratio # Force Refresh after update Screens are pre-rendered images. Changing calibration settings only affects new renders. After saving your changes, go to your plugin and hit **Force Refresh** to regenerate screens with your new settings. Otherwise you'll keep seeing the old cached images. # Reset to defaults Click **Reset to defaults** above the calibration fields to restore the original values for your device model. --- # Gaffa Gaffa has partnered with TRMNL to give users 6 months of **free** usage. Thereafter, your personal API key can be obtained from: . Keeping focused AND up to date with the news takes just a few seconds. ## **Step 1 - Visit the Gaffa Plugin** Inside TRMNL, navigate to Plugins > Gaffa. ## **Step 2 - Configure Plugin** - News Website URL - Paste the URL of the site you have chosen. - Custom Prompt - The instructions that the Gaffa AI will use to summarize the news. - API key (Optional) - After the 6 months trial period, your personal API key can be obtained from: . ## **Step 3 - You're Done!** You'll begin seeing your news content very soon. Please give TRMNL and Gaffa a couple of minutes to display your extracted data on the Plugin configuration page and then your TRMNL. --- # PurpleAir ## **Step 1 - Visit the PurpleAir Plugin** Inside TRMNL, navigate to Plugins > PurpleAir. ## **Step 2 - Hop over to the developer dashboard for the API key** *NB: TRMNL is very careful about the PurpleAir API usage. The first call fetches the last 24 hours. Subsequent calls only fetch the delta since last request (up to 24 hours). The estimated depletion time for the free credits is 2.5 years.* ## **Step 3 - Select the sensor ID that you would be viewing** You can find the Sensor ID from the PurpleAir Map URL. Example URL: ​ The id to copy is the "select" parameter value: **115463.** Or via the MAP UI: ## **Step 3 - Fill in the plugin Form on the TRMNL Dashboard** Fill in the sensor id and the API key that we have obtained above. The "**Read Key (Private Sensors)"** is optional that is required only for sensors that are private to you. ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing PurpleAir data very soon. Breathe easy! --- # AI Agent Writing code is cool, but wouldn't it be cooler to type "*build me a plugin showing populations around the world*" and it Just Worked? ​ Now you can. ## Requirements This feature is in *public beta*. Agent currently requires an API Key from OpenRouter. You can load credits on their [billing portal](https://openrouter.ai/settings/credits). ​ ​*Alternatively, if you have a Claude API account with higher [rate limits](https://platform.claude.com/docs/en/api/rate-limits) you can [buy credits](https://platform.claude.com/settings/billing) directly and use an* *Anthropic [API Key](https://platform.claude.com/settings/keys)*. Optionally, a free [Tavily](https://www.tavily.com/) API key can give your agent access to web search and web page fetching, enhancing your agents capabilities. In the near future you'll be able to purchase credits directly from TRMNL. ## Quickstart 1. Visit your [TRMNL Account](https://trmnl.com/account) 2. Toggle Agent on the left side, click Save 3. Click the "Configure model, keys & usage" to open up the settings page. 4. Add the API keys mentioned in the requirements section and click Save 5. Visit [Plugins > Private Plugins > New](https://trmnl.com/plugin_settings/new?keyname=private_plugin) and click Save 6. Click Edit Markup, then click the Agent tab 7. Ask Agent to build whatever you want or to guide you would like to understand more of the TRMNL framework. ## How it works TRMNL has developed a comprehensive system prompt written in plain English, which describe our platform's capabilities and design patterns in depth. Whenever you interface with the Agent inside our private plugin interface, the agent has access to this context, which you may even ask it about. ​ In addition, the agent has been given access to a range of tools, including markup editing, settings updates, data refreshing, internet search, and a list of API endpoints to name a few. This empowers and grounds the agent in the "real world" to add value to your experience. ## MCP Server (Advanced) To develop TRMNL plugins within your local development environment, create an MCP API Key instead from your private plugin's settings page, located on the right side after clicking Save. Provide this key to your desktop agent for a more customizable experience with your local tooling. ​ ## Frequently Asked Questions *Which models does this feature support?* Gemini, Codex, Claude Sonnet, Kimi, and even OSS models (e.g. Llama). Set a preferred model from your [Account](https://trmnl.com/account) > Agent configuration. *How much does it cost to build a plugin?* We're seeing an average of $1-3 per plugin. For example, plugin can be created with a single sentence prompt: `make a daily deal plugin for meh.com`. After thinking and developing for a couple minutes, Agent required us to provide a free Meh.com API Key, then we requested that the daily deal item's image be included. Total cost: $2.20 USD. *Can I publish plugins with this feature?* Not currently. Our team reviews several plugin submissions per day and we need to scale internal tools before we can handle the inevitable Agent-driven volume bump. *Can I use other llm models (including free ones)?* Yes, even though Claude Sonnet 4.6 was used in developing this Agent, other state-of-the-art models like Gemeni 3.1 Pro are excellent replacements. Feel free to experiment with any [OpenRouter model ID](https://openrouter.ai/models). The free models on OpenRouter tend to be less capable and slower. ## Troubleshooting Agent isn't perfect. Send us a live chat or email if (when) you discover bugs or quirks. --- # Plugin recipes TRMNL has 3 types of plugins: 1. native, which our team builds + maintains 2. private, which users build for themselves 3. community, which anyone can build for other users Native plugins include Shopify, reddit, Google Calendar, and [dozens more](https://trmnl.com/integrations). Private plugins range from [college football rankings](https://github.com/usetrmnl/plugins/blob/master/lib/usa_college_football_rankings.md) to [xkcd comics](https://github.com/SnarfulSolutionsGroup/TRMNL-Plugins/blob/main/TRMNL_Comic.md). Community plugins include [STRMNL](https://trmnl.com/integrations/strmnl) (Strava), [Alpenglow](https://trmnl.com/integrations/alpenglow), [ISS tracker](https://trmnl.com/plugin_settings/new?keyname=iss_location), [Apple Photos](https://trmnl.com/integrations/apple-photos), and 100s of others. But in some cases, **users prefer to share private plugins *without* hosting user data on their own server**. For this we have **Recipes**. # Introducing Recipes Our [open source plugins repository](https://github.com/usetrmnl/plugins) hosts a growing number of private plugins -- developed originally for a single person -- that have been shared for anyone to enjoy. However setting these up may take time or a bit of coding knowledge. To alleviate these requirements, Recipes provide a 1-click approach to sharing private plugins. **Disclaimer** The TRMNL team manually approves every Recipe. This does not mean they are immune from errors. In addition, some Recipes require passing potentially sensitive information through the author's own server(s). Be sure to review the "Advanced Settings" dropdown on each Recipe page to know how your data may be used. **Enabling a Recipe** 1. Visit [Plugins](https://trmnl.com/plugins) > scroll down to Recipes 2. Choose one, then click "Install" 3. You're done! **I**f you prefer to *modify* the Recipe, click "Fork" instead of "Install" in step 2 above. Forking a Recipe will make the markup and other settings editable, as if it was your own, from the Plugins > Private plugin interface. **Install vs Fork** Each approach has pros and cons: - Installed recipes receive automatic updates (bug fixes, markup improvements) - Forked recipes can be modified, including the look and feel - Forked recipes do not receive any updates from the original creator For less technical users, we suggest Installing a Recipe. For more technical users, Fork a Recipe if you anticipate changes, then feel free to re-install (or re-fork) a Recipe if you discover that the original creator made improvements. Here's another way to understand Recipe installation: | **`must_fork?`** | **`requires_oauth?`** | **`shared_oauth?`** | **Result** | | --- | --- | --- | --- | | `false` | * | * | `:simple_install` | | `true` | `true` | `true` | `:oauth_choice` | | `true` | * | * | `:read_only_fork` | If your TRMNL device does not have the [Developer edition add-on](https://trmnl.com/blog/developer-edition), you will only be able to Install Recipes, not Fork them. # Maintaining Recipes **Recipe owners** If you're the *proud author* of a Recipe, you'll notice a "Recipe Master" badge inside the Plugins > Private Plugins interface: This indicates you have influence over devices all around the world. Please be careful when modifying your origin plugin instance, as updates will be pushed to all other users automatically. We suggest using the copy feature (clone icon) to test changes first, then update your "Recipe Master" instance when you feel comfortable. **Note**: if your Recipe has [custom fields](https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder), their values will not be copied over. So your personal information will not be shared. However, we use the latest screen generation as the Recipe preview image, so just be careful about which data you plug into the official "master" Recipe. ;) **Recipe users** If you have *installed* a Recipe, you'll notice a "Recipe" badge inside the Playlists interface: This is simply a reminder that the plugin may not share all the features as others. # Publishing Recipes Sharing your work with the TRMNL community helps make the platform both accessible and extendable for users of all technical backgrounds. If you're open to this labor of love: 1. Build a private plugin 2. Click the icon on your plugin settings page beside "Publish plugin?" 3. We'll review your plugin, then publish it here: We look forward to seeing what you build. # Public vs Unlisted Publicly published Recipes can be found by anyone from `/recipes` or `/plugins`. These plugins get more installs because they have greater visibility and are promoted by our team. However if you prefer not to comply with [Chef](https://www.loom.com/share/e41edefd3c2643668b35e41d74d07020) ([more details](https://help.trmnl.com/en/articles/11395668-recipe-form-fields-best-practices)) or your plugin does not meet our Moderation Guidelines, you are welcome to publish the plugin as Unlisted. Unlisted plugins skip the automated + manual moderation steps and generate a shareable link immediately that may be used to promote your work to others. # Editing Recipes If your plugin is Unlisted or In Review, it may be edited. Click the pencil/edit icon to the right of its publication status. You'll be presented with a few options, based on the status. 1. Unlisted plugins may be reverted to Private status or submitted to the public marketplace 2. In Review plugins may be reverted to Private status By design, published plugins may only be Unlisted by contacting our team. They also may not be deleted without coordination by our team and a notification to affected users (external installs). # Moderation Guidelines We believe in free expression, and don't want to crimp your development style or personal interests. We also envision a world where TRMNLs are not just for adults, but families and children too. For this reason, Recipes with the following content will most likely be denied from publication: - NSFW / suggestive imagery (real or animated) - egregious profanity (stuff like [this](https://trmnl.com/recipes/142466) is OK) - depicts physical pain or suffering (video games OK) If your work overlaps with 1 or more categories above, it can still live on the TRMNL platform, and you can still share it with others in a few ways. 1. Upload the source code or [zip export](https://help.trmnl.com/en/articles/10542599-importing-and-exporting-private-plugins) to your GitHub or personal profile 2. Publish your plugin as "Unlisted," then link others to it directly 3. Publish a family friendly version of your plugin that links to #1 or #2 above in the `author_bio` section --- # User level API Keys TRMNL's most popular API endpoints require a device-level token, which you can retrieve for any Developer edition enabled device from [Devices > Edit](https://trmnl.com/devices) > Developer Perks. A device-level access token may be used to: - Send webhooks to a [Private plugin](https://help.trmnl.com/en/articles/9510536-private-plugins) / Recipe - [Fetch screen content](https://docs.trmnl.com/go/private-api/screens) View the Device API Key docs: # Where is my User API Key? Generate a User API Key from your account tab: ​ # What can a User API Key do? A user-level access token may be used to: - [Fetch plugin content](https://docs.trmnl.com/go/private-api/plugin-data) (Data Mode) - Run local dev tools like [trmnlp](https://github.com/usetrmnl/trmnlp) - [Export private plugins](https://help.trmnl.com/en/articles/10542599-importing-and-exporting-private-plugins) to a zip file (for backup or sharing) - Fetch and update Devices, Playlist items, and more View the User API Key docs: --- # Custom plugin form builder If you're new to building plugins, see [Private Plugins](https://intercom.help/trmnl/en/articles/9510536-private-plugins) and [Public Plugins API Docs](https://docs.trmnl.com/go/plugin-marketplace/introduction). Below is a guide to adding user-facing form fields inside a Private Plugin or [Recipe](https://intercom.help/trmnl/en/articles/10122094-plugin-recipes)**.** Prefer to build it with web forms? Try out our [visual form builder](https://usetrmnl.github.io/trmnl-form-builder/) to generate the YAML you need. # How it works **Step 1 - Plugin author (you) creates custom fields** **Step 2 - Plugin user (including you) can interact with rendered fields** **Step 3 - Provided values are accessible inside the Markup Editor** ###{{ trmnl.plugin_settings.custom_fields_values }} # => { "password": "secret", "lookback_period": "7" } These values are also accessible from the settings themselves, for example this field: - keyname: api_key field_type: string name: API Key description: Find this at my-server.com/settings Can be leveraged in a Polling Header or Polling URL with just the `keyname`: https://my-server.com/api/stats?access_token=###{{ api_key }} For real-life examples, fork a few published Recipes: ​ # Supported field types Where possible, TRMNL follows [HTML5 input types](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/HTML5_input_types) to leverage native browser validations and UX features. Our current selection includes: - author_bio - url - string - multi_string (store a string with multiple comma separated values) - text (textarea) - code (like textarea, but monospace font) - number - password - date (M Y D) - time (HH:SS) - select, xhrSelect (single or multi) - xhrSelectSearch - plugin_instance_select - time_zone - copyable (produces 'click to copy' button) - copyable_webhook_url (produces 'click to copy' with a plugin's UUID) - boolean Coming soon: - radio (binary toggle) - email # Anatomy of a form field The following key/value pairs are available to all field types: - `keyname` - parameterized identifier, not user-facing, ex `user_email` - `field_type` - one of the above "type" values - `name` - field label - `description` - secondary field label, HTML-friendly - `description-LOCALE` - provides option to localize a `description` field in 1 or more [supported locales](https://github.com/usetrmnl/trmnl-i18n/tree/main/lib/trmnl/i18n/locales/web_ui) (ex: `de`, `es-ES`, `ja`, etc) - `help_text` - tertiary field label, HTML-friendly (optional) - `optional` - boolean; unless set to true, `required` will be added to HTML input (omit if field should be required) - `default` - value if input is left blank; must be a lowercase, parameterized version of one of the provided `options` values or will not be pre-chosen on `select` field type. example: "Upcoming Movies" option => `upcoming_movies` as `default` - `group` - used to combine multiple fields into a dropdown accordion for visual organization The following key/value pairs are available for *some* field types: - `rows` - number of visible rows on a `text` or `code` field - `options` - collection of option values for the `select` field - `value` - string to be added to a user's clipboard via `copyable` field - `min` - minimum integer allowed in a `number` field - `max` - maximum integer allowed in a `number` field - `step` - allowed decimal place for a `number` field - `maxlength` - maximum number of characters allowed in a `string` or `text` field - `placeholder` - example value if input is cleared; supported by `url`, `string`, `text`, `number`, `code` - `endpoint` - URL from which to return options that populate an `xhrSelect` or `xhrSelectSearch` field - `http_verb` - HTTP method (GET, POST, etc) with which to make requests to `xhrSelect` and `xhrSelectSearch` fields; defaults to `POST` When building a form field, only the following values are required: - keyname, name, field_type # Building form fields Below are examples of every supported field type, which you may modify with optional parameters described above. 📌 **Important** As the form field definition is YAML based, every field must be indented by 2 spaces, with the first line denoted with a leading hyphen (`-`), also called a [block sequence](https://www.tutorialspoint.com/yaml/yaml_collections_and_structures.htm). ## author_bio This field operates like a README. It appears below your plugin's preview image on logged out + logged in pages. Here you can provide a description, verbose installation instructions, and more. This field type also has specialized properties, rendered as clickable icons on your plugin as opposed to plain text links. Optional special properties include: - `email_address` (if you want users to contact you) - `category` (up to 2x, comma-separated from approved list below) - `github_url` (can point to your own profile or source code) - `learn_more_url` (feel free to provide your own website, etc) - `youtube_url` (if you made a demo video) - keyname: doesnt_matter name: About This Plugin category: life,news field_type: author_bio description: Dad Jokes Daily™ was created by Abraham, father to many. github_url: https://github.com/father-abraham learn_more_url: https://www.bible.com/ email_address: fatherabe@hotmail.net youtube_url: https://www.youtube.com/@useTRMNL Categories increase your visibility in search results. Categories currently include `analytics`, `art`, `calendar`, `comics`, `crm`, `custom`, `discovery`, `ecommerce`, `education`, `email`, `entertainment`, `environment`, `finance`, `games`, `humor`, `images`, `kpi`, `life`, `marketing`, `nature`, `news`, `personal`, `productivity`, `programming`, `sales`, `sports`, `travel`. For an up-to-date list of categories, just poll the following URL from our Public API ([docs](https://trmnl.com/api-docs)): https://trmnl.com/api/categories **Note**: Recipe `author_bio` values are cached for ~2 hours. If you update a plugin's category, it will take up to 2 hours for it to appear in search results. ## url - keyname: url field_type: url name: Web Address description: URL of RSS Feed placeholder: https://www.site.com/rss help_text: Please include http:// or https:// ## string - keyname: api_key field_type: string name: API Key description: Account API Key placeholder: sa_api_key_qwerty With maxlength attribute: - keyname: nickname field_type: string name: Nickname description: Shown in the corner of your screen. maxlength: 12 placeholder: jimbob help_text: Can be up to 12 characters long. ## multi_string - keyname: device_ids field_type: multi_string name: Device Id(s) description: List of device ids to fetch data from placeholder: id_xxx ## text (textarea) - keyname: prompt field_type: text name: Prompt description: Enter your prompt for Chat GPT to respond. placeholder: Tell me a Dad Joke under 200 words. With multiple description translations: - keyname: prompt field_type: text name: Prompt description: Enter your prompt. description-es-ES: Ingrese su mensaje description-ko: 프롬프트를 입력하세요 placeholder: Tell me a Dad Joke under 200 words. ## number - keyname: retirement_age field_type: number placeholder: 65 name: Retirement Age description: At what age do you plan to retire? With decimals allowed: - keyname: height_in_feet field_type: number placeholder: 5 step: 0.1 name: Height With minimum and maximum allowed values: - keyname: posts_to_show field_type: number placeholder: 3 min: 1 max: 5 name: Number of Posts description: How many stories would you like to see? help_text: Maximum 5. ## password - keyname: password field_type: password name: Password description: Mobile app login password placeholder: s3cret! ## code - keyname: json_query field_type: code rows: 8 name: JSON Query description: Provide a raw query placeholder: | { "cannot": "spare a square" } help_text: Learn how to do this here. ## date - keyname: start_date field_type: date default: "2025-07-04" # YYYY-MM-DD; must be wrapped in quotes name: Start Date description: Provide your start date When using the date field, the rendered form will display a date picker. However, the value inside the template object `###{{ trmnl.plugin_settings.custom_fields_values }}` will be in the `YYYY-MM-DD` format. **💡 Hint** You may also leverage `default: "today"` to pre-populate the current day. ## time - keyname: scroll_time field_type: time name: Fixed start time? description: By default, the week's earliest event time will be used. help_text: Optional. Applies to week view only. ## select **📌 Important** If you'd like to use the options "Yes" and "No" in your field, and you'd like to set the default option to be "No," you must wrap the default in quotes as "no" or it will be considered truthy. **Example:** - keyname: include_description field_type: select name: Include Description description: Select up to 3 statuses options: - "Yes" - "No" default: "no" # unless wrapped in quotes, value will be true **select** (single, unified label/value option tags) - keyname: filter_by field_type: select options: - Upcoming # value will be 'upcoming' - Now Playing # value will be 'now_playing' default: now_playing name: Filter By description: Select a movie type ​**select** (single, with separate label/value option tags) - keyname: lookback_period field_type: select options: - One Week: 7 - Two weeks: 14 default: 14 # should be set to the value, not the label name: Lookback Period **select** (multi) - keyname: sales_pipeline field_type: select name: Pipeline Statuses description: Select up to 3 statuses options: - 'Discovery' - 'Pending' - 'Closed-lost' - 'Closed-won' multiple: true **💡 Hint** When setting `multiple: true` , it's helpful to clarify how this works to end-users. Here is some recommended `help_text` and how it renders: Use +click or ctrl+click to select and deselect multiple options. ## xhrSelect - keyname: team name: Baseball Team field_type: xhrSelect endpoint: https://trmnl.com/custom_plugin_example_xhr_select.json The URL assigned to the `endpoint` key should respond to POST requests and return an array of options. For example: # key/value, where key is user-facing label and value is stored in db [{ 'Braves' => 123 }, { 'Yankees' => 456 }] **ℹ️ Note** The provided endpoint will be sent a POST request by default. Set `http_verb: ` to change it. Payload will include sibling settings (default + custom fields), which may be useful for providing a conditional dropdown user experience. You may also wish to disable the `X-CSRF-Token` header. To do so, set `exclude_csrf_token: true`. ### depends_on xhrSelect fields have a special property available only to them called `depends_on` that creates chained dropdowns—where a child field's options depend on the parent field's selected value. - keyname: team name: Baseball Team field_type: xhrSelect endpoint: https://trmnl.com/custom_plugin_example_xhr_select.json - keyname: player name: Player field_type: xhrSelect depends_on: team endpoint: https://trmnl.com/custom_plugin_example_xhr_select_###{{team}}.json When the user selects "Yankees" (value: 456), the player endpoint becomes: . Behavior: - The child field shows "Select Team first" until a parent value is selected - When the parent changes, the child automatically refreshes - The parent field must appear before the child in your YAML There are two ways to access the parent value: 1. URL Interpolation: Use ###{{keyname}} in your endpoint URL. Ideal for static JSON files or third-party APIs: 1. - keyname: player depends_on: team endpoint: https://example.com/teams/###{{team}}/players.json 2. Request Body: The parent's value is also included in the POST body as a sibling setting. Ideal when your backend needs to combine the parent value with stored credentials or make authenticated API calls: 1. { "function": "player", "plugin_setting": { "settings_custom_fields_values_team": 456, ... } } ℹ️ **Note** The key format is `settings_custom_fields_values_ + the parent's keyname`. So if your parent field has `keyname: team`, look for settings_custom_fields_values_team in the request body. Your backend can read `params[:plugin_setting][:settings_team]` to fetch players for that team using your own API credentials. 💡 **Hint** Both methods work simultaneously. You can interpolate the URL AND read from the body in the same request. ## xhrSelectSearch An extension of `xhrSelect`, this field includes a dynamic search input and is a better choice for dropdowns with several options. - keyname: project name: Project field_type: xhrSelectSearch endpoint: https://trmnl.com/custom_plugin_example_xhr_select_search.json​ ℹ️ **Note** The provided endpoint will be sent a POST request by default. Set `http_verb: ` to change it. Search query will be inside a `query` body parameter for filtering on your server. If you set the `http_verb` to GET, the search query will be inside the query string itself. ​ Example responses from our test endpoint: # https://trmnl.com/custom_plugin_example_xhr_select_search.json​ # query=null [ { 'id' => 'db-123', 'name' => 'Project Tasks' }, { 'id' => 'db-456', 'name' => 'Team Goals' }, { 'id' => 'db-789', 'name' => 'Family Goals' } ] # query=goa [ { 'id' => 'db-456', 'name' => 'Team Goals' }, { 'id' => 'db-789', 'name' => 'Family Goals' } ] You may also wish to disable the `X-CSRF-Token` header. To do so, set `exclude_csrf_token: true`. ## plugin_instance_select This field type creates a dropdown populated with the user's active plugin instances. It enables templates using plugin_merge strategy to reference plugins via form field keynames (`#{{ my_weather.temp }}`) instead of hardcoded IDs (`#{{ weather_42.temp }}`). This is particularly useful for recipes where the actual plugin instance isn't known at template creation time. ℹ️ **Note** - Requires the recipe to be forked. - Only plugins that are currently in a playlist or mashup are shown in the dropdown. - This only works for the "plugin_merge" strategy - [learn more](https://docs.trmnl.com/go/private-api/plugin-data). - keyname: weather_source field_type: plugin_instance_select name: Weather Data Source plugin_keyname: weather description: Select which Weather plugin instance to pull data from help_text: Only your active Weather plugins are shown. The selected value will be in the format `#{{ weather_source }}` Accessing data: Temperature: #{{ weather_source.temperature }}° Humidity: #{{ weather_source.humitidy }} ## time_zone - keyname: time_zone field_type: time_zone name: Time Zone description: Where are you located? ## copyable - keyname: shared_api_key field_type: copyable name: API Token value: qwerty-fdsa-1234 description: Paste this into your Vandelay Industries account. ## copyable_webhook_url - keyname: webhook_url field_type: copyable_webhook_url name: Webhook URL description: Paste this into your Apple Shortcut help_text: This will populate once the plugin has been created/saved. ## boolean *Value should either be* `true` *or* `false` - keyname: show_timestamps field_type: boolean default: true name: Timestamps Enabled? description: Shows a timestamp in the bottom corner. # Groups If your plugin has several custom fields, you may want to separate them visually with groups. Below is how our calendar plugins organize options by type: When clicked, each heading expands to show a handful of form fields. Encapsulate fields inside these accordions with the `group` parameter, for example: - keyname: include_past_events name: Include past events? group: "Event Display" field_type: select options: - 'Yes' - 'No' default: 'no' # Conditional field validations As described above, all fields are `required` unless `optional: true` is included in the field's YAML definition. Further, all fields are visible. But there may be cases where Field B should only be required or visible if Field A has a given value, and vice versa. For this scenario we offer conditional validations. To get started, attach `conditional_validation` to the (parent) field whose value (or lack thereof) should dictate the behavior of one or more other fields. Then nest one or more of the strategies below. ## Conditional visibility **💡Use Case** Show field B unless field A's value is `xyz`, but show field C (and hide field B) if field A's value is `abc`. Any combination is possible. Here's an example from our native Private Plugin architecture: - keyname: strategy field_type: select options: - Polling - Webhook - Static default: polling conditional_validation: - when: polling hidden: - static_data - webhook_url - when: webhook hidden: - polling_url - polling_verb - polling_headers - polling_body - static_data - when: static hidden: - polling_url - polling_verb - polling_headers - polling_body - webhook_url The logic above will show or hide several fields based on the selected value of the "Strategy" dropdown. **ℹ️ Note** The property `when: xxx` means, "when this field has value `xxx`". Thus, when the Polling dropdown value is set to `webhook`, 4x `polling_*` fields are hidden in addition to the `static_data` field. ## Conditional "required" state **💡Use Case** Make field B's value dependent on the value of field A. Here's an example from our native Weather plugin, which has 2x data providers (WeatherAPI, Tempest) and 2x location-related fields. If a user selects the Tempest data provider, `lat_long` is required. But if a user selects the WeatherAPI data provider, the `location` field is required. - keyname: data_provider field_type: select options: - Tempest - WeatherAPI default: tempest name: Data Provider description: Provides underlying metrics and layout design. conditional_validation: - when: "tempest" required: ["lat_lon"] - when: "weatherapi" required: ["location"] - keyname: lat_lon field_type: string name: Latitude/Longitude (Tempest only) placeholder: -28.001499,153.428467 description: Provide your location. optional: true - keyname: location field_type: string name: Location (WeatherAPI only) placeholder: Atlanta, GA, USA description: Choose a location optional: true This treatment produces the following user experience: # Troubleshooting ## Form fields don't appear If your YAML cannot be parsed for any reason, it will be ignored and no errors will be returned. Check your YAML syntax inline with our syntax checker, located above the form fields input box. This will save you from losing your work due to incorrect formatting. If you don't want to use our inline verifier, here's another free tool that you can use to parse your fields before click Save. --- # Stock Price Picking the stocks you want to keep up with takes a few seconds and doesn’t require any coding. ## **Step 1 - Visit the Stock Price plugin** Inside TRMNL, navigate to Plugins > Stock Price. Here you can: - Name your Plugin - Provide tickers (supports Canadian, German, London, Indian, USA markets) Indexes are not supported. Please use their ETF equivalents. For example: - DJI becomes DIA - SPX becomes SPY We currently support the following metals - XAUUSD (Gold) - XAGUSD (Silver) - XPTUSD (Platinum) - XPDUSD (Palladium) - HGUSD (Copper) Currency pairs work via the .FX suffix (e.g. USDJPY.FX, EURUSD.FX). To track crypto prices, use the CoinMarketCap plugin. ## **Step 2 - You're Done!** Based on your desired refresh settings, TRMNL will begin showcasing the most recent price and daily change very soon. **Troubleshooting** - You can show up to 12 tickers at a time. We suggest creating multiple instances of the Stock Price plugin, then building a Mashup layout from your Playlists tab, if you want to see more on the same screen. - To edit a ticker, create a new plugin instance with the correct symbol and delete the incorrect plugin instance. - Some currencies like EUR will use a "," separator versus "." for decimals - If you submit a ticker that is invalid, it will show the code you entered with no price - Some tickers require a different format than their trading syntax. [Read this post](https://www.marketdata.app/education/stocks/how-to-look-up-preferred-stock-symbols/) to learn more, for example NASDAQ should be input as "LFMDP". Another resource for figuring out tickers is [here](https://eodhd.com/financial-apis/covered-tickers-eodhd). --- # API Catalog Ever have the urge to hack on something, but you don't want to research for available data? Introducing the API Catalog. Inside TRMNL > [Plugins > Private Plugins > New](https://trmnl.com/plugin_settings/new?keyname=private_plugin), select the Polling strategy to expose the catalog. Search by category or name. Endpoints requiring an API Key will provide instructions to create one. Others will have a `Free` badge, with URLs you can copy/paste directly to get started. Hint: if you use our [AI Agent](https://help.trmnl.com/en/articles/14130438-ai-agent) to build plugins, it also has access to this catalog, as well as every (published) native + community plugin. --- # Plugin Markup Version Control If you're like me, you've lost countless essays to computer crashes and have been smashing `cmd+s` every 8 seconds for the last 20 years. TRMNL is here to alleviate that habit. ## Demo Inside the [Markup Editor](https://markup), switch between versions with the left and right arrows. A new version is created whenever you click "Save" or type `cmd+s` (Mac) or `ctrl+s` (Windows). ## **How it Works** This feature lives inside your browser, leveraging the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API). If you goof up some code, our team will never know. You may clear your history at any time, or jump between versions from the timestamped table. --- # UI Framework versioning Our [UI Framework](https://trmnl.com/framework) is constantly growing and improving. And as a plugin author, the last thing you want is to scramble to fix a library of custom plugins after a new Framework release has unintended side-effects. That's why we've introduced Framework versioning. Private Plugins are now tied to a specific Framework release that won't change until you want it to. The version can be upgraded or downgraded at any time. Plugin authors are encouraged to upgrade Framework at their convenience. Framework releases contain bug fixes, new features, and support for an ever-growing pool of screens and devices. If your middle name happens to be "Danger", select "Always track latest", which means your plugin will always use the newest, bleeding-edge Framework release the moment it's published. --- # Grayscale: 1-bit, 2-bit, 4-bit in Framework The plugin design [Framework](https://trmnl.com/framework/background) is a very powerful tool for designing plugins that not only look good, but also function reliably across multiple device sizes and capabilities. This includes tradeoffs that users make when they prefer faster refresh over more color detail. Part of designing for that framework is understanding the classes that are **exact matches** to device capabilities. `*--gray-55` is meant to represent multiple classes, such as `bg--gray-55` or `text--gray-55`, with the asterisk (`*`) acting as a placeholder. ## 1-Bit (Black & White) This option uses [dithering](https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering) to approximate 14 shades of gray. If you only wish to use non-dithered classes, they will be in the format: - `*--black` - `*--white` ## 2-Bit (Grayscale - 4 shades) This option uses [dithering](https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering) to approximate 12 additional shades of gray. If you only wish to use non-dithered classes, they will be in the format: - `*--black` - `*--white` - `*--gray-30` (darker gray) - `*--gray-55` (lighter gray) ## 4-Bit (Grayscale - 16 shades) Supported by our [TRMNL X](https://shop.trmnl.com/products/trmnl-x) product, no dithering is necessary to display 16 shades of gray. # A Note About Temperature Settings Some displays may offer the ability to adjust the *[temperature](https://help.trmnl.com/en/articles/12566040-tuning-a-washed-out-display)*[of the display](https://help.trmnl.com/en/articles/12566040-tuning-a-washed-out-display) due to features added to the firmware, allowing the user to *nudge* the grays slightly lighter or darker to match their personal preferences or to compensate for variations between e-ink displays. While the Framework classes will always align, the exact shade of gray will vary from device to device. --- # Sleep Mode Inside TRMNL > [Devices](https://trmnl.com/devices) > Settings are a few options related to battery consumption. Key among them is Sleep Mode. # What is Sleep Mode? Whether TRMNL is on your desk or your kitchen counter, you're probably not there to look at it 24 hours per day. At least, we hope not. To save power (and recharge your device less often) we offer Sleep Mode. Enabling this will instruct your device to not* wake up and fetch content (the usual behavior) for an extended period of time, determined by you. If your device is set to fetch new screens every 15 minutes, for example, you can enable Sleep Mode from 10p - 6a (example) and reduce your daily refreshes by 32x, or 4x per hour, for 8 hours. Each refresh expends just a tiny bit of power, but eliminating 32x refreshes per day that you'll never see can add weeks to your battery life. See advanced details about TRMNL power consumption [here](https://github.com/usetrmnl/firmware/?tab=readme-ov-file#power-consumption). # What is the Sleep Screen? When your device is asleep, we figure you want to know about it. That way you don't assume it's 105 degrees outside when in fact, that was the weather 5 hours ago. Our simple approach to this is a sleep screen, which looks like this: But some TRMNL owners prefer their device to linger on the last-shown content, which we respect. Simply disable "Sleep Screen" to show the last piece of content while your device sleeps. It will not expend any more energy to do this, and there is no risk of "screen burning" given all sleep periods are <= 24 hours. # Customizing your Sleep Screen Do you prefer to design your own sleep screen? Have you tried [our tutorial](https://help.trmnl.com/en/articles/12387950-create-a-custom-sleep-screen)? # Setting Sleep Mode Programatically You can now send an API request to your TRMNL device to initiate sleep mode. - **Endpoint**: `/api/devices/{id}` - **METHOD**: PATCH - **Example Payload**: { "sleep_mode_enabled": true, "sleep_start_time": 1320, "sleep_end_time": 450 } - Times should be set as **seconds since midnight***,* *e.g.* **1320** (22:00) and **450** (07:30). --- # Private Plugins TRMNL customers with the Developer add-on or a BYOD license may use our API to quickly and easily create a plugin for themselves and others. Here's a 5 minute demo that showcases what's possible, and how easy it is to get started. Only limited coding knowledge is required. [https://www.youtube.com/embed/Ofb-mp_x_gM?rel=0](https://www.youtube.com/embed/Ofb-mp_x_gM?rel=0) # Step 1 - Upgrade your Account While logged into TRMNL, hover the device picker dropdown and click on the gear cog icon. Scroll down to "Developer perks" and click to upgrade. This one-time unlocks several new features on your account, forever. Learn more [here](https://trmnl.com/blog/developer-edition). If you chose the "Developer edition" package during purchase, this will automatically be applied on your account and no additional action is required. If you are using a [BYOD license](https://docs.trmnl.com/go/diy/byod#build-from-scratch-advanced) with 3rd party hardware, no additional action is required. # Step 2 - Create a Private Plugin From the Plugins tab, search for "Private Plugin" or "Public Plugin" to begin building. Below we'll follow along with a Private Plugin. These may still be shared with others as [Recipes](https://help.trmnl.com/en/articles/10122094-plugin-recipes), but for even more control, consider [Public Plugins](https://docs.trmnl.com/go/plugin-marketplace/introduction) or [compare all types](https://help.trmnl.com/en/articles/10546870-compare-custom-plugin-types). Let's start with the **Name**. This is for your eyes only, and will help you organize from both the Playlists interface and list view. You can build as many plugins as you'd like. Next is your plugin's **Strategy**. This determines how TRMNL will receive data that gets rendered on a device screen. **Strategy - Webhook** If you choose Webhook, you'll need to set up your own web service that sends a POST request to TRMNL with a payload of content (max 2kb size). You don't necessarily need a server for this option -- consider [Apple Shortcuts](https://intercom.help/trmnl/en/articles/10108737-custom-plugins-with-apple-shortcuts), [Google Sheets](https://help.trmnl.com/en/articles/11400219-using-google-sheets-with-private-plugins), Google Sheets App Scripts (with automation timers), free Cloudflare Workers, a free REPLit account, GitHub Actions, or a free PythonAnywhere account. Learn how to send webhooks [here](https://docs.trmnl.com/go/private-plugins/webhooks). ​ ​**Strategy - Plugin Merge** If you choose [Plugin Merge](https://docs.trmnl.com/go/private-api/plugin-data#how-it-works), all your plugin data will be available to use in your Private Plugin template. This feature unlocks the full freedom to create a plugin from any combination of parsed data. ​ Available data can be seen in the "Your Variables" section of the "Edit Markup" section. **Strategy - Polling** If you choose Polling, TRMNL will fetch this content on your behalf. **Polling URL(s)** If you choose the Polling strategy, TRMNL needs to know where to fetch this data. Provide 1 or more URLs, line-break separated, in the text box. The response payloads of these URLS will be available as `IDX_` (index-based) nodes within the `###{{ merge_variables }}` payload on the Markup Editor view, explained later. Polling URLs may be of content type JSON, RSS, XML, plaintext, or CSV. They may access values from custom form fields via `###{{ form_field_keyname }}` interpolation. See more details below in "Form Fields." For more advanced Polling URL functionality, [go here](https://help.trmnl.com/en/articles/12689499-dynamic-polling-urls). **Polling Verb** Defaults to GET, but POST is also available. **Polling Headers** You may also add headers to your Polling URL (optional). Assign header key/values with = and separate them with `&`. So `authorization=bearer xxx&content-type=application/json` becomes: ​ { "authorization":"bearer xxx", "content-type":"application/json" } If a key/value requires an `=` sign in the value, for example `bearer jwt==`, simply encode it as `%3D`, so `bearer%20jwt%3D%3D`. You can also use `apikey=#{{ api_key | url_encode }}` to programmatically encode the key as required. You can even use this field to override default headers such as the `user-agent` header by including something like `user-agent=your_agent`. To get started quickly, we've prepared a public endpoint with simple JSON in the response: `https://trmnl.com/custom_plugin_example_data.json` To test how a pure collection response is handled, use this: ​`https://trmnl.com/custom_plugin_example_data.json?collection_only=true` To confirm your headers are formatted correctly, make a GET to our example endpoint above with `authorization=token qwerty` as one of your header values for a special surprise in the response. ;) **Note**: your polling headers may access values from custom form fields via `###{{ form_field_keyname }}` interpolation. See more details below in "Form Fields." **Polling Body** The last of several Polling strategy options. Here you can provide a JSON object to be included as a body payload. **Note**: your polling body payload may access values from custom form fields via `###{{ form_field_keyname }}` interpolation. See more details below in "Form Fields." **Form Fields** Another optional feature. Here you can create your own GUI for this plugin, and then interpolate those values into your markup, polling URL, polling body, or polling headers. Learn how to build custom fields [here](https://intercom.help/trmnl/en/articles/10513740-custom-plugin-form-builder). **Dynamic Values inside Polling URL, Body, Headers** To access form field values in your Polling URL, Polling Body, or Polling Headers, leverage the `keyname` you assigned in the Form Fields YAML definition like so: # Polling URL - static https://my-store.com/api/v1/products?api_key=qwerty-asdf # Polling URL - using form field values https://my-store.com/api/v1/products?api_key=###{{ api_key }} # Polling Headers - static authorization=bearer token-here # Polling Headers - using form field values authorization=bearer ###{{ api_key }} To access global variables (including the installed user's name, timestamp, etc) in your Polling URL, Polling Body, or Polling Headers: https://my-service.com/api/v1/products?api_key=###{{ api_key }}&first_name=###{{ trmnl.user.first_name }} **Remove Bleed Margin** Finally, decide whether you want to remove the small amount of padding (5-10 pixels) that is added to all plugins by default, including our native ones. We suggest building your plugin first, then deciding if this is necessary. This option is primarily for plugins that want to display image content edge-to-edge. It is not a great solution for fitting more text on the screen. Click Save in the top right to finish stubbing out your plugin. # Step 3 - Design your Plugin **Markup** is the last and most important configuration. This is the frontend HTML template for your data to be merged into. After saving your plugin above, click the "Edit Markup" button from your plugin settings page. Expand the "Quickstart Examples" section to start working with valid markup right away. Then use our [framework docs](https://trmnl.com/framework) for help designing the perfect screen. Each tab represents a different [Mashup layout](https://help.trmnl.com/en/articles/10168132-mashups) available to you. The Shared tab is where you can define reusable components, bits of JavaScript, shared stylesheets, and more. Learn more about the Shared markup [here](https://docs.trmnl.com/go/reusing-markup). TRMNL supports the popular [Liquid](https://github.com/Shopify/liquid) templating engine to handle merge variables, which means you can leverage `###{{ variable }}` syntax in your Markup to merge in data when you want to create a new screen. If you chose "Polling" as your Strategy in the previous step, click the "Force Refresh" from the screenshot above to grab fresh data. Then you can work these variables into your markup like so: Liquid is a powerful templating library, complete with `for` / `each` loops as well as filters like `###{{ text | truncate: 15 }}` and many more. You may also notice some **global variables** inside a `###{{ trmnl }}` namespace from the "Your Variables" dropdown. These are optional values that may be useful to customize your plugin's markup, for example by merging in a TRMNL user's first name, or leveraging their time zone to make a client-side calculation. Liquid 101 guide: ​ Unofficial Liquid docs: Advanced, TRMNL-specific filters: [https://help.trmnl.com/en/articles/10347358-custom-plugin-filters](https://intercom.help/trmnl/en/articles/10347358-custom-plugin-filters) **Multiple Polling URLs** When you provide multiple URLs in the Polling URL field, each response becomes available as a separate variable: - First URL response: `###{{ IDX_0.field_name }}` - Second URL response: `###{{ IDX_1.field_name }}` For example, if your first URL returns `{"name": "John", "status": "active"}`, access it with `###{{ IDX_0.name }}` and `###{{ IDX_0.status }}`. If you only have one polling URL, the data is available directly as `###{{ field_name }}`without the `IDX_` prefix. # Step 4 - Share your Work Totally optional, but we'd love to see what you built. From your private plugin's settings page, click Publish as a Recipe on the right side. Our linter, named Chef, will run a few helpful checks to make sure your work is in good shape. Then our team will be notified to review + publish your plugin, which usually takes just a day or two. You may also join the Developer-only Discord server (invite link in [your Account page](https://trmnl.com/account)) or tag us on social media. If you prefer to share code only, but not have any connection with user data, consider PR-ing your work into [our OSS collection](https://github.com/usetrmnl/plugins). # Whitelisting Server IPs If you host your own data source for the polling strategy, you may wish to lock down your API so that it is not available to the wider Internet. This can be done via IP address whitelisting on your server. The TRMNL core server IPs to whitelist are published at # Troubleshooting **I don't see any content** Click 'force refresh' from the Plugin Settings page to request a new screen generation at any time, versus waiting on your device to refresh on a schedule. If you select Webhook as your strategy, you must `POST` data to the TRMNL server. Instructions to build this URL are available in our [API docs](https://docs.trmnl.com/trmnl-internal-documentation/custom-plugins/create-a-screen#create-a-screen-webhook-strategy). The latest private plugin screen renders will appear first inside the Plugins > Private Plugin UI, then* on your device (when it requests new content). **Mashups with my private plugin are broken or duplicated** See [Troubleshooting Mashups](https://help.trmnl.com/en/articles/10168132-mashups#h_d8837b6acf). **My JavaScript isn't doing anything / only works in live preview** Due to our rendering logic, you'll have more success with client side mutations by wrapping your JS like this: ​ document.addEventListener("DOMContentLoaded", function(e) { // code goes here }); This strategy is more reliable than `window.onload = function () {}` or other approaches. **I'm sending data but the screen isn't refreshing** By default, our backend skips generating screens if the merge variables are the same between requests. This applies to private and native plugins. If you're attempting to "force generate" a screen, perhaps in order to test new Markup or styling, you can work around this by by clicking the "Force Refresh" feature below your Plugin UUID. **Payload data isn't merging into my markup template** If you select Polling as your strategy, we suggest putting relevant merge variables in the root node of your payload. GET /some-endpoint.json # works { name: "Jimmy", age: 43 } # won't work unless markup uses ###{{ data.name }}, ###{{ data.age }} etc { data: { name: "Jimmy", age: 43 } } If your root node is an array, you can access it via `data[0]`, `data[1]`, and so on. Get started with Liquid syntax [here](https://help.trmnl.com/en/articles/10671186-liquid-101), or read our advanced guide [here](https://help.trmnl.com/en/articles/10693981-advanced-liquid). --- ***Unhappy with this article?*** *Let us know what you were searching for and why this article missed the mark for you. Email [support@trmnl.com](mailto:support@trmnl.com?subject=Private%20Plugins%20Article%20Feedback) and let us know (use the link to automatically include the article title)!* --- # Dynamic Polling URLs The [Private Plugin](https://help.trmnl.com/en/articles/9510536-private-plugins) "Polling" strategy offers a few ways to gather data for your plugins. # Static Endpoints Provide as many URLs as you'd like, line-separated in the Polling URL box. Each endpoint can yield a different format (JSON, XML, etc), so long as they all work with the same HTTP Verb, defined separately. Endpoints will be pinged sequentially, with results going inside index-based JSON nodes like so: { "IDX_0": {"data": "stuff here from some-data-source.com" }, "IDX_1": {"data": "stuff here from anothersite.io" }, "IDX_2": {"data": "stuff here from my-server.net" } } # Dynamic Endpoints Polling URLs become more powerful by interpolating values into them, for example: https://twitter.com/api/tweets?access_token=###{{ api_key }} This `api_key` value points to a custom form field with "api_key" set as the `keyname`. Learn how to build form fields [here](https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder). In addition to user-provided values, you have access to the entire Liquid templating library. Here's an example that interpolates today's date: # convert "now" time helper into YYYY-MM-DD https://reddit.com/api/posts.json?since=###{{ "now" | date: "%Y-%m-%d" }} # result https://reddit.com/api/posts.json?since=2025-10-29 For more examples of advanced Liquid syntax, see [here](https://help.trmnl.com/en/articles/10671186-liquid-101) and [here](https://help.trmnl.com/en/articles/10693981-advanced-liquid). For custom filters (or to propose one for us to build), go [here](https://help.trmnl.com/en/articles/10347358-custom-plugin-filters). # Dynamic URLs Putting together the concepts of multiple URLs and value interpolation, you may also use Liquid to build an arbitrary set of URLs at runtime. Suppose you want a dashboard of every plugin you've published for other users to install, like this one by [Daniel Sitnik](https://github.com/danielsitnik/): To achieve this you can define a custom form field, `recipe_ids` with either the "string" or "multi-string" field type: Which creates the following user experience: Next, inject Liquid into your Polling URL markup that iterates those user values: {% assign ids = recipe_ids | split: "," %} {% for id in ids %}https://trmnl.com/recipes/###{{ id }}.json {% endfor %} This will generate URLs on the fly, like so: https://trmnl.com/recipes/49610.json https://trmnl.com/recipes/18267.json https://trmnl.com/recipes/11914.json You can even define URLs without form field inputs, for example: {% assign recipe_ids = '49610,18267,11914' %} {% assign ids = recipe_ids | split: "," %} {% for id in ids %}https://trmnl.com/recipes/###{{ id }}.json {% endfor %} This will yield the same result as above. # Troubleshooting **Testing syntax** Use the "Parse" button below the Polling URL box to test your Liquid markup without waiting for a refresh or clicking Save. **Managing line breaks** On the backend, TRMNL first injects your Polling URL content into the Liquid template engine, *then* splits the resulting content into separate URLs by detecting line breaks (`\r`, `\n` characters). If your Liquid syntax creates empty line breaks, that's OK, they will be ignored. But if your markup renders like below, it will be considered 1 URL and not behave as you expect: --- # Dropbox Photo Album Connecting Dropbox to TRMNL takes just a few seconds and is done securely via our OAuth2 integration. ## Step 1 - Visit the Dropbox Photo Album Plugin Inside TRMNL, navigate to Plugins > Dropbox Photo Album. Click "Connect with Dropbox Photo Album" on the right side of the screen. ## Step 2 - Connect Dropbox After being redirected to Dropbox, approve TRMNL's permission request. We do not have or request the ability to edit files, only to read files and share files. Shared files are also limited to a specific folder inside Dropbox. Permitting TRMNL to access your Dropbox does not allow access to all folders. ## Step 3 - Create a photo album Inside your Dropbox account, find the folder called "Apps." Open it up. Inside you should find a folder named "trmnl.com" with the note "App folder" beneath it. This means it was automatically created. Put as many photos as you'd like inside this folder, or any dimension, but ideally matching your intended device use. If you set your TRMNL to portrait mode, upload portrait images, and so on. ## **Step 4 - You're Done!** Based on your chosen refresh settings, you'll begin seeing photos from Dropbox very soon. Stay focused. --- # What is fast refresh? In August of 2025, TMRNL released **fast refresh** in firmware 1.6.x. We announced it with a blog post, "[No more flicker](https://trmnl.com/blog/no-more-flicker)," which contains images and videos providing some examples. But what does it mean? # Where We Started When our TRMNL (OG) 1-bit device changed screens using the default EPD (Electronic Paper Display) drivers, the manufacturer's spec did not support *fast refresh*, so whenever the screen changed, it would need to display all-black and all-white before displaying the content. While this *flicker* could prove distracting, it was a *normal refresh* to ensure there were no pixels left in the wrong color (black or white). # Changing Only Part of the Screen ***Fast refresh* is the similar to partially updating the screen contents by changing *only* the pixels that need to *flip* from black to white (or visa versa), and leaving the rest of the pixels unchanged.** This makes the screen change nearly instant, a feeling similar to the LCD screens we are exposed to every day. Even with this *fast refresh*, it's necessary to do a *normal refresh* every once in a while, due to an effect called **ghosting**, where a pixel doesn't properly change, leaving a *ghost* of the previous state. This can leave the screen muddy, gray, or unclear until a *normal refresh* occurs. We perform a *normal refresh* after a regular interval, or when switching between bit depths. # Fast Refresh and Bits At the same time, we released 2-bit support (4 shades: Black, white, light gray, dark gray) on a 1-bit EPD that didn't support that feature with the default driver; so we replaced it. However, that left *fast refresh* as a choice, between it and 2-bit images, because due to EPD limitations, we couldn't have both at the same time. So if the device was going to display a 2-bit image, then some *flashing* was going to return, while if the image was 1-bit, we could use *fast refresh* and make the screen transition very fast (as far as the user can see). It's not a choice *you* make, but a choice the device makes to serve you the content you chose, in the format you chose*. * The ability to choose 1-bit or 2-bit rendering for a particular plugin will be released in late 2025. --- # Playlist Scheduler First some terminology: - **Plugins** are content shown on your device - **Playlists** are that content's schedule This guide will walk you through the Playlist editor. To learn more about building plugins, [go here](https://help.trmnl.com/en/articles/10546870-compare-custom-plugin-types). # Playlist Items A playlist item is a single piece of content that represents a single plugin or [Mashup](https://help.trmnl.com/en/articles/10168132-mashups). From left to right: - A running number that indicates its position within the playlist - The icon, decided by its author (TRMNL or a community member) - A thumbnail of this item's most recent render - Title (e.g. "The Office"), item type, and schedule(s) - Badge indicating the last time your device rendered this content - Triple dots to access different options - "Hamburger" icon to drag/drop the ordering of this item relative to others, for example Calendar-Weather-News, or Weather-News-Calendar The tripe dots provide access to these options: - plugin settings (not available for installed community recipes that do not provide further settings via form fields) - direct access to the plugin's refresh rate (how often a new image is rendered) - show/hide this item temporarily (ignored by your device, not disconnected from your account) - manage the item's schedule and the used [color palette](https://help.trmnl.com/en/articles/12985974-understanding-color-palettes) - completely remove this item from your Playlist # Playlist Scheduling You can dictate the timing, duration, and visibility of each playlist item on its own unique schedule - even specify its own color palette. Click "Adjust Schedule" behind the ( i ) icon to see how it works. ## Default duration and schedule If you don't make any changes, each item on your playlist will display 7 days per week for a length of time equal to [your device's](https://trmnl.com/devices/current/battery/edit) refresh rate. The slower your refresh rate, the longer your battery lasts ([more details](https://help.trmnl.com/en/articles/10556850-device-battery-faq)). If you are OK with seeing the same content every day of the week, in the ordering you specifiy via the drag/drop functionality in the playlist, no action is required. ## Custom duration and scheduling Click "Set a custom schedule" to expand the editor. In this example we've updated our Work Calendar playlist item to show for 30 minutes, only during the M-F work week, from 08:00-12:30. ## Multiple schedules Click "+ And during" to set additional schedules for this same item. In this example we've extended our Work Calendar item to also appear on Sunday afternoons from 15:00-21:00, which could be helpful for planning the next week. ## Priority / Importance Below your schedules is a toggle, Important. If you enable this, other items that match the set schedules will be ignored unless they are also set as Important. This can be helpful for users with many different playlist items, who want to ensure that high priority content (like upcoming appointments) is not "buried" by other matching items. You may tag as many items "important" as you want. Just note that if all your items are marked important, then your playlist will behave as if none of your items are marked as important. ## Copy schedule from For common use cases like "hide my work calendar on the weekends" there are a few default options in the "Copy schedule from" dropdown: As you build out custom schedules, for example to see [XKCD comics](https://trmnl.com/recipes/18267) when they're released on M/W/F, you can adopt existing items' schedules into new items, reducing the setup time to make things exactly how you want them. ## Color Palette Our e-ink screens can display different numbers of gray shades, from simple black and white up to 16 or more levels, depending on your device. Larger palettes produce smoother fonts, clearer hierarchy, and better-looking photos, but they require bigger files, slower transitions, and more battery. Smaller palettes are faster and more efficient, but visually simpler. You can set the desired [Color Palette](https://help.trmnl.com/en/articles/12985974-understanding-color-palettes) per playlist item to balance performance and image quality for each use case. ## Time Travel On the playlist page, the *time travel* feature allows you to adjust the day and time and see how your playlist schedules react. **No items or settings are changed using this feature**. It's a great way to verify all your hard work is behaving as expected, without having to just "wait". --- # Compare custom plugin types From easiest to hardest: | **Difficulty**
| **Public** | **Server required?** | **OAuth required?** | **Language** | | --- | --- | --- | --- | --- | | **Private Plugin** | no | no | no | [Liquid](https://docs.trmnl.com/go/private-plugins/templates) | | **Recipe** | yes | no | no | [Liquid](https://docs.trmnl.com/go/private-plugins/templates) | | **Screenshot** | no | yes | no | any | | **3rd Party** | yes | yes | yes | any | | **BYOS** | no | yes | no | any | ## Private Plugin The simplest way to write a custom plugin is as a [Private Plugin](https://intercom.help/trmnl/en/articles/9510536-private-plugins). Feed it data via HTTP polling or webhooks, even [Oauth2](https://trmnl.com/blog/oauth2-plugins). Design a beautiful dashboard with (or without) our [Framework UI](https://trmnl.com/framework/) and we handle the rest. Private plugins can remain private forever, shared offline via [exporting](https://help.trmnl.com/en/articles/10542599-importing-and-exporting-private-plugins), or "Unlisted" for with a hidden URL. ## Recipe Recipes are simply Private Plugins that have been approved by the TRMNL team to be listed publicly. As recipe master, any changes you make will automatically propagate to others who have installed your recipe. Learn more about [building and submitting Recipes](https://intercom.help/trmnl/en/articles/10122094-plugin-recipes). ## Screenshot Another way to get custom content to a TRMNL is to leverage the [Screenshot plugin](https://intercom.help/trmnl/en/articles/10302121-screenshot). Point this plugin to any website that looks decent in your TRMNL device's screen dimensions and TRMNL will periodically poll it to generate images. Common use cases for this plugin include publicly posted schedules, PDFs, school lunch menus, or even news sites. ## Third Party Plugin A third party plugin is the most flexible - and the most complex - custom plugin type. You provide a web application that connects to TRMNL via our simplified OAuth2 authentication flow, and in return get per-user settings management, and full control over generated markup. TRMNL servers will periodically poll your application to generate images for the device. While third party require the most effort, they have a couple unique benefits: - User's full name + email is shared with you (for building a newsletter, etc) - You can control how often you allow data to be refreshed, keeping your server and API costs under control, by simply throttling inbound markup requests Learn how to [build and submit a public plugin](https://docs.trmnl.com/go/plugin-marketplace/introduction). ## BYOS For complete control over your TRMNL device you can [Bring Your Own Server](https://docs.trmnl.com/go/diy/byos). This capability bypasses all plugin functionality provided above. --- # Device Static IPs Thanks to [this feature request](https://github.com/usetrmnl/trmnl-firmware/issues/281) we now offer static IPs for TRMNL devices. Note: your device must be running [FW 1.7.5](https://github.com/usetrmnl/trmnl-firmware/releases/tag/v1.7.5) or later. Benefits: - Battery savings - Predictable networking - Whitelisting for enterprise environments (also see [WPA2 Enterprise WiFi](https://help.trmnl.com/en/articles/13722338-wpa2-enterprise-wifi)) Follow these steps to provision a static IP address for your TRMNL. ## Step 1 - Open the Captive Portal On your OG TRMNL device, hold the button on the back for 7-9 seconds. After letting go it should enter WiFi pairing mode like so: If you have a TRMNL X, press down on the far left and right bottom corners for a few seconds. You'll be asked to hold down on the middle section to confirm a WiFi reset. Then you should see a similar screen as above. From your phone or computer, connect to the "TRMNL" WiFi network being broadcasted by your device. Then scroll down and tap Advanced. ## Step 2 - Advanced Settings On the advanced page, navigate to "Use Static IP (disable DHCP)" and check the box to enable it. You should see these new fields: ### Static IP Address Enter whichever IP you want to use, noting a few precautions. For example, first check your router's IP. Usually it is either 192.168.0.1 or 192.168.1.1. Different routers and ISP's can provide different IPs, and you need to know yours in order to provide a valid range. After confirming your router's local IP (for example 192.168.1.1) you can set a static IP. It's recommended to use something outside the default DHCP range (192.168.1.100-192.168.1.200), so use either 192.168.1.2-99 or 192.168.1.201-255 to avoid conflicts with DHCPS. ### Gateway In 99% of all situations, this should be your router's local IP (e.g. 192.168.1.1). But in some corporate networks or custom routers, you may want to make your traffic flow through a VPN or a subnet. Otherwise you can leave this blank. ### Subnet Mask In most cases it will be 255.255.255.0, but you may need something else for e.g. a fragmented subnet. ### DNS Server Use whatever DNS server you're comfortable with, can be local or public. If no value is provided TRMNL will default to Google's 8.8.8.8, a common option. --- # Setting up TRMNL on tricky Wi-Fi situations Most Wi-Fi networks are straightforward and work without a hitch. This article is not about those. This article is about more complicated Wi-Fi setups. We tested three devices in three different scenarios. If you've tried the steps in these guides and they haven't worked for you, please email us at or chat us so that we can investigate. The four non-standard scenarios are: 1. Regular WiFi that simply doesn't work with your device 2. WiFi with a captive portal (think hotel WiFi) 3. WPA/WPA-2 Enterprise WiFi, AKA "WiFi that requires a username and password" (see our [WPA2 Enterprise WiFi guide](https://help.trmnl.com/en/articles/13722338-wpa2-enterprise-wifi)) 4. Static IP pre-approval (see our [Static IP guide](https://help.trmnl.com/en/articles/13673576-device-static-ips)) If the guides in Scenarios #3-4 above do not apply to you, this article recommends an alternative solution: travel routers. **Note**: some offices frown on third-party routers in their networks. Please check with your local network administrator or IT person to ensure you're allowed to do this. ## My TRMNL won't connect and I have a regular (password-only, no captive portal) WiFi network Sometimes there's a corporate environment where, say, your cell phone connects just fine to the network, but the TRMNL doesn't. Maybe you get the "WiFi connected but can't connect to the API" message. In that case, we suspect there's some corporate network security that, for whatever reason, just doesn't play nicely with TRMNLs. We've had at least one report where a user bought this device, connected THAT device to the network, and then connected the TRMNL to it. For this scenario, the [Opal (GL-SFT1200) Wireless Travel Router](https://www.amazon.com/dp/B09N72FMH5?th=1) worked. It's $35 at the time of writing and we are not affiliated with them at all. ### Setting up the travel router Follow the travel router's instructions to set it up. These are, roughly, plug it in, connect to it's broadcast network (starts with GL), and connect to it with the default password `goodlife`. Then, visit `http://192.168.8.1/` (or whatever your router manual said). You might see a `NET::ERR_CERT_AUTHORITY_INVALID` error. This error is safe to ignore because the connection to your router is local. [Here is an article on the subject if you'd like to learn more](https://docs.gl-inet.com/router/en/4/faq/warning_from_your_browser_during_setup/). You will then have the router act as a repeater for the network you want to use. From this point forward, what you do depends on if you connect your travel router via Ethernet cable to the source network or not. We'll start with wireless, since that's more complicated. ### Wireless Connect to the travel router's network. It starts with GL. Then, click the "Connect" button in the "Repeater" block. Select your WiFi network from the list. Enter the password and connect. It might take a few minutes, but your travel router is now connected to the internet. To verify, ensure you're still connected to the travel router's network (starts with GL). Try to use the internet. We suggest . If the page loads, you're good to go. ### Wired If you plug an Ethernet cable into your travel router, it should just work. You should be able to point the TRMNL straight at the travel router's network (starts with GL) to get it up and running. You can now set your TRMNL up like normal by following the guide at ​. Be sure to use your travel router's network (starts with GL) as the SSID. ## My TRMNL won't connect and the WiFi network requires a username and password TRMNLs can't natively connect to WiFi networks that require usernames *and* passwords (AKA WPA/WPA-2 Enterprise WiFi), but there are travel routers that can connect to those networks on your behalf and broadcast a password-only network that your TRMNL can connect to. We've tested two routers that work for this scenario. Which one you'll need depends on if the WPA/WPA-2 Enterprise network is 2.4GHz or 5GHz. - If it's 2.4GHz, you can use the less-expensive [Shadow (GL-AR300M16-Ext) Mini Smart Router](https://www.amazon.com/dp/B07794JRC5) ($33 at time of writing) - If it's 5GHz, you need a router that supports 5GHz. We tested the [Slate Plus (GL-A1300 Gigabit) Travel Router](https://www.amazon.com/dp/B0B4ZSR2PX) ($70 at time of writing) ### Finding out if your network is 2.4GHz or 5GHz #### On MacOS Hold the option key and click the WiFi icon in the top right of your screen. Look for the line item called "Channel". #### On Windows 10 and 11 Open Settings > Network & Internet > Properties and select your network. The "Properties" info block will show you if it's a 2.4GHz or 5GHz network. ### Setting up the travel router Connect to the travel router's network. It starts with GL. The default password is `goodlife,` or whatever the manual says. Then, click the "Connect" button in the "Repeater" block. Select your WiFi network from the list. When clicking the network, you should be prompted to enter a username and password. Enter them. If you aren't prompted for a username and password here, reach out to us so we can troubleshoot. . It might take a few minutes, but your travel router is now connected to the internet. To verify, ensure you're still connected to the travel router's network (starts with GL). Try to use the internet. We suggest . If the page loads, you're good to go. You can now set your TRMNL up like normal by following the guide at ​. Be sure to use your travel router's network (starts with GL) as the SSID. ## My TRMNL won't connect and the WiFi has a captive portal A captive portal is a popup when that appears before you can use the network that requires you to accept some terms. You see them most often in hotels and coffee shops. TRMNL uses a captive portal to assist in setup. These handy travel routers can help you through those situations, too. Never be without your TRMNL again—even at hotels! We've tested two routers that work for this scenario. Which one you'll need depends on if the WPA/WPA-2 Enterprise network is 2.4GHz or 5GHz. - If it's 2.4GHz, you can use the less-expensive [Shadow (GL-AR300M16-Ext) Mini Smart Router](https://www.amazon.com/dp/B07794JRC5) ($33 at time of writing) - If it's 5GHz, you need a router that supports 5GHz. We tested the [Slate Plus (GL-A1300 Gigabit) Travel Router](https://www.amazon.com/dp/B0B4ZSR2PX) ($70 at time of writing) ## Finding out if your network is 2.4GHz or 5GHz #### On MacOS Hold the option key and click the WiFi icon in the top right of your screen. Look for the line item called "Channel". #### On Windows 10 and 11 Open Settings > Network & Internet > Properties and select your network. The "Properties" info block will show you if it's a 2.4GHz or 5GHz network. ### Setting up the travel router Connect to the travel router's network. It starts with GL. The default password is `goodlife`, or whatever the manual says. Then, click the "Connect" button in the "Repeater" block. Select your WiFi network from the list. Wait for the captive portal to appear. Sometimes leaving and rejoining the travel router's network will force the portal to appear. On Apple devices, visiting will also force the portal to appear. Captive portals look something like this: Do whatever the captive portal tells you to access the internet. Your travel router is now through the captive portal and should remain so until the router is powered off or until the guest network renews its DHCP leases. Note that captive portal owners can configure their networks so that these travel routers can't bypass them in this way. If that happens to you, please email us at or chat us so we can note that in this article. You can now set your TRMNL up like normal by following the guide at . Be sure to use your travel router's network (starts with GL) as the SSID. ## On 5GHz networks, travel routers, and TRMNL TRMNL devices can only connect to 2.4GHz networks. However, the 5GHz travel routers appear to be able to take a 5GHz signal and rebroadcast it as 2.4GHz. So, this setup should work even if you only have access to 5GHz networks. ## Other travel routers Your mileage may vary with travel routers not in this article, but surely others work as well. Here's what you should look for: - If you have a username password WiFi, the travel router will need to support Extensible Authentication Protocol (EAP) - If you have a captive portal, we recommend you Google {your_router_name} captive portal to find out if your router supports captive portal bypass. There's no captive portal protocol like there is for username and password, so ways to get around it can vary - Otherwise, any 2.4GHz travel router will work. If you do find others that work, please email us at so we can update this article --- # WPA2 Enterprise WiFi If your WiFi configuration requires a username and password to connect, we have a feature for you. After [enabling WiFi pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode), tap your network's name like you would on a computer. TRMNL will attempt to detect if the WPA2 protocol is being used, and if so, generate additional form fields below the network name where you can provide those credentials. --- # Connect your Xteink X4 to TRMNL The Xteink X4 is a palm-sized, lightweight e-reader with a magnetic back. Thanks to the community it may also be used as a passive, personal dashboard with hundreds of official and community plugins. ## Adding TRMNL to your X4 1. With a USB-C cable, plug your device into your computer. 2. Visit our [Flash Assistant](https://trmnl.com/flash). 3. Select "Xteink X4" as your device and the latest firmware. 4. Click the Connect button, select "USB JTAG/serial..." from the list, and select "Install" from the popup menu. 5. Wait for the device to install the firmware (this may take a minute or two). Avoid disconnecting your device or closing the tab until the operation is complete. You should see a confirmation message within the popup menu. 6. Restart your device by pressing and releasing the small Reset button near the bottom right, followed quickly by pressing and holding of the main power button for about 3 seconds. (*If your X4 is brand new, you may be able to simply click the power button after unplugging it in Step 5*.) ## Connecting to a server To see content on your X4, you can either create a self-hosted server ($0) or purchase a BYOD license (one-time fee) to use trmnl.com as your server. The BYOD license grants you lifetime access to hundreds of native and community plugins, 1;1 support with our team, Discord access, and more. ### Option A - Set up a self-hosted server You are welcome to [Bring Your Own Server](https://docs.trmnl.com/go/diy/byos) (BYOS), no license required, for any TRMNL compatible device. If you decide on [Terminus](https://github.com/usetrmnl/byos_hanami), refer to our [Terminus setup guide](https://help.trmnl.com/en/articles/12263392-connect-your-device-to-terminus-byos). Even more live Terminus demonstrations are available on [our YouTube channel](https://www.youtube.com/@usetrmnl). ### Option B - Get a BYOD license Buy it here: ​ After purchasing your license: 1. [Claim your Device](https://trmnl.com/claim-a-device) by entering your TRMNL order number for the BYOD license, this will give you the device's *Friendly ID*. 2. If you don't have an account yet, follow the instructions to sign up now. If you already own an account, hover over your existing device in the top-right of trmnl.com and click "[Add new device](https://trmnl.com/devices/new)". 3. Then, hover over your BYOD device and click on the [gear](https://trmnl.com/devices/current/developer/edit) icon. You are now in the device settings. 4. Here, set your Device Model to the X4: 5. Now, click **Developer Perks**, then enter your MAC address for your device and **save**. Learn how to [find your MAC address](https://help.trmnl.com/en/articles/10614205-finding-your-trmnl-mac-address). 6. Finally, power your physical device off and on by holding the power button for a few seconds, and you'll be good to go! ## Removing TRMNL from your X4 Returning to stock firmware or CrossPoint Reader is easy. Head back to [our flash page](https://trmnl.com/flash) and follow the steps to replace TRMNL with another library. We're working to support dual boot between e-reader mode and TRMNL dashboard mode. We're also exploring a "manual refresh" option which will improve your battery life by only fetching new TRMNL screens when you click the power (wakeup) button. ## FAQ *If I decide I don't like my BYOD license and prefer to self-host, can I get a refund?* Yes, we offer a 2 week full return for BYOD licenses. *Can I transfer my BYOD license to another X4 or another device entirely?* Yes, your BYOD license can be pointed to any hardware at any time. Just visit your device settings and update the model + MAC address. *Is any coding required to use TRMNL with the X4?* Nope, but it is encouraged! Learn how to [build your own plugins](https://help.trmnl.com/en/articles/9510536-private-plugins). *How long does the battery last?* Because of a suboptimal PCB design, current tests indicate that an X4 with TRMNL running full-time lasts about 2 days between charges. We're working closely with the community to figure out how we can improve the efficiency of X4's sleep mode. *Can I run TRMNL and CrossPoint Reader at the same time?* We're working on it! TRMNL's team is in touch with the CrossPoint Reader lead maintainers and increasingly involved in the Xteink Discord community. Stay tuned. --- # Special Functions TRMNL devices have a few interactive elements, but they're not obvious. Get started by selecting your device below. **## TRMNL OG (7.5" screen)** The back of your OG TRMNL device has an on/off switch, USB-C input, and multi functional "boot" button. The boot button does 3 things by default: 1. Skip to the next screen in your playlist (click) 2. Reset WiFi credentials (hold for 6-7 seconds) 3. Soft reset your device (hold for 16-18 seconds) But you can program a 4th function, which we call Special Functions. # Determining your special function action If your device is running Firmware 1.6.1 or newer, special functions are enacted by a "medium press" button hold of 1-2s, then release. If your device is running Firmware released before August 2025 (1.4.X, 1.5.X), special functions are enacted by a double-click on the boot button. **## TRMNL X (10.3" screen)** Your model X has a touch bar that supports several different gestures. Over time this collection will grow, but as of February 2026 they are as follows: - Tap => skips to the next screen in your Playlist - Hold (for ~1s) => enables or disables OTG (ability to power other devices) - Swipe left => goes back a screen, reading from device memory - Swipe right => goes forward a screen, reads from device memory (requires a pervious "Left" swipe, otherwise reloads current screen) You can also [enter pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode) with the following gesture: [https://www.youtube.com/embed/aXiA_liuCvI?rel=0](https://www.youtube.com/embed/aXiA_liuCvI?rel=0) If videos aren't your thing: 1. Hold both ends until screen blinks 2. Tap the middle to cancel, or hold the middle to enter pairing mode # Setting a Special Function Visit your [Device settings page](https://trmnl.com/devices/current/edit) and scroll down to the Special Function section. There are several options, which we'll explain below. As of February 2026, Special Functions are not yet available on the X model device. They will be soon however, and you'll be able to set a unique gesture per function versus restricted to 1. # Identify Triggers a simple identification screen to help you determine which device is which. This is helpful for customers with multiple devices who may not give them descriptive names, or who have their devices in the same room with a similar case color. # Sleep Puts your device to sleep for 8 hours. In the future you may be able to customize this sleep period length. # Add WiFi Instructs your device's firmware to activate the "TRMNL" WiFi portal so that you can connect to a new network, without resetting your existing credentials. This works even on captive-portal / WPA-Enterprise networks. [See details here](https://intercom.help/trmnl/en/articles/11663377-setting-up-a-trmnl-on-tricky-wi-fi-situations). # Restart Playlist Begins showing content from your Playlist at the top/first position. Think of this as an enhancement to the single-click "skip" feature, as it skips multiple screens all at once. # Rewind Goes back to the previously viewed screen. Helpful if you noticed something interesting just as your TRMNL device began showing new content. # Send to me Sends yourself an email with the latest (current) device screen content. Helpful if you want to save/bookmark something for later but don't want to search for it in your Playlist history. # Guest Mode Switches your screen to any item of your choice. Helpful if strangers are nearby and you'd rather show the weather (for example) than a personal calendar. This interface will appear once you set the Special Function dropdown to Guest Mode. Set an item from your playlist as well as a duration to ensure sensitive information does not re-appear until guests are gone. --- # Google Calendar Connecting your Google Calendar to TRMNL takes just a few seconds and is done securely via our adherence to [Google's API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy). ## **Step 1 - Visit the Google Calendar Plugin** Inside TRMNL, navigate to Plugins > Google Calendar. Click "Connect with Google Calendar" on the right side of the screen. ## **Step 2 - Connect Google Calendar** After being redirected to Google, select an email account and approve access to TRMNL. Note that you must click "Select all" on the right side to manually approve each required permission. If you receive an error like "Something went wrong" after clicking Continue at the bottom (), you likely need to grant more permissions at your Google account level. Ask your administrator if the email is associated to G Suite, otherwise try the connection flow again from a phone or a different browser if using a personal Gmail account. ## **Step 3 - Configure Plugin** Back inside TRMNL, select which Google Calendars you want to display on your device and optionally set a few preferences. **Select Calendars** On a Mac, hold "cmd" and click multiple calendars. On a PC, hold "ctrl" and click multiple calendars. **Time Format** 24 hours vs 12 hour (AM/PM) style **Include Description** If "Yes," a truncated preview of the event's description field will be shown beneath the event title. Note that on the "default" Layout (explained below), setting this option to "No" will ~double the number of events shown thanks to saved space. **First Day of Week** Applies only to the "month" view. Similar to native Google Calendar (month view), this allows you to set which day of the week is represented by the first column, furthest to the left. Defaults to Sunday. **Ignored Phrases** Supports a list of words that will be used to ignore events based on title or description content. **Layout** - Default (upcoming events for today, tomorrow, next day) - Week (5-7 days upcoming) - Month (this calendar month) Note that only "Week" and "Month" layouts support multi-day events, but every layout supports all-day events. **Example** - Week layout with multi-day event ("Michael OOO") **Example** - Default layout with all-day event ("Michael OOO") **Example** - Month layout with multi-day event: To see even more layouts and size options, visit [this page](https://trmnl.com/plugins/demo) while logged into TRMNL and click to open each of the `google_calendar` variations. ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Google Calendar events very soon. **Note**: while we fetch data according to your refresh interval, TRMNL only generates new screen renders if the data fetched contains changes. Thus setting your layout to "Default" (which shows 2-3 days) but updating an event 4+ days in the future will not trigger a screen refresh. Learn more about refresh rates [here](https://help.trmnl.com/en/articles/10113695-how-refresh-rates-work). Stay focused. --- ***Unhappy with this article?*** *Let us know what you were searching for and why this article missed the mark for you. Email [support@trmnl.com](mailto:support@trmnl.com?subject=Google%20Calendar%20Article%20Feedback) and let us know (use the link to automatically reference this article)!* --- # Advanced Liquid This guide's purpose is to serve as a growing list of advanced Liquid topics. If you're not familiar with Liquid, we recommend starting with our [Liquid 101](https://help.trmnl.com/en/articles/10671186-liquid-101) guide. # Working with Time Zones When developing [Recipes](https://help.trmnl.com/en/articles/10122094-plugin-recipes) that can be installed and used by a larger TRMNL audience, it's important to take the user's timezone into consideration, especially if your recipe is going to display date or time based information. For this task we have a few tips on timezone conversion. ## Dates & Times Let's say a recipe uses an API that responds with ISO8601 dates like this: `2025-02-28T13:35:00Z`. If you simply pipe this into Liquid's `date` filter, the user will see the date/time in its original UTC timezone. To convert this date/time into the user's local timezone, we can leverage the variable `trmnl.user.utc_offset` that TRMNL injects into every plugin. This variable holds the user's timezone offset in seconds, so we can use it like this: #{{ "2025-02-28T13:35:00Z" | date: "%s" | plus: trmnl.user.utc_offset | date: "%H:%M" }} The first date filter with `%s` converts the date/time into a unix epoch. We then add the user's timezone offset to that, and pipe it again to the date filter to format as we want. Suppose the user's offset is -3 hours (-10800 seconds), this would display the time as `10:35`. ## Dates and Times in JavaScript Inside your script tags, use the IANA time zone (`#{{ trmnl.user.time_zone_iana }}`) and locale (`#{{ trmnl.user.locale }}`) provided in the TRMNL variables. These values allow you to format times in the user's proper local context, adjusting not only the time zone offset but also formatting rules like 12h vs 24h clocks and regional date styles. In JavaScript you can use the `Intl.DateTimeFormat` or `.toLocaleString()` methods with both the `locale` and `timeZone` options. Here's a simple example that formats a UTC timestamp as a localized, correctly offset time with a timezone abbreviation — this example will output the correct time to the console. You should use `date.toLocaleString()` anywhere that you need to render text that is appropriately formatted and normalized to the time zone of the end user: ​ const date = new Date("2025-06-09T17:00:00Z"); // always in UTC const timeZone = "#{{ trmnl.user.time_zone_iana }}"; // e.g. "America/New_York" const locale = "#{{ trmnl.user.locale }}"; // e.g. "en-US" const options = { hour: 'numeric', minute: '2-digit', hour12: true, timeZone, timeZoneName: 'short' }; console.log(date.toLocaleString(locale, options)); // → "1:00 PM EDT" (if user's time zone is Eastern Daylight Time) This technique ensures that your TRMNL plugin produces consistent, user-friendly time output, regardless of where the server is located or when the page is rendered. If you are rendering a full range of time values (e.g., a time range like “3:00 PM – 4:30 PM”), apply the same formatting approach to both the start and end times. You can even include the full date and time zone label using similar options, or separate formatting for date and time if you need finer control. If displaying the time zone abbreviation is not important, you can omit the `timeZoneName` option for a more concise format. ## Dates Only Be mindful when working with date-only cases like `2025-02-28`. When you pipe this into Liquid's date filter, it will assume the time is `00:00:00`. If you apply the previous concept to this case, a user who is behind UTC is going to see a date the day before. This is because `2025-02-28T00:00:00` in UTC will convert to `2025-02-27T21:00:00` in UTC-3 (for example). You'll need to analyze this on a case by case basis to decide if the conversion is relevant or if it's going to confuse the end-user who installs your plugin. ## Last updated/refreshed at For some recipes it may be interesting to display the time at which the last update/refresh occurred. Liquid's date filter supports the special keyword `now` to represent the current date/time. We can apply what we learned before to also show this date/time in the user's timezone:

Updated at #{{ "now" | date: "%s" | plus: trmnl.user.utc_offset | date: "%H:%M" }}

If you want to go a step further, you could use localization so the word "Updated" is rendered in the user's language:

#{{ 'updated' | l_word: trmnl.user.locale }} #{{ "now" | date: "%s" | plus: trmnl.user.utc_offset | date: "%H:%M" }}

This uses our [custom plugin filters](https://help.trmnl.com/en/articles/10347358-custom-plugin-filters) and our [i18n library](https://github.com/usetrmnl/trmnl-i18n) to do the localization based on the user's locale (set in their account). # Random Numbers Liquid does not have native filters to generate random numbers, but fortunately we can adapt the `date` and `modulo` filters for this purpose! ## Simple random number To generate a simple random number between 0 and 9: {% assign random_number = "now" | date: "%N" | modulo: 10 %} This is useful when you want to select a random element from a collection, as you could do: {%liquid assign rand_index = "now" | date: "%N" | modulo: my_collection.size assign rand_item = my_collection[rand_index] %} ## Random between an interval Here's a more complex example that generates a random number between two other numbers, with both of them included. {%liquid # random number between 5 and 10 assign min_range = 5 assign max_range = 10 assign range = max_range | minus: min_range | plus: 1 assign random_number = "now" | date: "%N" | modulo: range | plus: min_range %} # Type Conversion Liquid is capable of doing some magic type conversion between numbers and strings behind the scenes. Let's say you have the number `1.5` but you want to display it using a comma as the separator. One would think that the `replace` filter only works on strings, but it actually works on numbers too: #{{ 1.5 | replace: ".", "," }} ## Decimal Numbers If you try the code below you'll be surprised to see the number `0` displayed as a result: #{{ 1 | divided_by: 2 }} This is because both numbers are integers, so Liquid also outputs the result as an integer. When you need the result to be a decimal number, simply "force" the divisor or dividend to also be a decimal number. For example: #{{ 1.0 | divided_by: unknown_number }} This way, if `unknown_number` was actually the integer `2`, the result would be correctly displayed as `0.5`. # Left Padding Strings Sometimes you may need to left-pad strings with other characters, such as zeroes or spaces. For example, let's say you're converting a number of seconds into hours and minutes to display as as `hh:mm`. You do all the math and end up with something like this: `1:3`. Wouldn't it make more sense to display it as `01:03`? Using the `prepend` and `slice` filters we can achieve this result: ​ {%liquid # original values (suppose these were calculated) assign hours = 1 assign minutes = 3 # left-padding with zero assign hours = hours | prepend: "00" | slice: -2, 2 assign minutes = minutes | prepend: "00" | slice: -2, 2 %} You can modify this logic by changing the padding characters in the `prepend` filter and adjusting the lengths accordingly in the `slice` filter. # Mixing Liquid and JavaScript One commonly overlooked concept when developing TRMNL plugins is that you can actually mix Liquid and JavaScript together. There are many use cases for this, but a very common one is building [Charts](https://trmnl.com/framework/chart) using data returned from an API. Mixing both worlds is just a matter of using Liquid syntax inside JS code: When mixing Liquid and JS, we need to pay attention to the data types of the Liquid variables, as not all of them translate directly to cognate JS data types. ## Rendering Due to our rendering logic, you'll have more success with client side mutations by wrapping your JS like this: ​ document.addEventListener("DOMContentLoaded", function(e) { // code goes here }); This strategy is more reliable than `window.onload = function () {}` or other approaches. ## Strings When you have a Liquid string that you want to use as a JS string, don't forget to enclose it in quotes: ## Numbers and Booleans Numbers and Booleans are the only Liquid data types that convert naturally to JS: ## Lists/Arrays Lists are the most controversial case as you cannot directly assign a Liquid list to a JS array like the previous examples. You need to iterate through the Liquid list values and push each one of them to the JS array: You'll come across this pattern when creating arrays to be used for a chart's series data. ## Objects Liquid objects also do not translate directly to JS objects. Fortunately, TRMNL has implemented a custom `json` filter that does exactly this: Be sure to check out the resources below for more TRMNL custom filters. # More Resources - [TRMNL Custom Liquid Filters](https://help.trmnl.com/en/articles/10347358-custom-plugin-filters): a growing list of custom Liquid filters developed by and available only on TRMNL. --- # Custom plugins with Apple Shortcuts Behind the scenes at TRMNL it takes a little bit of magic to turn useful information into a beautiful graphic on your device. But TRMNL users don't need to worry about any of that. You can build custom plugins with just a little bit of HTML and an HTTP request. Since HTML and HTTP basically run the entire internet, this makes Apple Shortcuts a viable and free candidate for creating todo lists, weather tickers, and more, directly from an Apple Device. Without setting up a web server you can generate new plugins with dynamic data, automatically from your iPhone, iPad, or Mac computer. ## Step 1 - Create a Private Plugin Inside TRMNL, navigate to Plugins and search for Private Plugin. This requires our Developer edition upgrade, which is just a 1 time fee of $20 from [trmnl.com/upgrade](https://trmnl.com/upgrade). Provide a name for your plugin, then select the Webhook strategy, then hit Save. ## Step 2 - Get the Template A TRMNL community member created this public template to help you get started: ​ If you click the link above from an iOS device, you'll see a setup screen like this: Click "Set Up Shortcut," then you'll land on a screen like this: In this blank text box, copy / paste your Private Plugin's UUID from inside your TRMNL account. This will not appear until you click Save (the end of Step 1 above). (**Tip**: if you're using an Apple computer to create the plugin, and your iPhone or iPad to create the Shortcut, then you can likely "copy" on your computer, then "paste" on your device, to avoid manually inputting this long string of characters) Click "Add Shortcut" to finish adding the Shortcut template to your Apple device. You may now view it from your Shortcuts dashboard and begin building custom logic. ## Step 3 - Add functionality Open your "TRMNL Starter Template" Shortcut on your Apple device by clicking the 3 dots in the top right corner of the widget. Optionally rename it by clicking the down arrow along the top of your screen. In this tutorial we'll build a plugin that showcases random facts about dogs. The final result will look something like this: In the bottom search bar, type "Get contents of" and select the "Get contents of URL" option. This Shortcut step will be added to the bottom of your template. Hold it down with your finger or mouse and drag it up above the empty Dictionary step. **Update, May 2025** The Dog Facts API we used to create this guide has been deprecated. Some of the JSON traversal syntax has changed from what you see below in screenshots and HTML examples. Click the placeholder "Url" icon, then "Clear variable," then input the following: `https://dogapi.dog/api/v2/facts` Your Shortcut should now look like this (**again, ignore the different API URL**): If you click the play button on the bottom of your screen, Apple may ask you to approve access to open Chrome or another web browser. Select Yes. You might see a new area populate with data, which in our case will be a random dog fact. In our example above, Apple Shortcuts recommends a "next action" called Get Dictionary Value. Select this, or search for it in the bottom search bar if it does not automatically appear. After adding a "Get Dictionary Value" step and dragging it below your "Get contents of" step added previously, change the step description to "Get Value for facts in Contents of URL." Click the play icon again, and your screen should look like this: Here we simply eliminated all the extra API data from the free Dog Facts tool, making a simple string of text that TRMNL can merge into a plugin design. As a final step, input "dog_fact" in the 'key' field of our previously empty Dictionary step. Then in the 'value' field on the right, select Dictionary Value from the list of available variables. And that's it! Clicking the play button should now: 1. fetch a random dog fact 2. assign it to a variable called `dog_fact` 3. nest the `dog_fact` value inside `merge_variables` (generated by the starter template) 4. send the `merge_variables` content to your TRMNL private plugin Here are the results of clicking the play button after following all the steps above: If your Shortcut isn't working as expected, here is another Shortcut template with all of the steps above completed. You will just need to provide your Plugin UUID to connect it to your account, which is explained above in Step 2. ## Step 4 - Design your plugin Back inside your TRMNL account, click the "Edit Markup" button at the top of your private plugin's settings screen. Copy/paste the content below into the "Full" tab code editor, which should be pre-selected:
Did you know?
{% if dog_fact.size < 300 %}

#{{dog_fact}}

{% else %}

#{{dog_fact}}

{% endif %}
Dog Facts
Notice the use of `#{{ dog_fact }}`, this is how we merge in our data from the Dog Facts API. Next, return to the Shortcut you created on your Apple device. Click the play button. A dog fact will be fetched + sent to TRMNL. Finally, inside TRMNL go back to your plugin settings screen from the link near the top: Click Force Refresh, then wait a few seconds. Refresh the web page in your browser and you should see a random dog fact: ## Step 5 - Automate new plugin content Once you're happy with your Shortcut, head over to the Automation tab on your Shortcuts app. ​ Select "New Automation", then choose some logic for how often the automation should be executed. For example, you could choose "Time of Day" and select "Daily", "10a." Select your "Dog Facts" Shortcut from the My Shortcuts section: And you're done! A new dog fact will be generated on your TRMNL device every day at 10a. If you want more dog facts, just duplicate the automation at different times throughout the day. ## Step 5 - Build more plugins Phew! That was quite the process. Apple Shortcuts can be tricky at first, but once you get the hang of it, ~anything is possible. For example, here is a custom Todo List plugin created by another TRMNL user: How it works: 1. open plugin from the iPhone's home page 2. input a todo item 3. send it to TRMNL 4. display on device Pretty cool, huh? We can't wait to see what you build. Stay focused. --- # Account Deletion If for whatever reason you want to wipe your footprint from TRMNL, there are a few places that need to be updated: 1. Our storefront (), powered by Shopify, has a Customer record. This must be removed manually by our team, however a child Order record will remain for accounting purposes. 2. Our email marketing tool (). Again, our team must manually delete this profile. It is possible that your name or email will still appear in logs of previously sent messages. 3. Our web application (). Here you can remove 99% of your footprint self-service. Start by deleting your plugin connections, then any remaining Playlist items, followed by unpairing your device from the device settings portal. Send our team a live chat to delete the user record itself. Alternatively you can adjust the account's name and email to fake values, click save, and you're done. ## Benefits of un-pairing devices By unpairing your device (versus deleting it), another person may set it up should you choose to sell or give it away. If you prefer to have it permanently deleted however, our team will take care of this on your behalf. ## Other data storage providers The web application has a read replica database for emergency backup purposes, but this is routinely overridden. The web application also stores image assets (plugin screens) in Amazon's S3 services, but these too are purged whenever you destroy a plugin connection. ## Special removals If you are a Plugin Author wishing to delete your account, TRMNL may first need to migrate the work to an active account to prevent disruption for connected users per our [Plugin License](https://trmnl.com/plugin-license). If published work depends on your own resources (servers, database, etc), naturally you are welcome to remove this without any approval by TRMNL. --- # TRMNL for iPad On your iPad, head to [trmnl.com/mirror](https://trmnl.com/mirror) and provide [your device's API Key](https://trmnl.com/devices/current/developer/edit) from the *Developer Perks* section. *This feature requires your device to have a developer license (included with the Clarity Kit or purchased separately). You can upgrade at any time in your [device settings](https://trmnl.com/devices/current/edit).* iOS 10 and later is supported. Please see [this compatibility table](https://iosref.com/ipados) to check if your device can be upgraded to iOS 10 or later. This will mirror whatever is currently displaying on your native or BYOD TRMNL (*not a Virtual Device*). It will also wake refresh to the next screen based on your Playlist preferences. **Pro tips** 1. use Safari (iOS) to set up, then tap Share > Add to Home Screen to make it accessible as an app icon. Not only is this easier to access, you will also benefit from a full bleed image instead of a web browser bar 2. disable your iPad's default "sleep" settings so the utility can switch screens without interaction 3. to switch devices, just tap the image to return to the Device API Key input form 4. this may be available with some BYOS clients, just input your Custom Server URL to find out ;) To provide feedback on the Mirror utility, start a live chat or email . --- # Skipping Screens within Plugin Markup Sometimes a plugin isn't relevant, such as in the off-season for your favorite sports team, or because it's not the right day of the week. Other times, you may want to keep content stale on purpose. If this is strictly a day of the week or hour of the day schedule, then you could use our [playlist scheduler](https://help.trmnl.com/en/articles/11663305-playlist-scheduler) to set things up. But if you want to control it from the plugin level, there are two special variables available in markup. ## `TRMNL_SKIP_SCREEN_GENERATION` **Use Case:** When no news is good news. {% if condition %} {% endif %} *Screen generation* is based on your plugin's set refresh rate. When it is time to refresh your plugin's content, our worker servers will look/refresh your data source and then look at your markup. If the `TRMNL_SKIP_SCREEN_GENERATION` variable is set to `true`, the worker will skip generating a new screen. This setting does not affect whether a plugin is shown on a playlist, unless you enable the [playlist](https://trmnl.com/playlist) feature "Smart Playlist": ## `TRMNL_SKIP_DISPLAY` {% if condition %} {% endif %} If set to `true`, `TRMNL_SKIP_DISPLAY` will cause the plugin to be skipped on your playlist when it's requested by your device. 💡Tip: You can also return `TRMNL_SKIP_DISPLAY = true` as a top level property of your polling payload response, and it will have the same effect as setting `window.TRMNL_SKIP_DISPLAY = true` in markup. { "TRMNL_SKIP_DISPLAY": true, "other_data": "..." } `TRMNL_SKIP_DISPLAY` should **not** be used in published recipes, as user's have no indication that the plugin's markup caused it to be skipped in their playlist and can lead to confusion. Use in private plugins is the most appropriate. --- # TRMNL Mounting Options ## TRMNL (OG) While the TRMNL (OG) has a built-in triangle cutout for easy wall mounting with a nail or a [3M Sawtooth Picture Hanger Strip](https://www.command.com/3M/en_US/p/d/b10014175/), the power of TRMNL is the ability to go anywhere a WiFi signal will reach and provide months of uninterrupted value. To assist with that, the community has designed a [variety of mounts](https://github.com/usetrmnl/mounts). If you do not have access to a 3d printer, check your local library or maker space for a free or low-cost option! Of course, you could also do more direct modifications, such as gluing magnets onto the back of the device to attach the device to a refrigerator. The TRMNL (OG) case is a soft-touch ABS plastic, so a **CA (Cyanoacrylate) Glue**, more commonly known as "Super Glue", is a great option for attaching magnets permanently. ## TRMNL X The TRMNL X introduces even more flexibility with mounting solutions designed to fit any space. For those seeking a minimalist, low-profile setup, the device can be paired with an adhesive magnetic or metal sticker. This allows the X to snap directly onto the surface for an almost completely flush finish. Alternatively, for a more permanent installation, a dedicated U-mount is available that can be screwed directly to the wall. The X Dock snaps securely into this mount, providing a sturdy 20mm standoff. --- # Demo Data for Publishing Plugins There are a few different strategies to making sure that your private plugin is ready for publishing, in terms of protecting personal information. While **no form field data** is visible to anyone installing a published recipe, the number one risk is with the screen generated to show what the plugin does. **Best Practice**: When publishing a plugin, it will have a new badge, "Recipe Master". This should stay in a "demo" state, and you should **Install** the recipe again in your account, just as other users would do, for personal use. # Webhook Strategy Recipe This is the easiest option, since the data is pushed to our servers via a [webhook](https://docs.trmnl.com/go/private-plugins/webhooks). Using cURL, a tool like Postman or Insomnia, or other method, you can manually push demo/fake data to your plugin recipe, then **force refresh** your plugin to render the screen using the data you pushed. # Polling Strategy Recipe There are a few options when pulling data from a 3rd party source, in various levels of difficulty. **Tip**: You can *freeze* your plugin's screen by removing it from any playlists. A plugin (in full view) **not** in a playlist will not have its (full view) screen refreshed, which is the view used for the install page of a recipe. ## Creating a Demo Account If the service offers a free account option, create an account and populate it with demo data. If that data would automatically deteriorate due to it being time based (*e.g.*, last 90 days), then use the *freeze* tip above. ## Different URL Some plugins are pointed to a self-hosted service or custom URL, which is data collected in the [form fields](https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder). Create a sample response, for instance JSON, with fake data, then host that file on a personal server or as part of a GitHub project, providing a reliable source to feed the demo data. ## API Key or Other Unique Form Field Value If a demo account isn't an option, and the plugin uses a form field for an API Key, you can use a trick in the markup to provide fake data when the API key matches a specific value. For instance, imagine you have a form field with the *keyname* of `apikey`. You set the value to `0` in your plugin's settings. Then, edit the markup. Assuming that Your Variables returned a variable `data`, representing the response from the polling URL, you could add code at the top of your **Shared** view like: {% if trmnl.plugin_settings.custom_fields_values.apikey == "0" %} {% assign data = '[{"id":"01ABC123XYZ456"},{"id": "01FIELD123"},{"id": "01ROLE123"}]' | parse_json %} {% endif %} In this example, we're providing replacement JSON data for the `data` variable, before the rest of your script would process that variable, but only if the API Key is "0". Using this trick, you can set your *recipe master*'s API Key to 0 and always be displaying this fake data to showcase your plugin. # Featured Image If you want to control which screen appears when people are viewing your recipe, edit your recipe settings, and there will be a button to set the preview image as the current screens image. --- # How to Enable 2-bit on TRMNL (OG) The ability to choose 1 or 2 bit rendering on a per-plugin basis is not yet a released feature. It will be released in late 2025. # Seeing 2-Bit Images with a Demo When 2-bit was released with firmware version 1.6.x or higher, we enabled it automatically with the [Redirect](https://help.trmnl.com/en/articles/11035846-redirect-plugin) and [Alias](https://help.trmnl.com/en/articles/10701448-alias-plugin) plugins, since they were primarily used for images, it was a great way to show-off the functionality if you properly formatted the images you were targeting. Thanks to a [community contribution](https://github.com/rmitchellscott/trmnl-2bit-demo), a few easy steps lets anyone see the glory of the 2-bit upgrade for images. - Add the [Redirect Plugin](https://trmnl.com/plugin_settings/new?keyname=redirect) - Set the **Web Address** to: `https://2bitdemo.scottlabs.io` - Click **Save** That's it! Now a random 2bit image will be displayed on the frequency you set in your [playlist](https://trmnl.com/playlists). # Enable 2-bit for your Device *This feature is available to any account with firmware version 1.6.x or higher (1.5.12 is 1-bit only).* You can now [set the palette](https://help.trmnl.com/en/articles/12985974-understanding-color-palettes) at the device and playlist item levels. --- # Mashups TRMNL offers several layout combinations for your plugins. The default layout is full screen, illustrated by the top left icon in the graphic below. # Create a Mashup When you connect a new plugin, or create a new plugin "instance" (for an existing plugin), **TRMNL adds a full screen layout to your Playlist automatically.** **TIP:** You don't need to have the full version in your playlist for mashups to work for most* plugins, they are refreshed even if the full plugin is removed from your playlist (you'll see a message on the plugin's page saying "refreshing mashups" in that scenario). * If you used the `install` option on a public recipe that, when clicked, brought you directly to your playlist and NOT the plugin editing page, that plugin has no customization (form fields), so if removed from the playlist, it will be removed overall. In this scenario, used the *hide* option to ignore the full version. To instead leverage a Mashups layout, visit your [Playlists tab](https://trmnl.com/playlists) and hover your mouse over the "Add a Plugin" dropdown. Here you can select a layout, then begin building it out with a few clicks. In the example above we chose the 50/50 split screen layout, which is the top-right icon in the graphic at the top of this guide. Next, we can choose **any*** of our connected plugins from the "Section A" and "Section B" dropdowns. As you can see, whatever plugin we provide in the Section A plugin will be mapped to the left pane layout, denoted by the 'A' inside the template. Once you've chosen a plugin for each section, your Playlist will refresh. # See your Mashup After creating a mashup, it should then take just a few seconds for the Mashup content to be generated. So instead of waiting for your device to fetch this particular content, you can actually refresh your Playlists tab to see the preview image. If you'd like to inspect your Mashup more closely before it appears on your device, simply click on the thumbnail. Once it is updated, clicking on the mashup's image will "zoom" into it. # Edit a Mashup To update the contents of an individual item / section within a Mashup, click the tripe dots and choose Settings: # Troubleshooting Mashups If you've created a Mashup but still see orange boxes, try waiting a few more seconds for our queue to pick up your inner contents and generate a graphic. If your Mashup contains Private Plugin or Recipes, and the content is duplicated between unique instances, check that the markup (HTML, CSS, JS) is containerized to have **[uniqueness](https://help.trmnl.com/en/articles/10347358-custom-plugin-filters)** across instances. For example, if you have a chart that binds to "#my-chart," then one of your Mashup sections will work, and the other will not. If your Mashup has some working sections, and some blank sections, consider it's possible our system "timed out" while loading all the content and taking a snapshot for your device. If you have access to the markup and logic of the blank section, try making it smaller and faster. If the blank section is a native plugin, let us know. --- # Create a Custom Sleep Screen If you don't want to use the TRMNL **Sleep Screen**, you can use your playlist, a preferred plugin/mashup, and a little configuration to make your own *sleep screen*! # High-Level Overview - Pick a sleep time and enable sleep mode - Schedule a plugin to show at least **X** minutes before sleep mode is enabled, where **X** is the refresh time of your device. - Optionally, use the `Important` toggle for easy *sleep screen* setup. # Set up Your Device's Sleep Schedule In your [device settings](https://trmnl.com/devices/current/edit), *Battery & Sleep* section, select your start and end times for your device to sleep and save energy, and enable **Sleep Mode**. ***Note:*** *When you toggle **Sleep Mode** on, it automatically toggles **Sleep Screen**. You want to disable **Sleep Screen**.* For this tutorial, we are going to use the example sleep time of 23:00 (11 pm) going forward, but you are welcome to apply any time that is right for you. Take note of the time you set, because everything will be adjusted based on that schedule. # Configure Your Playlist ## Adding a New Plugin Instance If the plugin you wish to use as your *sleep screen* is not yet in your [playlist](https://trmnl.com/playlists), use the **Add a Plugin** dropdown in the top-right and select the plugin or mashup you want to use, selecting the plugin(s) to be displayed. ## Schedule Your Sleep Screen Once the plugin/mashup screen is in your [playlist](https://trmnl.com/playlists), there are two approaches, one using the **Important** toggle, and the other without it. *In either case, our goal is to make sure a specific screen is always visible before the sleep timer is triggered.* You'll be using the "Adjust schedule" function by clicking the triple dots: ### Using the Important Toggle Look at the **Duration** dropdown and check the *Device default* amount. You want to subtract *at least* that amount from your sleep start time. Because the screen naturally turns off between screen changes and *requests* the next screen from our servers, we can't set a specific time for it to act; we can only make sure that when it *does* act, we have the correct screen ready. In this example: Sleep Time = 11:00 PM Device Default Duration = 30m 11:00 - :30 = 10:30 pm You should also set the end time to the same duration ***after*** the sleep time. If you have other plugins scheduled all day or during this time, AND they have a *longer duration* than the default, you will want to adjust the *start time* based on this longer duration value. Select all the days (dark orange is selected) you want it to be active, then finally toggle the **Important** toggle on, making sure this plugin takes priority over any other non-important screens during this time. ### Not Using Important (or Multiple Important Screens) While the procedure is the same, there are a few other considerations. You need to make sure that all *other* screens on your playlist are ending *before* the start time you set for your *sleep screen*, to ensure there is no overlap. Of course, if you want overlap, you can do that, but know that **whatever is on the screen when your sleep time starts, that is the plugin that will be displayed for the duration of the sleep period**. # Verifying Your Setup At the top of your playlist, we're going to use the **Time Travel** toggle to make sure everything is set up correctly, picking a time between your *sleep start time* and the *start time* you selected for your *sleep screen*. If everything is correct, the only plugin that does not show the *disabled* cross-hatching background should be the *sleep screen* you configured. If not, then double-check the instructions in the previous sub-sections. Congratulations, you just created a custom *sleep screen*! --- # Image Display For a more dialed-in experience than the [Screenshot](https://help.trmnl.com/en/articles/10302121-screenshot) plugin, our Image Display plugin lets you generate custom graphics that TRMNLs convert to e-ink friendly screens. # Use cases - Family photo album at `mysite.com/hidden/family.jpg?auth=qwerty` - Home Assistant dashboard - Deer cam CCTV To get started with Image Display, navigate to Plugins > Image Display and input a URL to your image, which can be in any format or size. # Where to host images We've heard mixed reports of sites like imgur.com being unreliable. Potentially they use anti-scraping tech that treats TRMNL like a robot. We don't deserve that. So you're welcome to point to Google Drive instead. Simply upload an image, share it to "Anyone with the link," then copy + paste the link like so: Here's the above example if you'd like to try it:​ https://drive.google.com/file/d/164qYbGtnzQq_PDLE-eB6cu4a6mIT8DzN/view?usp=sharing You can also use Dropbox to host your images. When entering a Dropbox link, you'll need to change `dl=1` or `dl=0` to `raw=1`. Otherwise, the link points to a preview webpage rather than the actual image file, which won't work with the plugin. For example: https://www.dropbox.com/scl/fi/your-file-id/image.png?rlkey=your-key&raw=1 Read more about forcing downloads from Dropbox here: # Ensuring content changes TRMNL syncs plugin content according to your plugin's refresh interval, for example every 15 minutes. But we only generate new screen renders if the content changes. Learn more about how this works [here](https://help.trmnl.com/en/articles/10113695-how-refresh-rates-work). For most plugins, this is straightforward as the underlying data will surely be different if something happens. But with images, we use the headers `etag` with `If-None-Match` and `last-modified` with `If-Modified-Since` to determine if we need to process that image. Adjust your image endpoint to support these headers. Alternatively you can use our [Alias plugin](https://help.trmnl.com/en/articles/10701448-alias-plugin) and TRMNL will serve your generated image directly. Just note that this method requires you to format images to [our exact specifications](https://docs.trmnl.com/go/imagemagick-guide), while Image Display does this for you automatically. --- # How to change my time zone Many TRMNL plugins work best when we're aware of your local time zone. During registration at trmnl.com/signup we attempt to "guess" this, but it's not always perfect. To update your time zone, visit your Account page by clicking the bottom-right icon in the nav bar or going directly to . From here you can set both your time zone and your preferred language. The language setting will improve your TRMNL experience in a few places: 1. the web app interface (labels, description text, buttons, etc) 2. plugin renders (localized dates, keywords) 3. custom plugins (via a growing "word dictionary") To contribute to our localization efforts, go here: --- # Weather icons Our native [Weather](https://help.trmnl.com/en/articles/10033272-weather) and [Tempest Weather Station](https://help.trmnl.com/en/articles/10447991-tempest-weather-station) plugins correlate various conditions (sunny, partly cloudy, etc) to SVG icons. You are welcome to use these icons for your own weather plugin development. ### Icons - all layouts Prefix the following URLs with `https://trmnl.com`. /images/plugins/weather/wi-alien.svg /images/plugins/weather/wi-barometer.svg /images/plugins/weather/wi-celsius.svg /images/plugins/weather/wi-cloud-down.svg /images/plugins/weather/wi-cloud-refresh.svg /images/plugins/weather/wi-cloud-up.svg /images/plugins/weather/wi-cloud.svg /images/plugins/weather/wi-cloudy-gusts.svg /images/plugins/weather/wi-cloudy-windy.svg /images/plugins/weather/wi-cloudy.svg /images/plugins/weather/wi-day-cloudy-gusts.svg /images/plugins/weather/wi-day-cloudy-high.svg /images/plugins/weather/wi-day-cloudy-windy.svg /images/plugins/weather/wi-day-cloudy.svg /images/plugins/weather/wi-day-fog.svg /images/plugins/weather/wi-day-hail.svg /images/plugins/weather/wi-day-haze.svg /images/plugins/weather/wi-day-light-wind.svg /images/plugins/weather/wi-day-lightning.svg /images/plugins/weather/wi-day-rain-mix.svg /images/plugins/weather/wi-day-rain-wind.svg /images/plugins/weather/wi-day-rain.svg /images/plugins/weather/wi-day-showers.svg /images/plugins/weather/wi-day-sleet-storm.svg /images/plugins/weather/wi-day-sleet.svg /images/plugins/weather/wi-day-snow-thunderstorm.svg /images/plugins/weather/wi-day-snow-wind.svg /images/plugins/weather/wi-day-snow.svg /images/plugins/weather/wi-day-sprinkle.svg /images/plugins/weather/wi-day-storm-showers.svg /images/plugins/weather/wi-day-sunny-overcast.svg /images/plugins/weather/wi-day-sunny.svg /images/plugins/weather/wi-day-thunderstorm.svg /images/plugins/weather/wi-day-windy.svg /images/plugins/weather/wi-degrees.svg /images/plugins/weather/wi-direction-down-left.svg /images/plugins/weather/wi-direction-down-right.svg /images/plugins/weather/wi-direction-down.svg /images/plugins/weather/wi-direction-left.svg /images/plugins/weather/wi-direction-right.svg /images/plugins/weather/wi-direction-up-left.svg /images/plugins/weather/wi-direction-up-right.svg /images/plugins/weather/wi-direction-up.svg /images/plugins/weather/wi-dust.svg /images/plugins/weather/wi-earthquake.svg /images/plugins/weather/wi-fahrenheit.svg /images/plugins/weather/wi-fire.svg /images/plugins/weather/wi-flood.svg /images/plugins/weather/wi-fog.svg /images/plugins/weather/wi-gale-warning.svg /images/plugins/weather/wi-hail.svg /images/plugins/weather/wi-horizon-alt.svg /images/plugins/weather/wi-horizon.svg /images/plugins/weather/wi-hot.svg /images/plugins/weather/wi-humidity.svg /images/plugins/weather/wi-hurricane-warning.svg /images/plugins/weather/wi-hurricane.svg /images/plugins/weather/wi-lightning.svg /images/plugins/weather/wi-lunar-eclipse.svg /images/plugins/weather/wi-meteor.svg /images/plugins/weather/wi-moon-alt-first-quarter.svg /images/plugins/weather/wi-moon-alt-full.svg /images/plugins/weather/wi-moon-alt-new.svg /images/plugins/weather/wi-moon-alt-third-quarter.svg /images/plugins/weather/wi-moon-alt-waning-crescent-1.svg /images/plugins/weather/wi-moon-alt-waning-crescent-2.svg /images/plugins/weather/wi-moon-alt-waning-crescent-3.svg /images/plugins/weather/wi-moon-alt-waning-crescent-4.svg /images/plugins/weather/wi-moon-alt-waning-crescent-5.svg /images/plugins/weather/wi-moon-alt-waning-crescent-6.svg /images/plugins/weather/wi-moon-alt-waning-gibbous-1.svg /images/plugins/weather/wi-moon-alt-waning-gibbous-2.svg /images/plugins/weather/wi-moon-alt-waning-gibbous-3.svg /images/plugins/weather/wi-moon-alt-waning-gibbous-4.svg /images/plugins/weather/wi-moon-alt-waning-gibbous-5.svg /images/plugins/weather/wi-moon-alt-waning-gibbous-6.svg /images/plugins/weather/wi-moon-alt-waxing-crescent-1.svg /images/plugins/weather/wi-moon-alt-waxing-crescent-2.svg /images/plugins/weather/wi-moon-alt-waxing-crescent-3.svg /images/plugins/weather/wi-moon-alt-waxing-crescent-4.svg /images/plugins/weather/wi-moon-alt-waxing-crescent-5.svg /images/plugins/weather/wi-moon-alt-waxing-crescent-6.svg /images/plugins/weather/wi-moon-alt-waxing-gibbous-1.svg /images/plugins/weather/wi-moon-alt-waxing-gibbous-2.svg /images/plugins/weather/wi-moon-alt-waxing-gibbous-3.svg /images/plugins/weather/wi-moon-alt-waxing-gibbous-4.svg /images/plugins/weather/wi-moon-alt-waxing-gibbous-5.svg /images/plugins/weather/wi-moon-alt-waxing-gibbous-6.svg /images/plugins/weather/wi-moon-first-quarter.svg /images/plugins/weather/wi-moon-full.svg /images/plugins/weather/wi-moon-new.svg /images/plugins/weather/wi-moon-third-quarter.svg /images/plugins/weather/wi-moon-waning-crescent-1.svg /images/plugins/weather/wi-moon-waning-crescent-2.svg /images/plugins/weather/wi-moon-waning-crescent-3.svg /images/plugins/weather/wi-moon-waning-crescent-4.svg /images/plugins/weather/wi-moon-waning-crescent-5.svg /images/plugins/weather/wi-moon-waning-crescent-6.svg /images/plugins/weather/wi-moon-waning-gibbous-1.svg /images/plugins/weather/wi-moon-waning-gibbous-2.svg /images/plugins/weather/wi-moon-waning-gibbous-3.svg /images/plugins/weather/wi-moon-waning-gibbous-4.svg /images/plugins/weather/wi-moon-waning-gibbous-5.svg /images/plugins/weather/wi-moon-waning-gibbous-6.svg /images/plugins/weather/wi-moon-waxing-6.svg /images/plugins/weather/wi-moon-waxing-crescent-1.svg /images/plugins/weather/wi-moon-waxing-crescent-2.svg /images/plugins/weather/wi-moon-waxing-crescent-3.svg /images/plugins/weather/wi-moon-waxing-crescent-4.svg /images/plugins/weather/wi-moon-waxing-crescent-5.svg /images/plugins/weather/wi-moon-waxing-gibbous-1.svg /images/plugins/weather/wi-moon-waxing-gibbous-2.svg /images/plugins/weather/wi-moon-waxing-gibbous-3.svg /images/plugins/weather/wi-moon-waxing-gibbous-4.svg /images/plugins/weather/wi-moon-waxing-gibbous-5.svg /images/plugins/weather/wi-moon-waxing-gibbous-6.svg /images/plugins/weather/wi-moonrise.svg /images/plugins/weather/wi-moonset.svg /images/plugins/weather/wi-na.svg /images/plugins/weather/wi-night-alt-cloudy-gusts.svg /images/plugins/weather/wi-night-alt-cloudy-high.svg /images/plugins/weather/wi-night-alt-cloudy-windy.svg /images/plugins/weather/wi-night-alt-cloudy.svg /images/plugins/weather/wi-night-alt-hail.svg /images/plugins/weather/wi-night-alt-lightning.svg /images/plugins/weather/wi-night-alt-partly-cloudy.svg /images/plugins/weather/wi-night-alt-rain-mix.svg /images/plugins/weather/wi-night-alt-rain-wind.svg /images/plugins/weather/wi-night-alt-rain.svg /images/plugins/weather/wi-night-alt-showers.svg /images/plugins/weather/wi-night-alt-sleet-storm.svg /images/plugins/weather/wi-night-alt-sleet.svg /images/plugins/weather/wi-night-alt-snow-thunderstorm.svg /images/plugins/weather/wi-night-alt-snow-wind.svg /images/plugins/weather/wi-night-alt-snow.svg /images/plugins/weather/wi-night-alt-sprinkle.svg /images/plugins/weather/wi-night-alt-storm-showers.svg /images/plugins/weather/wi-night-alt-thunderstorm.svg /images/plugins/weather/wi-night-clear.svg /images/plugins/weather/wi-night-cloudy-gusts.svg /images/plugins/weather/wi-night-cloudy-high.svg /images/plugins/weather/wi-night-cloudy-windy.svg /images/plugins/weather/wi-night-cloudy.svg /images/plugins/weather/wi-night-fog.svg /images/plugins/weather/wi-night-hail.svg /images/plugins/weather/wi-night-lightning.svg /images/plugins/weather/wi-night-partly-cloudy.svg /images/plugins/weather/wi-night-rain-mix.svg /images/plugins/weather/wi-night-rain-wind.svg /images/plugins/weather/wi-night-rain.svg /images/plugins/weather/wi-night-showers.svg /images/plugins/weather/wi-night-sleet-storm.svg /images/plugins/weather/wi-night-sleet.svg /images/plugins/weather/wi-night-snow-thunderstorm.svg /images/plugins/weather/wi-night-snow-wind.svg /images/plugins/weather/wi-night-snow.svg /images/plugins/weather/wi-night-sprinkle.svg /images/plugins/weather/wi-night-storm-showers.svg /images/plugins/weather/wi-night-thunderstorm.svg /images/plugins/weather/wi-rain-mix.svg /images/plugins/weather/wi-rain-wind.svg /images/plugins/weather/wi-rain.svg /images/plugins/weather/wi-raindrop.svg /images/plugins/weather/wi-raindrops.svg /images/plugins/weather/wi-refresh-alt.svg /images/plugins/weather/wi-refresh.svg /images/plugins/weather/wi-sandstorm.svg /images/plugins/weather/wi-showers.svg /images/plugins/weather/wi-sleet.svg /images/plugins/weather/wi-small-craft-advisory.svg /images/plugins/weather/wi-smog.svg /images/plugins/weather/wi-smoke.svg /images/plugins/weather/wi-snow-wind.svg /images/plugins/weather/wi-snow.svg /images/plugins/weather/wi-snowflake-cold.svg /images/plugins/weather/wi-solar-eclipse.svg /images/plugins/weather/wi-sprinkle.svg /images/plugins/weather/wi-stars.svg /images/plugins/weather/wi-storm-showers.svg /images/plugins/weather/wi-storm-warning.svg /images/plugins/weather/wi-strong-wind.svg /images/plugins/weather/wi-sunrise.svg /images/plugins/weather/wi-sunset.svg /images/plugins/weather/wi-thermometer-exterior.svg /images/plugins/weather/wi-thermometer-internal.svg /images/plugins/weather/wi-thermometer.svg /images/plugins/weather/wi-thunderstorm.svg /images/plugins/weather/wi-time-1.svg /images/plugins/weather/wi-time-10.svg /images/plugins/weather/wi-time-11.svg /images/plugins/weather/wi-time-12.svg /images/plugins/weather/wi-time-2.svg /images/plugins/weather/wi-time-3.svg /images/plugins/weather/wi-time-4.svg /images/plugins/weather/wi-time-5.svg /images/plugins/weather/wi-time-6.svg /images/plugins/weather/wi-time-7.svg /images/plugins/weather/wi-time-8.svg /images/plugins/weather/wi-time-9.svg /images/plugins/weather/wi-tornado.svg /images/plugins/weather/wi-train.svg /images/plugins/weather/wi-tsunami.svg /images/plugins/weather/wi-umbrella.svg /images/plugins/weather/wi-volcano.svg /images/plugins/weather/wi-wind-beaufort-0.svg /images/plugins/weather/wi-wind-beaufort-1.svg /images/plugins/weather/wi-wind-beaufort-10.svg /images/plugins/weather/wi-wind-beaufort-11.svg /images/plugins/weather/wi-wind-beaufort-12.svg /images/plugins/weather/wi-wind-beaufort-2.svg /images/plugins/weather/wi-wind-beaufort-3.svg /images/plugins/weather/wi-wind-beaufort-4.svg /images/plugins/weather/wi-wind-beaufort-5.svg /images/plugins/weather/wi-wind-beaufort-6.svg /images/plugins/weather/wi-wind-beaufort-7.svg /images/plugins/weather/wi-wind-beaufort-8.svg /images/plugins/weather/wi-wind-beaufort-9.svg /images/plugins/weather/wi-wind-deg.svg /images/plugins/weather/wi-windy.svg ### Icons - specific layouts Icon options below are resized to fit better in a given layout. As above, prefix these URLs with `https://trmnl.com`. /images/plugins/weather/full/cloudy-gusts.svg /images/plugins/weather/full/showers.svg /images/plugins/weather/full/snow.svg /images/plugins/weather/full/sunny.svg /images/plugins/weather/full/windy.svg /images/plugins/weather/half_horizontal/cloudy-gusts.svg /images/plugins/weather/half_horizontal/showers.svg /images/plugins/weather/half_horizontal/snow.svg /images/plugins/weather/half_horizontal/sunny.svg /images/plugins/weather/half_horizontal/windy.svg /images/plugins/weather/half_vertical/cloudy-gusts.svg /images/plugins/weather/half_vertical/showers.svg /images/plugins/weather/half_vertical/snow.svg /images/plugins/weather/half_vertical/sunny.svg /images/plugins/weather/half_vertical/windy.svg /images/plugins/weather/quadrant/cloudy-gusts.svg /images/plugins/weather/quadrant/showers.svg /images/plugins/weather/quadrant/snow.svg /images/plugins/weather/quadrant/sunny.svg /images/plugins/weather/quadrant/windy.svg --- # DNS and Network Requests for Firewall Whitelists This information only applies to using **trmnl.com** as your device's server (where you organize plugins and playlists). If you are using a [BYOS (Bring Your Own Server)](https://docs.trmnl.com/go/diy/byos) solution, this is not applicable unless *proxying* (server-specific feature). There are only three domains that your TRMNL device uses to download its next screen: - https://**trmnl.app** - Receiving information about the next screen - https://**time.google.com** - Accurate time keeping - https://**trmnl.s3.us-east-2.amazonaws.com** - Downloading the screen itself (PNG file) Note that the [Alias](https://help.trmnl.com/en/articles/10701448-alias-plugin) and [Redirect](https://help.trmnl.com/en/articles/11035846-redirect-plugin) plugins are [special](https://help.trmnl.com/en/articles/11628971-testing-your-alias-or-redirect-plugin) and designed so you can make requests to unique sources for image screens. **If a TRMNL device can connect and retrieve screens via one network or hotspot but not another, and you are sure the[WiFi connection is working](https://help.trmnl.com/en/articles/10193157-device-wifi-troubleshooting) properly, this can be a possible cause of problems in restrictive environments.** If you are on a corporate network, it may require whitelisting to access particular domains, *or* verifying that traffic to these domains is not blacklisted. ### Pi-Hole Query Log If you use Pi-Hole as a DNS server in your home network, you can check your query log to ensure all requests are being allowed through: If any of these requests are blocked, your TRMNL device will not be able to retrieve the next screen to display. --- # Polymarket Connecting Polymarket to TRMNL takes just a few seconds and does not require any authentication with your Polymarket account. ## **Step 1 - Visit the Polymarket plugin** Inside TRMNL, navigate to Plugins > Polymarket. ## **Step 2 - Connect Polymarket** Select a popular tag (Market category) from the dropdown. From business to crypto, pop culture to politics. Polygon has ~9,000 possible tags, so if you'd like to add one that is not listed in our application, email the Tag ID and we'll plug it into your account. Find a list of all tags + tag IDs here (note: JSON data, just search the page): For example, if you're interested in space travel, search the page content for "space" and then give us this ID, 500: After select a tag, click Save. ## **Step 3 - Configure Plugin** Determine how often you'd like this plugin to refresh. Note that we currently look for market trades that are active, not closed, and with an expiry/end between today and 2 weeks from now. ## **Step 4 - You're Done!** Based on your desired refresh settings, TRMNL will begin aggregating popular market events from Polymarket very soon. Stay focused. --- # How to Disassemble your TRMNL This video walks through taking apart your device in under 1 minute. We recommend first watching the entire video, then trying it yourself. You may also get tips from our other guide: [how to assemble your TRMNL](https://help.trmnl.com/en/articles/10003236-how-to-assemble-your-trmnl). [https://www.youtube.com/embed/uvF9No5Z1_A?rel=0](https://www.youtube.com/embed/uvF9No5Z1_A?rel=0) If you have any issues, email [support@trmnl.com](mailto:support@trmnl.com). --- # Finding your TRMNL Serial Every TRMNL has 2 immutable characteristics -- a Mac Address, and a Serial. Mac Addresses are generated by our manufacturer, and Serial values are printed directly on the PCB (circuit board). In some cases, TRMNL may need to know one of these values to help debug a connection issue. If you can't [find your MAC](https://help.trmnl.com/en/articles/10614205-finding-your-trmnl-mac-address), you can instead [disassemble your device](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl). ## **Open your TRMNL** Taking apart your TRMNL does not void any warranties, just [watch our video](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl) to do it safely. Once open, you will see a small QR code with a 5 digit value beneath it. In this example, the Serial is `01174`. ## **Let us know** Send us a live chat (bottom right chat icon) or email if you need help leveraging your Serial to resolve an issue. --- # Screensaver TRMNL has a small (but growing) repository of fair use images that comprise our Screensaver graphic library. The currently collection of images is limited and we welcome submissions from our community. Email with images you'd like to be considered for the Screensaver library. --- # TRMNL mobile app We built TRMNL with a mobile-friendly navigation and responsive design for iPhones, Androids, tablets, and everything in between. But how can you get to this experience in 1 tap? Save TRMNL to your device's home screen. ## Step 1 - Visit TRMNL On your phone or table, visit [trmnl.com](https://trmnl.com) with the Safari or Firefox mobile browser application. We suggest logging in and visiting your dashboard, which looks like the screen above. ## Step 2 - Add TRMNL to your device In the browser application navigation you should see a "share" icon, which is usually a square with an up-arrow inside. Tap this icon, then find and select the "Add to Home Screen" option. Provide a new name, we suggest simply "TRMNL." Select "Add" to finish. ## Step 3 - You're Done! TRMNL is just a tap away while you're on the go. Stay focused. --- # Reset 2FA To remove the 2FA requirement from your account: 1. Log into the email inbox that your TRMNL account belongs to 2. Email with subject line "Reset 2FA" 3. This will create a ticket in our live chat tool, which is managed by several team members and checked frequently 4. We will disable 2FA and send you a confirmation message --- # Missing Data in Multiple Polling URLs ## Using Multiple Polling URLs Plugins that use the **[polling strategy](https://help.trmnl.com/en/articles/9510536-private-plugins)** can provide multiple URLs, line-break separated, to get data from multiple sources or endpoints to create a more robust plugin. For example: This provides data in **Your Variables** for each URL under an indexed object in the format `IDX_#`, where the # is incremented from 0. So, if you have 4 URLs, like in the example above, you would have data in Your Variables like: - `#{{ IDX_0 }}` - `#{{ IDX_1 }}` - `#{{ IDX_2 }}` - `#{{ IDX_3 }}` - `#{{ trmnl }}` ## URL Data Missing, Less `IDX_#` Variables Than Expected In the normal plugin flow of screen generation, this would not be noticeable because our renderer would not generate a screen until all the data was retrieved (assuming no errors with unreachable URLs). However, if you were building a plugin and ran a *Force Refresh*, then went into *Edit Markup* relatively quickly, the system may still be loading all of the URLs. The solution is to wait 2-3 seconds (cumulatively per URL), then refresh the markup editor page. *Remember: The data is not being refreshed in your browser, but via our backend servers, and then, once collected, becomes visible in your browser inside the web app.* ### Variables Being Overwritten This can only occur for plugins created **before** August 19, 2025 Using the polling strategy with multiple polling URLs, if you changed the order of the line-separated URLs in your plugin later on, it could cause the data indexes to be overwritten in the resulting data shown in **Your Variables**. This was visible if *Debug Logging* was enabled in your plugin settings page and a *Force Refresh* was run. This was an elusive bug that has been resolved as of **August 19, 2025**. However, if your plugin has been affected by this bug, you will need to send a special command to our servers using a URL query string to fix the issue. Plugin Settings URL Format: `https://trmnl.com/plugin_settings/#{{pluginId}}/edit` Example: `https://trmnl.com/plugin_settings/987654321/edit` You will need to append `?reset_polling_url_namespaces=true` to the end of the browser URL: Example: `https://trmnl.com/plugin_settings/987654321/edit?reset_polling_url_namespaces=true` Once appended to the end of the URL in your browser, press **Enter**. You will receive a notification that it has been completed: You can now *Force Refresh* to collect the data again, accurately. --- # How to cancel an order You may cancel + refund an unfulfilled order, for any reason, in 1 click. ## Step 1 - Visit your Order Status page The easiest way to get here is by clicking the link in your Order Confirmation email. Click "View your order," no login or signup is required. ## Step 2 - Click Cancel You should see an "Order actions" widget on the Order Status page. Click the Cancel button. ​ You will be prompted to confirm: ### Step 3 - You're Done After clicking Proceed, you should see an updated Order Status. On our backend, this order will appear as canceled/archived, and be automatically refunded. You will also receive a cancelation confirmation email, to the same inbox as your initial Order Confirmation email: Pending your payment method, refunds may be instant or take a few minutes/days to process. Note: **if you paid with crypto you need to email orders@trmnl.com** **for a manual refund**. Our crypto payment partner does not offer automatic refunds. ## Troubleshooting Automatic order cancelation is available for unfulfilled orders < $1,200. If your order has been fulfilled, it is subject to our Terms of service, linked from our website and online store: (see "Billing" section) If your order is > $1,200, email for a manual cancelation. This limitation is in place to prevent potential fraud or misunderstandings about a bulk/group order. --- # How to Contact Support If you have an unresolved issue or have questions about the product, use the **icon in the bottom-right of the page** on this article's web page to open a live chat conversation. You may also email questions or suggestions directly to . --- # Debugging native plugins TRMNL owners with the Developer edition add-on can click "Force Refresh" on any plugin settings screen to generate a new image, based on their latest settings. But this takes a few seconds, and you can't reason about the graphic if your intention is to figure out a styling issue, or expose hidden information. For this purpose we have **Demo Mode**. ## **What is Demo Mode?** To see how native plugins work *in the browser* visit [trmnl.com/plugins/demo](https://trmnl.com/plugins/demo) while logged into TRMNL. Select a plugin from the list to view all 4x layouts with demo data. For example `https://trmnl.com/plugins/google_analytics`: ## **Using your own data** Simply append `?data=true` to the plugin you're reviewing to merge in real data from your TRMNL account. For example `https://trmnl.com/plugins/simple_analytics?data=true` injects actual traffic from our website. (You must connect the chosen plugin for this to work) ## **Choosing a specific instance of your plugin data** Suppose you've connected multiple calendars, and want to examine the markup of a specific one. https://trmnl.com/plugins/apple_calendar?data=true&plugin_setting_id=xxx Set `xxx` to the ID of the Apple Calendar you want to review. Find this in the URL of the plugin settings page for that instance of Apple Calendar. ## **What's next?** Combine Demo Mode (this) with [Data Mode](https://trmnl.com/blog/introducing-data-mode) ([docs](https://docs.trmnl.com/go/private-api/fetch-plugin-content)) to quickly remix a native plugin into a private one, or to mock features for us to consider incorporating for everyone. --- # LLM-ready docs To feed TRMNL's knowledge base into your own LLM, here are some resources. **API docs** Created via GitBook, which [supports](https://docs.gitbook.com/llm-ready-docs) the new `llms.txt` standard. You may also pull a copy of our API docs from GitHub: ​ **Help Center** Created via Intercom, JSON here: (*last updated January 30, 2026*) **Framework (plugin UI system)** Pre-trained: ​ ​ v1 (legacy design system): - Static: - JSON: v2 (current design system): - Static: - JSON: **What else?** Let us know what you'd like to see -- . --- # What is a Virtual Device As a perk to supporters and developers who purchased a [developer edition license](https://help.trmnl.com/en/articles/11629486-calculating-byod-and-dev-edition-add-ons) (separate add-on or as part of the Clarity Kit add-on), when a hardware purchase is made, we offer the option to create a *virtual device*. # Claiming a Device You can [claim a device](https://trmnl.com/claim-a-device) with your order number and email address. This provides you a FriendlyID, necessary to [register for an account](https://trmnl.com/signup) on trmnl.com. Once your account is created with a virtual device, you are welcome to start creating private plugins and learning about the interface, its tools, and its powerful [features](https://help.trmnl.com/en/articles/11663305-playlist-scheduler). # When Your Device Arrives During [device setup](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device), you will add your device to your account using the *FriendlyId* displayed on the unit. **Your virtual device's plugins and playlist will automatically be assigned to your new hardware device,** and the temporary virtual device will be removed automatically. --- # Refund and Exchange Policy ## Source of Truth (Official Policy) The TRMNL [terms of service](https://trmnl.com/terms) are the source of truth for all things. # Physical Devices ## Defective or Damaged in Shipment We make every effort to [build](https://trmnl.com/blog/in-house-fulfillment) and [deliver](https://trmnl.com/blog/slashing-postage-rates) our products in great condition, but accidents happen and sometimes couriers or software misbehaves. We resolve these issues on a case-by-case basis through our online chat (see bottom right of any page) or by emailing . ## Something else Thousands of customers have been ecstatic with their devices after receiving them, but sometimes there is a hiccup or a bug or a quirk that our team has not experienced, leading to frustration. Send us a live chat (bottom right corner) or email and we'll make it right. # BYOD Software License Bring Your Own Device ([BYOD](https://shop.trmnl.com/products/byod)) licenses are fully refundable within 14 days of purchase. Please send a request to with your order number. --- # Framework Design Docs The *source of truth* is always the [Framework Design Documentation](https://trmnl.com/framework/). *An overview of elements in Framework.* # Utilities - **Size**: Define exact width and height dimensions for elements - **Spacing**: Control element spacing with fixed margin and padding values - **Gap**: Set precise spacing between elements with predefined gap values - **Flex**: Arrange elements with flexible layouts and alignment options - **Grid**: Create grid layouts with predefined column structures - **Background**: Grayscale dithered patterns optimized for 1-bit rendering - **Border**: Apply border patterns that create the illusion of different border intensities - **Rounded**: Control element rounding with predefined values - **Visibility**: Control element visibility based on display bit depth - **Responsive**: Adapt styles based on screen width using breakpoint prefixes - **Responsive Test**: Test responsive utilities and compare SCSS mixins with CSS classes - **Text**: Control text color, alignment and formatting - **Image**: Optimize images using dithering techniques for 1-bit rendering - **Image Stroke**: Legible images when displayed on shaded backgrounds - **Text Stroke**: Legible text when displayed on shaded backgrounds - **Scale**: Scale interface to affect content density and readability - **Aspect Ratio**: Maintain consistent proportions for elements regardless of their content # Modulations - **Overflow**: Handle column items overflow - **Table Overflow**: Handle table rows overflow - **Clamp**: Manage text overflow with single and multi-line truncation - **Format Value**: Format numbers and values with consistent styling - **Fit Value**: Automatically resize numbers and values to fit within their containers - **Content Limiter**: Change font size when content overflows to fit within the container - **Pixel Perfect**: Ensure text renders with crisp edges by aligning to the pixel grid - **Framework Runtime**: How the runtime applies layout, clamping, overflow, and presentation adjustments at render time # Base - **Screen**: Device screen dimensions, orientation, and display properties - **View**: Show your plugin in different sizes with Mashup view containers - **Layout**: Primary container for organizing plugin content - **Title Bar**: Standardized title bar with plugin information and instance details - **Columns**: Implement zero-config column layouts for content organization - **Mashup**: Assemble multiple plugin views into a single interface # Elements - **Title**: Style headings with consistent typography - **Value**: Display data values with consistent formatting - **Label**: Create clear labels for unified content identification - **Description**: Format descriptive text with standardized styles - **Divider**: Create horizontal or vertical dividers between elements # Components - **Rich Text**: Display formatted paragraphs with alignment and size variants - **Item**: Build standardized list items and content blocks - **Table**: Create data tables optimized for 1-bit rendering - **Chart**: Visualize data optimized for 1-bit rendering - **Progress**: Display progress bars in different styles # Fonts See our [fonts article](https://help.trmnl.com/en/articles/12494341-what-fonts-are-used-in-framework). --- # BOYD / BYDO / BDYO - [BYOD Info](https://docs.trmnl.com/go/diy/byod#build-from-scratch-advanced) - [Calculating BYOD Licenses](https://help.trmnl.com/en/articles/11629486-calculating-byod-and-dev-edition-add-ons) --- # TRMNL+ FAQ Learn more about the origin of TRMNL+ here: ​ # Pricing $5 per month, per device in your account. You may unpair a device and create a new account with it if you prefer to only enable TRMNL+ on 1 device. # Benefits summary - up to 5 minute per-plugin refresh rate (default: 15 minutes) - 5kb webhook payloads (default: 2kb) - up to 30 webhooks per hour, per Private Plugin (default: 12) - early access to new features To request additional controls, email . ***Unhappy with this article?*** *Let us know what you were searching for and why this article missed the mark for you. Email [support@trmnl.com](mailto:support@trmnl.com?subject=TRMNL+%20FAQ%20Article%20Feedback) and let us know (use the link to automatically include the article title)!* --- # Using Data Mode to Redesign a Native Plugin `This article is a stub. The source of truth is our [Plugin Data API docs](https://docs.trmnl.com/go/private-api/plugin-data).` Our native plugins, such as [Weather](https://trmnl.com/integrations/weather), try to meet the needs of most people, but sometimes you want to customize them even further. To do that, you can use the **Plugin Merge** strategy in a *Private Plugin* to incorporate the **data** from a *different plugin* with your **design**. While the original plugin would be in your playlist and *hidden*, the data is available to use with this strategy. Follow our guide docs on using [data mode](https://docs.trmnl.com/go/private-api/plugin-data) for more information. --- # Redirect Plugin Setting up the Redirect plugin lets you configure endpoints that respond to your TRMNL device as if it were the core web application. # Use Cases 1. You already use the [Alias plugin](https://help.trmnl.com/en/articles/10701448-alias-plugin), but want to avoid screen flicker if the content hasn't changed. 2. You want a more dynamic rotation of content. Instead of the fixed minutes interval from your [Playlist > Playlist Group](https://trmnl.com/playlists) preferences, you may instruct a device to wait 3 mins, or 20 mins (for example) based on the content being returned. 3. Your server's content is uploaded to remote locations, where the URL is not predictable. Either because it is hashed (e.g. S3 bucket) or otherwise unique. The [Alias plugin](https://help.trmnl.com/en/articles/10701448-alias-plugin) requires a static endpoint, whereas Redirect allows you to pass back a different URL on each request. # Step 1 - Configure Plugin From the Plugins directory, select the Redirect plugin. Provide a URL endpoint from your server. This Web Address must be publicly accessible on the web. TRMNL will ping your server to get the response data. Your server should respond with the below JSON data. Please keep in mind that TRMNL has strict timeout of 2 seconds, before sending a failure response. { filename: 'get-coffee', url: 'https://192.168.2.1:4567/get-coffee.bmp', refresh_rate: 3000 } Note that the `url` response param can be anywhere, including a local network. **Filename** The filename attribute is used as the "diff" parameter. If the filename between requests does not change, your device will not refresh the screen, thus avoiding unnecessary screen flicker and saving battery power. ​**URL** Make sure this points to an image that is 1 bit bitmap(bmp3) or 1 bit PNG, 800x480 pixels. [See here](https://docs.trmnl.com/go/imagemagick-guide) for a hint on generating this format with ImageMagick. Your image may be at a local destination, for example `192.168.5.3/my/image.png`. **Refresh Rate** The number of seconds your device should sleep before waking up again to request new content. The fastest you can go is 1x/minute (subject to change). # Step 2 - You're Done! Based on your device's refresh settings, TRMNL will begin forwarding your content requests to the Web Address specified above. Stay focused. Credits: Redirect by Alex Bickov from [Noun Project](https://thenounproject.com/browse/icons/term/redirect/) (CC BY 3.0) # Troubleshooting **Request origins** TRMNL operates a network of worker boxes for plugin processing, including Hetzner servers in the EU. If your JSON URL endpoint expects only requests from a specific region, it may be blocking our attempt to fetch the JSON endpoint's 3x response body payload params. **Image metadata** Most issues with this plugin are due to the image formatting not matching [our specifications](https://docs.trmnl.com/go/imagemagick-guide) exactly. For example, the OG TRMNL device model expects 800x480 pixel images, not 1600x960 or some other equivalent ratio. **Image response headers** There have been reports that your image's endpoint must respond with a `content-length` header; the value should be an integer. This is a requirement we are [actively investigating](https://github.com/usetrmnl/firmware/issues/156) and intend to remove. If you are generating images on the fly, you may not have this value by default because the content is still streaming. In this case, use a dummy value. **Caching** At the bottom of your Alias plugin settings, there is a section called **Enable cache?**: If set to **no**, your device will always re-draw the image URL, even if its filename matches the last rendered filename (*e.g.*, the image is rewritten at the source using the same filename). --- # TRMNL Hackathons # Is There a Hackathon Right Now? Head over to our [blog](https://trmnl.com/blog) to see if there are any active Hackathons. # Where can I Get an Account to Participate? ## Where do I Submit a Plugin? We have a dedicated [hackathon](https://trmnl.com/hackathon) page that let's you sign-up (if you do not already have a device and/or an account) or submit (if you are logged in). # I Am Having Issues Submitting My Entry Contact --- # So richtest du dein neues Gerät ein Herzlichen Glückwunsch zu deinem TRMNL! Wir freuen uns riesig, dass du dabei bist. Wenn du in der Verpackung eine eine transparente Folie gefunden hast, findest du [hier weitere Infos](https://intercom.help/trmnl/en/articles/10217443-applying-the-screen-anti-glare-protector) dazu. Wir haben diese Seite ins Deutsche übersetzt, um Benutzern aus der DACH-Region einen leichteren Einstieg zu ermöglichen. # Schritt 0 – Verlier keine Zeit Wenn dir die folgenden Schritte unklar sind oder sich etwas in deiner Oberfläche unterscheidet, [suche bitte die MAC-Adresse deines Geräts heraus](https://help.trmnl.com/en/articles/10614205-finding-your-trmnl-mac-address) und schick uns via Live-Chat eine Nachricht (unten rechts auf der Website). Du kannst auch unser [Find My Device-Tool](https://trmnl.com/find-my-device) nutzen, um deine Friendly ID herauszufinden, ohne auf uns warten zu müssen. # Schritt 1 – TRMNL einschalten Schalte dein Gerät auf der Rückseite über den Netzschalter ein. Nach dem Start und ein paar kurzen Blinksignalen sollte dein Bildschirm ungefähr so aussehen: Soll sich so gar nichts tun, ist der Akku möglicherweise nicht geladen. Schließe in diesem Fall dein TRMNL an das USB-C-Kabel an (falls es in deiner Bestellung enthalten war) und starte das Gerät neu, indem du den Schalter auf der Rückseite einmal aus- und wieder einschaltest. # Schritt 2 – Mit dem WLAN verbinden Suche auf deinem Handy oder Computer nach einem WLAN mit dem Namen „TRMNL“ und verbinde dich damit, wie hier abgebildet. Nach der Verbindung öffnet sich automatisch ein WLAN-Portal (wie im mittleren Screenshot). Warte kurz, während TRMNL nach verfügbaren Netzwerken sucht, oder gib den Namen (SSID) und das Passwort deines Netzwerks händisch ein, falls dein WLAN nicht angezeigt wird oder versteckt ist. Klicke anschließend auf „Connect“ (Verbinden), um die Zugangsdaten an dein TRMNL zu übermitteln. *(Deine WLAN-Daten werden [niemals](https://docs.trmnl.com/go/how-it-works#opinionated-device-less-than-greater-than-server-relationship) an unsere Server gesendet.)* Wenn du Schwierigkeiten in einem Firmen- oder Uninetzwerk hast, wirf einen Blick in unsere [entsprechende Anleitung](https://intercom.help/trmnl/en/articles/11663377-setting-up-a-trmnl-on-tricky-wi-fi-situations). Tipp: Für seltene Probleme findest du weitere Hinweise in unserer [Troubleshooting-Anleitung](https://intercom.help/trmnl/en/articles/11663377-setting-up-a-trmnl-on-tricky-wi-fi-situations). # Schritt 3 – Dein Gerät registrieren Nachdem du die Zugangsdaten für deiN WLAN eingegeben hast, startet das TRMNL neu und zeigt eine Installations-Meldung an. Falls der Bildschirm ein Rauschen darstellt und du die 6-stellige Geräte-ID nicht lesen kannst, kannst du sie mit [Find My Device](https://trmnl.com/find-my-device) direkt herausfinden. Wenn du bereits ein TRMNL-Konto besitzt, [füge das neue Gerät](https://trmnl.com/devices/new) dort mit deiner Friendly ID hinzu. Wenn du noch kein TRMNL-Konto hast (das hat nichts mit deinem Bestellkonto zu tun), registriere dich unter # Schritt 4 – Plugins verbinden Besuche auf der TRMNL-Website das Plugin-Verzeichnis und wähle die Datenquellen aus, die du auf deinem Gerät anzeigen möchtest. Sobald dein erstes Plugin verbunden ist, kann es losgehen. Starte dein TRMNL einfach neu, indem du den Schalter auf AUS/AN stellst, oder drücke kurz den kleinen Knopf unter dem Netzschalter, um die Anzeige manuell zu aktualisieren. Dann fordert das Gerät neue Inhalte von der TRMNL-Webanwendung an. Du kannst außerdem eine [Sonderfunktion](https://help.trmnl.com/en/articles/9672080-special-functions) festlegen, die ausgelöst wird, wenn du den Knopf auf der Rückseite deines TRMNL einige Sekunden gedrückt hältst. # Schritt 5 – Viel Spaß! TRMNL soll dich nicht ablenken, sondern informieren und mühelos auf dem neuesten Stand halten, aber ganz ohne Hektik. In deinem TRMNL-Konto unter [Geräte > Bearbeiten](https://trmnl.com/devices) kannst du die Aktualisierungsrate anpassen, den Ruhemodus aktivieren und vieles mehr. --- **Could not display content** --- # Tempest Weather Station Connecting your Tempest WeatherFlow device to TRMNL takes just a few seconds. # Step 1 - Visit the Tempest Weather Station plugin Inside TRMNL, navigate to [Plugins > Tempest Weather Station](https://trmnl.com/plugin_settings/new?keyname=tempest_weather_station). Click to "Connect with Tempest Weather Station" on the right side of the screen. # Step 2 - Connect Tempest You'll be redirected to the tempestwx.com website. Log in and then approve TRMNL's data access request. This is "read only" and will not modify any settings or stations on your account. If for some reason the above screen does not appear, but instead yields "Unknown Application" like below, try switching to a desktop computer (if on a phone) or to Google Chrome (if on Safari). # Step 3 - Configure Plugin Select which Tempest Station's weather data you want to display on your TRMNL device, as well as the measurement format for a few metrics. ​ Click **Save** in the top-right once configured! ## Set the Refresh Rate On the right-sidebar, some new options are visible, but most importantly, the refresh rate option: This determines how often our servers fetch new data from your weather station, independent of how frequently your TRMNL device's [screen refreshes](https://help.trmnl.com/en/articles/10113695-how-refresh-rates-work), set in your [playlist](https://help.trmnl.com/en/articles/11663305-playlist-scheduler). Adjust as desired and click **Save** again. # Step 4 (optional) - Tracking Multiple Stations If you'd like to capture weather from multiple stations, and perhaps combine it on a single screen, [create multiple instances](https://trmnl.com/plugin_settings/new?keyname=tempest_weather_station) of this plugin and then [generate a Mashup](https://intercom.help/trmnl/en/articles/10168132-mashups) to see them all together. In the example above, 2 stations in nearby locations are shown side by side. # Step 5 - You're Done! Based on your device's refresh settings, you'll begin seeing Tempest Weather Station forecasts very soon. Stay focused. --- # Partnerships and Collaborations Contact us at [biz@trmnl.com](mailto:biz@trmnl.com?subject=Let's%20talk) --- # Developer Edition Features From plugin development, API access, device logs, to Discord access and early release firmware, there are multiple benefits to supporting TRMNL with a developer edition license. This feature was announced on November 4, 2024: # What is Developer Edition? Developer Edition is an optional add-on for devices that can be purchased [individually](https://shop.trmnl.com/products/developer-edition), as part of a bundle ([Clarity Kit](https://shop.trmnl.com/products/clarity-kit)), or in-app from your [device settings](https://trmnl.com/devices/current/edit) page. **Developer Edition licenses are linked to devices, not users.** For more information on deciding if and how many licenses you may want, see [this article](https://help.trmnl.com/en/articles/11629486-calculating-byod-and-dev-edition-add-ons). Here are some benefits of adding Developer Edition to your device: - [Private Plugin Development](https://help.trmnl.com/en/articles/9510536-private-plugins) - [Publishing Community Recipes](https://help.trmnl.com/en/articles/10122094-plugin-recipes) - API Keys - Programmatic [Screen Manipulation](https://docs.trmnl.com/go/private-api/screens?q=API) - [Mirror to iPad](https://help.trmnl.com/en/articles/11647459-trmnl-for-ipad) and Android - Device Logs and Render [Debug Logs](https://help.trmnl.com/en/articles/11586187-debugging-private-plugins) - Discord Community - Early Release option for Firmware - Supporting TRMNL's developer community # Developer Edition vs BYOD License Developer Edition is an add-on for TRMNL native devices. BYOD licenses are for DIY (BYOD) devices and include the same features as Developer Edition, plus access to the web platform itself. **You do not need** BYOD + Developer Edition. --- # Import: The ZIP file does not contain settings.yml When exporting a plugin from trmnl.com, it will contain 6 files: - shared.liquid - settings.yml - quadrant.liquid - half_vertical.liquid - half_horizontal.liquid - full.liquid To be able to import a plugin, it must have at least the `full.liquid` and `settings.yml` file present, otherwise you will receive the error: The ZIP file does not contain settings.yml If you are sure the contents necessary are present and well-formatted, then it's on to some other troubleshooting steps. # Private Browser Window (Incognito) Launching a private window or tab is a great option when you want to make sure a browser extension isn't interfering with your upload or other functionality, because, in most cases, they are disabled in an incognito window. However, sometimes it may be your browser, *e.g.*, Safari. # Try a Different Browser While we do test across all modern browsers, a variety of factors, including system installed programs, can interfere. So, use another browser, like Firefox, Chrome, or Safari to see if the error persists. # Export an Existing Plugin Since we can trust our export to be well-formed, export a private plugin and then try and re-import that same file. If it succeeds, it confirms the browser is working and that your original .zip file has an issue. --- # 429 Rate Limit Are you seeing this on your TRMNL? A 429 Rate Limit means "too many requests," which could be caused by a few scenarios. 1. You've just set up your device, and it was having trouble connecting to your local WiFi. The TRMNL firmware automatically retries several times in a row, which might breach our limit. 2. You are sharing a single API Key across multiple BYOD / DIY devices. Pending your use case, it may be better to [acquire more licenses](https://help.trmnl.com/en/articles/11629486-calculating-byod-and-dev-edition-add-ons) or leverage the `/current_screen` endpoint instead ([docs](https://docs.trmnl.com/go/private-api/fetch-screen-content#current-screen)). 3. You are repeatedly clicking the button on the back of your TRMNL within a short period of time. # Rate Limit Details For devices pinging the TRMNL server: - 10x requests within 1 minute for new devices actively being set up - 10x requests within 5 minutes for existing (completed setup) devices For webhooks, rate limits are detailed on the [webhook docs](https://docs.trmnl.com/go/private-plugins/webhooks). ## How to Resolve When your API requests trigger a Rate Limit, the response content will instruct your device to sleep for 5-10 minutes before trying again. This ensures **the rate limit is resolved automatically** on your next organic refresh. Alternatively you can turn your device off for 5 minutes, then back on. --- # TRMNL is (not) a clock There's a saying that even a broken clock is right twice a day. If you take that spirit to heart, TRMNL can be a fantastic clock. But truthfully, TRMNL was never built to keep perfect time. Our servers render each plugin's screen every 15 minutes (or every 5 minutes with [TRMNL+](https://help.trmnl.com/en/articles/11861887-trmnl-faq)) if there [is new content](https://help.trmnl.com/en/articles/10113695-how-refresh-rates-work), and the fastest frequency for the device to display the next playlist entry is 5 minutes. Not exactly the ideal setup for a clock. Also, after downloading a new screen, the TRMNL goes back to sleep to save battery. That is what gives it months of battery life, but also what makes it unsuitable for showing the exact time. Updating every minute or even second would use up the battery in no time and would keep our servers busy rendering a new image for every new minute, per user. While there are numerous e-ink clocks for a lower price point and one-task functionality available through online retailers, TRMNL is meant to show calm, glanceable information that does not need to change every second. A task list. A quote. Your next meeting. The weather. # Plugins and recipes Still, there are [several native clock plugins](https://trmnl.com/plugins/) on our site, and many users have built their own. Have a look at some [community clock recipes here](https://trmnl.com/recipes/?search=clock&sort-by=newest). One popular example is the [Literature Clock](https://trmnl.com/recipes/11141). It shows time in words, but rounded to the next 5 or 15 minutes. For most users, that feels accurate enough. Another great one is the Word Clock, available in [English](https://trmnl.com/recipes/25190/install), [French](https://trmnl.com/recipes/25204/install) and [German](https://trmnl.com/recipes/25205/install), which also shows fuzzy time. # Advanced methods If you want to go further, there are more advanced ways. The [Redirect plugin](https://help.trmnl.com/en/articles/11035846-redirect-plugin) lets you connect your TRMNL directly to your own server. Your server pretends to be the TRMNL core app and tells the device which image to show, how long to sleep, and when to refresh again. This allows you to update the screen more flexibly, currently as fast as once per minute. Perfect if you want to experiment with your own timing, including more frequent "clock-like" updates. Alternatively, you can use the TRMNL WiFi Captive Portal to connect your device to a custom backend like [Terminus (BYOS)](https://help.trmnl.com/en/articles/12263392-connect-your-device-to-terminus-byos) and update it as often as you like. Both options require some setup and are best suited for developers or tinkerers. --- # Ways to Distribute Devices and Plugins to Borrowed Devices You want to make sure a set of individuals can benefit from the content a TRMNL device can provide, but you aren't sure what the best method is for providing the devices and content for these individuals. Here are three options. ## Terminology **primary**: Individual who owns the devices, is defining the strategy, and main account holder. **secondary**: individual who *receives* the device from the ***primary*.** # Managed Device Distribution The **primary** purchases, loads plugins/playlists, and distributes the devices. See the full guide on [preloading and maintaining managed devices](https://help.trmnl.com/en/articles/12835885-preloading-and-maintaining-managed-devices-for-individuals). *Benefits:* - **Secondary** setup is minimal: WiFi only. - Device content is strictly controlled. - Devices cannot be repurposed by **secondary** individuals because it is linked to a single account controlled by the **primary** provider. *Downsides:* - Changes and updates require exclusive effort by the **primary**. - Low battery notifications all go to the **primary**. # Distributed Device Accounts with Unlisted Plugin The **primary** loans the devices to the **secondary** who onboards to trmnl.com like any other customer. However, the **primary** distributes a [private plugin](https://help.trmnl.com/en/articles/9510536-private-plugins) that connects to company information via an *unlisted* publishing link they can distribute directly to **secondary**. *Benefits:* - **Secondary** has full agency over their playlist and schedule. - **Primary** creates and publishes an *unlisted* private plugin to specifically benefit the **secondary**. *Downsides:* - Device is linked to the **secondary**, so they are being asked to do additional steps and cannot be monitored without user/password credential sharing. - **Primary** would need to create guidance for onboarding the individual with the proper plugins and recommended setup. # Self-Hosting Server (BYOS) [Terminus](https://github.com/usetrmnl/byos_hanami) is the open source [self-hosted server](https://docs.trmnl.com/go/diy/byos) maintained by TRMNL. It is still a *work in progress* that is catching up to trmnl.com in features and usability, but is not yet at its 1.0 release. The **primary** uses a Bring-Your-Own-Server solution to provision devices, monitor screen generation, and manage users. The **secondary** sets up the device using [existing guides](https://help.trmnl.com/en/articles/12263392-connect-your-device-to-terminus-byos). *Benefits:* - **Secondary** has full agency over their playlist and schedule. - All data flows through **primary**'s managed server solution with enterprise-minded design features. *Downsides:* - Managing your own server and server resources. - BYOS learning curve for setup and usage is higher for both parties. --- # How to replace your internal components Swapping out an internal part like a PCB is a simple process, but should be done carefully to not damage other components in your TRMNL. TRMNLs are built to last, but e-ink displays are fragile. So whatever you do, be very careful when handling the display module, especially when adding pressure. First, turn off the device. Then [open it up](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl) (video guide), laying it flat like this: # Removing the PCB Next, use a small Phillips screwdriver to remove the 3x screws on your PCB. A flat-head also works, but you'll need to be careful to avoid stripping. After removing all 3 screws, slowly lift the PCB board and detach the battery by pulling straight outward on both wires (red/black). # Changing the battery *If you are changing your battery*, be mindful that there is adhesive holding it in place, so carefully pry it up slowly using a flat, rounded-edge tool (puncturing the battery is hazardous!). Then, continue to [reattach the wires once replaced and rescrew the PCB to the case](#h_53502e31e5). # Disconnecting the PCB and the Screen We're almost done. Use a fingernail or screwdriver to gently "open" the black gate, located along the back edge of where the ribbon cable attaches to your board. The gate, when closed, is parallel to the board. This will allow the ribbon to slide out of the board with almost no effort. Notice that the open gate is now perpendicular to the board. Here's another angle. You may now replace the board or the screen. # Replacing the Screen If replacing the screen (EPD), you want to flex the **case** out of the way, not the screen, to remove it from its held position. The EPD should never have pressure placed on it at a single point, or it will cause it to break. Imagine a cupcake or loaf of bread in a pan; you don't want to squish it, so you adjust the *container* (the case), not the *object* (screen). # Reattaching the Screen to the PCB When reattaching the screen to the new or old PCB, you need to insert the ribbon cable again with the black gate open (perpendicular). When inserting the ribbon cable into your new PCB, be sure **not to insert it diagonally**. ​ Here's another example, with the board inserted at a diagonal angle. **This will not function.** Insert the ribbon cable **straight**, but note that the gold section of the ribbon cable will *not* be completely covered by the white/black connector. Seeing a narrow, even-width section of exposed gold pins is the expected installation. You can now lower the black gate to lock the cable in place. # Reattaching the Battery Lower the board back into place, re-attaching the battery. The red wire should be on the side of the USB-C cable. Finally, add back the 3x screws. We recommend starting with this one first (farthest from battery connector), as it will help "seat" the PCB into the tight tolerances of the enclosure. [Close your case](https://help.trmnl.com/en/articles/10003236-how-to-assemble-your-trmnl) (video guide) to complete this procedure. Turn the device back on, then re-pair WiFi and [add the device to your account](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device) as if it were brand new. --- # Plugin Composer The Composer is used to design screens for the [Tidbyt](https://help.trmnl.com/en/articles/12430929-tidbyt). You can design all varieties of Private Plugin, including recipes and forks. Behind the scenes, compositions are saved as JSON documents. They can be imported, exported, and forked along with the standard e-ink Liquid markup templates. You don't need to create a new private plugins to design for Tidbyt. Your existing private plugins with Liquid markup will also support this new screen format. # Access the Composer 1. Switch the current device to a [Tidbyt](https://help.trmnl.com/en/articles/12430929-tidbyt) that has been previously set up 2. Create or edit an existing [Private Plugin](https://help.trmnl.com/en/articles/9510536-private-plugins) 3. Click **Edit Composition** # Interface Tour 1. Use standard graphic design tools to draw on image layers 2. Manage multiple text and image layers 3. Plugin variables, which can be interpolated into the layer content by using [Liquid](https://help.trmnl.com/en/articles/10671186-liquid-101) 1. Click on a variable to automatically insert it into the content 4. Control layer visibility, lock, name, and deletion 5. Render the word "true" in this Liquid template to conditionally show this layer based on variables. Any other value will hide the layer. 6. Write text content with Liquid interpolation 7. Instantly preview the design on your device --- # Robinhood Connecting your Robinhood account to TRMNL takes just a few seconds and is done securely via our OAuth2 integration with Plaid, a leading financial institution connector. # Step 1 - Visit the Robinhood Plugin Inside TRMNL, navigate to Plugins > Robinhood. # Step 2 - Connect Robinhood Click to "Connect with Robinhood" on the right side of the screen, then click the Robinhood logo and follow the prompts to approve access to TRMNL. You'll be asked to choose a particular account within Robinhood, for example your stock portfolio or Crypto portfolio. After selecting an account (just 1 please), the Plaid modal will confirm the permissions TRMNL is requesting. They are the `investments` and `assets` products, which you can learn more about [here](https://plaid.com/docs/api/products/investments/) and [here](https://plaid.com/docs/api/products/assets/). # Step 3 - Configure Plugin After selecting the final "Allow" option in Step 2, you'll need to finish setting up your plugin inside TRMNL. Start by giving it a name and refresh rate. Next, determine if you want to see a chart of your portfolio value over time, or simply a large number with your current portfolio value. The "Portfolio Value" type is a number, and the "Value Chart" type is a line graph, similar to [this](https://trmnl.com/plugins/robinhood_portfolio) example. # Step 4 - You're Done! Based on your device's refresh settings, you'll begin seeing Google Calendar very soon. Stay focused. # Troubleshooting If you'd like to disconnect Robinhood from TRMNL, first delete the plugin via the top right trash can icon when viewing the plugin configuration. Next, we suggest making a free Plaid.com account with your email or phone number. This will automatically look up and link all of your Plaid-driven financial connections, such as TRMNL. Simply click the TRMNL connection from the list in your Plaid dashboard, then select that you want to stop sharing information and click Continue. TRMNL will no longer have any access to your Robinhood account (via Plaid), but you can reconnect anytime by following Steps 1-2 above. --- # Tidbyt Tidbyt is an LED display with a common ethos - see what you care about. Following [their acquisition](https://modal.com/blog/tidbyt-is-joining-modal) in November 2024 however, development was paused indefinitely and Tidbyt devices are slowly going offline. Since then, several Tidbyt fans joined the TRMNL community and we've developed a new way to design TRMNL plugins to breathe new life to Tidbyt devices. 1. [reddit announcement](https://www.reddit.com/r/TIDBYT/comments/1lp3d2j/tidbyt_is_coming_to_trmnl/), July 2025 2. [progress report #1](https://www.reddit.com/r/TIDBYT/comments/1m6yfus/progress_report_tidbyt_trmnl/), July 2025 3. [progress report #2](https://www.reddit.com/r/TIDBYT/comments/1nr7e99/trmnl_tidbyt_progress_update_2/), September 2025 To stay updated on this development, consider joining the Beta waitlist: # Getting Started 1. Purchase a [one-time BYOD license](https://shop.trmnl.com/products/byod). ​ 2. [Claim the BYOD license](https://trmnl.com/claim-a-device) to add it to your new or existing TRMNL account. ​ 3. After logging in to your TRMNL account, go to device settings and change the model to **Tidbyt (64x32)**. ​ Open the Tidbyt app on your phone and go to **Settings** -> **Developer** -> **Get API Key**. Copy both the **Device ID** and **Key** to your TRMNL account. 4. Browse for [plugins](https://trmnl.com/plugins), or design your own! # Designing Your Own Plugin The [TRMNL Framework](https://trmnl.com/framework) was designed for high-resolution e-ink screens, but it's not a good fit for small LED displays. That's why we designed the Composer, a drag-and-drop interface for building pixel-perfect Tidbyt screens. [Learn how to use the Composer](https://help.trmnl.com/en/articles/12881005-plugin-composer). # Limitations The following features are not supported by Tidbyt: 1. Framework HTML/CSS/JS -> use the Composer instead 2. Playlist item visibility and scheduling -> control from the Tidbyt app 3. Some plugins -> we are designing new plugins all the time # About Refresh Rates Tidbyt plugs into the wall, so it doesn't need to go to sleep to conserve battery power, and its Wi-Fi connection is always on. Updated screens are pushed directly to the device the moment they are generated, which means the Tidbyt device does not need to be configured for a particular refresh rate. For Tidbyt, the only refresh rate that matters is that of the plugins. As soon as a plugin is regenerated, its screen is immediately available on the device. --- # BYOD: Enter My MAC Address # Please match the requested format MAC addresses can have a variety of formats, but for TRMNL we use the following common format: **:**:**:**:**:** That is 6 pairs of hexadecimal digits, separated by colons, for instance: **28:37:2F:B0:FA:4D** That means a digit is represented as "0" to "9" like for decimal and as a letter of the alphabet from "A" to "F". Enter your MAC address for your BYOD device in your [device settings](https://trmnl.com/devices/current/edit) (top-right of trmnl.com), click the *gear* icon, then select the *Developer Perks* section. --- # FW1.6.2 troubleshooting A major firmware release was deployed on August 4, 2025 in the afternoon ET. Release notes are here: After upgrading, some TRMNL owners have reported the following issues. This guide helps resolve each one. # Device makes a high-pitched noise will refreshing This is a known issue and we may be able to tune it out a bit. Send us a live chat reporting "1.6.2 noise issue" and we'll send you an update after we take a deeper look. # Screen is faded, too bright, or washed out If your device is functional but the display quality has degraded, we likely need to tune our new render strategy to fit your screen's internal temperature. **Resolution**: visit your [device settings](https://trmnl.com/devices/current/firmware/edit) page > *Firmware* section, disable OTA updates, and enable Maximize Compatibility: Next visit the [web flasher](https://trmnl.com/flash) and follow instructions to flash FW1.6.5. This works similarly to our previous 1.5.12 stable release. You may not see any changes on your screen during the update. After the firmware update is complete, turn off your device for 15 seconds, then back on. If you still have issues, send us a live chat in the bottom right corner. # Device is non-responsive If your device appears to have begun (or finished) an automatic firmware update, but the screen stops changing, it may have become overheated or statically charged. **Resolution A**: turn off your device for 10-15 minutes, then turn it back on. Yes really, this has made everything good again for some users. There may be a static charge that needs to dissipate. It won't hurt to wait longer either. **Resolution B**: if you have a USB-C cable with data transfer capabilities (not just charging - the TRMNL cable works here), visit your [device settings](https://trmnl.com/devices/current/firmware/edit), *Firmware* section, and disable OTA Updates. Next, visit and follow the steps to roll back to FW 1.5.12. **Resolution C**: *(this method is no longer recommended and is left here to avoid confusion)* **Resolution D**: visit your [device settings](https://trmnl.com/devices/current/firmware/edit) page, *Firmware* section, disable OTA updates, and enable Maximize Compatibility: Next visit the [web flasher](https://trmnl.com/flash) and follow instructions to flash FW1.6.5. This works similarly to our previous 1.5.12 stable release. You may not see any changes on your screen during the update. After the firmware update is complete, turn off your device for 15 seconds, then back on. If you still have issues, send us a live chat in the bottom right corner. --- # Preloading and Maintaining Managed Devices for Individuals TRMNL devices can remember multiple WiFi access points, making it easy to setup at home and then bring to the office; just add the new network and you're ready to go! This makes it easy to provision multiple devices under a single account and then distribute those devices to other individuals, like employees or customers. Additionally, with the help of our [mirroring](https://help.trmnl.com/en/articles/10530871-mirroring-a-device) feature, you can make a playlist of plugins on a single **parent** device and quickly replicate it on any number of **child** devices. Let's get started. # Connect Your Parent Device to Your Account Using out [setup guide](https://help.trmnl.com/en/articles/9416306-how-to-set-up-a-new-device), get your **Parent** device connected. For simplicity, you should rename the device in [device settings](https://trmnl.com/devices/current/name/edit) to **Parent**. Since you're there, this is also a good time to make the **Parent** device sharable in the device settings as well: Make note of the **Parent**'s Friendly ID, seen at the top of device settings. You will need to enter this value later in each **child** device. # Create Your Playlist Get the device ready for others! Add any plugins you need; don't worry, if you need to login to a 3rd party via the plugin, the data will flow freely to your **child** device without having to log in again. It's also important to be specific when setting the [schedule for the plugins](https://help.trmnl.com/en/articles/11663305-playlist-scheduler) in the playlist, including days/times and the refresh interval. This is because any refresh rate *not* set in a plugin's schedule on your playlist will use the device's default rate, and that's just one more thing you'd need to check when setting up the **child** devices later; let's not make this harder on you! # Add Your First Child Device Go through the normal setup procedure, using the "Add new Device" option in the top-right to add the **child** device to your account via its Friendly ID. Once added, head into *device settings* (gear icon next to the device in the top-right). You can name the device **Child #** for simplicity, but we would recommend you include the name of the individual who will receive that device, such as **Child Johnson**. Using a label maker to mark the back of the unit with this name will also save you extra steps in the future if it ever needs to be reallocated. Depending on your use case, you may want to make adjustments to the *Low Battery Notification*, *Sleep Mode*, and *Sleep Screen*. Note that the low battery notifications will be delivered **to you**, so even if the need to recharge the device is infrequent, it will eventually be needed, and thanks to your device naming previously, you know who to forward the message to! Finally, near the bottom of the *device settings*, you want to enter the Friendly ID of the **Parent** device in the *Mirror another Device* section and click ***Mirror***. # Verify Your Playlist You can now go to [playlists](https://trmnl.com/playlists), select the **child** device in the top-right, and you should see a perfect *mirror* of the playlist from the parent! If everything looks good, there is one final step for any **child** device before its ready to pass along. If you wish to remove some items from the playlist for *this particular **child** device*, you can do so now. For instance, if your **Parent** playlist contains plugins that benefit multiple roles, you can remove the playlist items that do not match (*e.g.,* developer vs sales associate). # Reset WiFi (Reactivate Captive Portal) on Child Device On the **child** device, hold the button on the back of the device for 6-9 seconds. When released, the device will flash a bit as it switches back into *captive portal* mode, displaying the message you first saw during setup. Once that screen is visible, turn the device OFF using the power switch on the back. It is now ready to go to its new user! Because the last image on the screen will persist, its important to tell the recipient to turn the device ON using the slider on the back before following the instructions, otherwise they may get frustrated because the TRMNL network isn't broadcasting when the device is off, even if the screen looks exactly the same! # Rinse and Repeat You can now [repeat the process](#h_b2686bb685) for all additional **child** devices. # Updating the Parent and Child Device Playlists In the future, if you make changes to the playlist of the **Parent** device, you can re-sync *each* **child** device so it has the new playlist! To do so, use the *device settings* of *each* **child** device and the *Sync Screens* button. Yes, it would be easier if you could push the sync from the **Parent** device once, but since devices that are mirroring another device can have their playlist altered, as mentioned previously, we designed it as a *pull* system. Note that you will need to re-edit the playlist after sync if you did so [previously](#h_08eea1a6fc). --- # Registering your MAC address If your device displays "MAC not registered" and points you to this help page, you've purchased hardware from someone *other* than TRMNL. You will need to purchase a [BYOD license](https://shop.trmnl.com/products/byod) (Bring Your Own Device) to use **trmnl.com** as your server for your BYOD device. You are also welcome to run your own server ([BYOS](https://docs.trmnl.com/go/diy/byos)), no license required. **If this is your first device to connect to useTRMNL:** You can now [claim your device](https://trmnl.com/claim-a-device) using your order number and name. Then [sign up](https://trmnl.com/signup) for an account. **Once you have a trmnl.com account:** With a BYOD license you can easily add your MAC address yourself. Hover over your BYOD device in the top-right of trmnl.com and click on the *[gear](https://trmnl.com/devices/current/developer/edit)* icon, selecting the *Developer Perks* section. Enter your MAC address for your device and **save**. Find more information here: [https://docs.trmnl.com/go/diy/byod#build-from-scratch-advanced](<>) If you have further questions about using trmnl.com for your third-party hardware, send us a Live Chat or email . --- # Hardware specs for Custom authorities Most customers receive their TRMNL device without issue, but some are asked to provide more details about our hardware or licenses. **Antenna specification** **Channels/Frequencies** - - (page 18) Additional information is available in public FCC reports: **RoHS** [https://drive.google.com/file/d/1ogqznpgQzIVMditqznDL1fsit3Ct9fq7/view?usp=sharing](<>) **CE** **ISED-8428** **TELEC** **HTS** | HSCODE | 85285200 | | --- | --- | | UPC | 749429512 | | COO | USA | [Fire Rating Info](https://help.trmnl.com/en/articles/12511853-flammability-ratings-for-devices) --- # Withings Setting up a Withings dashboard on your TRMNL takes just a few seconds. ## Step 1 - Visit the Withings Plugin From the plugins directory, navigate to Withings. Click "Connect with Withings" on the right side. ## Step 2 - Connect Plugin Your browser will redirect to the Withings website where you'll be asked to log in and approve the TRMNL integration. Click "Allow this app" to be redirected back to TRMNL. Note that TRMNL does not request any permissions to "edit" your data and you may revoke this connection at any time. ## Step 3 - Configure Plugin Back inside TRMNL, select a time period for data visualization as well as a preferred metric unit. If you have a Withings wearable, for example the Steel HR, you can enable Activity Metrics. This will showcase step + sleep data versus weight data. Optionally give this connection a name on the right side, which will be visible in the title bar if you have that enabled. ## Step 4 - You're Done! Within a few moments, TRMNL will begin pulling health and weight data collected by your Withings device. The activity mode layout looks like this: To request additional data or device models, create a [pull request here](https://github.com/usetrmnl/plugins/blob/master/lib/withings/withings.rb#L52) or email . # Removing the connection To revoke your TRMNL <> Withings connection, visit . --- # QA Pass or Fail Message # Example FAIL image: # Example PASS image: # Why This Appears Due to a bug in **v1.7.1 firmware**, this message will accidentally appear on some devices. This should not happen and does not reflect an accurate device evaluation. There are two solutions: 1. Downgrade to firmware v1.6.9 and disable OTA [Over-The-Air] Updates in `Device Settings > Firmware` 2. Upgrade to v1.7.2+ (once available) [How to Flash Your Device](https://help.trmnl.com/en/articles/10271569-manually-flash-firmware) --- # Tuning a washed out display The [no flicker + grayscale](https://trmnl.com/blog/no-more-flicker) feature release exposed underlying issues with some customer displays. This was resolved by our factory for upcoming components, but previously affected customers may experience occasional washed out images: This guide provides a quick method to adjust your display's tuning and resolve the washed out effect without new hardware. An alternative to this guide is [enabling Maximize Compatibility](https://help.trmnl.com/en/articles/11934315-fw1-6-2-troubleshooting). # Step 1 - Flash the newest firmware release Visit [trmnl.com/flash](https://trmnl.com/flash) and select your device, then the latest firmware (1.7.x or higher), following instructions to add this build to your device. **Note**: the "temp profile" feature is currently only available for TRMNL OG devices. # Step 2 - Find profiles in your device settings Go to your Device settings, click "Firmware" and toggle between multiple profiles. # Step 3 - Test each profile We suggest first trying Profile A, then B, and so on. You may want to take a photo of each result under good lighting conditions, preferably with your exposure settings locked. To test a profile: 1. Select and save the profile from your Device settings 2. Install our [Temperature Profile Test Image plugin](https://trmnl.com/recipes/211135) and add it to your playlist multiple times. Drag and drop them in your Playlist to be the "next" items. Do this by ordering it after the row with a green "Displayed now" badge (example below) 3. Click the button on the back of your device to force-skip to the next screen 4. Observe results Repeat Steps 1-4 for each profile until you find one that looks best on your device. Send us a live chat to share before/after photos, which we'll use to generate additional profiles. --- # Find your Friendly ID A Friendly ID is a unique code that identifies your device. Think of it as your TRMNL's personal name. It's what connects your hardware to your account, your playlists, and your plugins. Please don’t share it with others, as it uniquely links to your device. Why do we call it "friendly"? Because it's made for humans. Instead of a long string of numbers or a hard-to-read MAC address, the Friendly ID is simple and recognizable. Depending on what device you have, there are different ways to get to your Friendly ID: **If you have a TRMNL DIY kit, an e1001/1002 by Seed or any other BYOD device:** You will need to buy a [BYOD license](https://shop.trmnl.com/products/byod) to use **trmnl.com** as your server for your device. After purchase, [claim your device](https://trmnl.com/claim-a-device) by entering your TRMNL order number for the BYOD license. This will give you the device's *Friendly ID*. Afterwards, follow the steps on [this page](https://docs.trmnl.com/go/diy/byod#build-from-scratch-advanced) in section "OSS + closed source approach". # If you’ve ordered the Developer Edition or Clarity Kit You can start exploring before your device even arrives: create playlists, select plugins, and more. Just go to [Claim a device](https://trmnl.com/claim-a-device) and enter your TRMNL order number along with either your email address or your first name. This will generate your Friendly ID, which you can then use to sign up [here](https://trmnl.com/signup). # If you already have a TRMNL device (shipped by us, no BYOD) Once our own hardware connects to you Wi-Fi, it will display your Friendly ID directly on the screen. # Alternatively If you've unpaired your device, visit [Find my device](https://trmnl.com/find-my-device). Enter your device’s MAC address or serial number, and you’ll get your Friendly ID right away. And if you've lost your Friendly ID, just reach out to so we can get you started. --- # Volume Pricing Groups in need of multiple devices can save money with a bulk order. Instead of predatory quoting based on perceived budgets, our rates are published below to demonstrate a TRMNL core value: transparency. # Use cases *Corporate gifting / customer rewards program* **Give away** **TRMNLs** without any logistics hassle via our [Partners API](https://docs.trmnl.com/go/partners-api/introduction). *Wholesalers / retailers* **Resell TRMNLs** to your own customers, online or in person. *In-house distribution* **Equip your organization with TRMNLs** for personal/work reasons or infrastructure purposes like signage and meeting room companions. # Pricing All devices include an individually wrapped box. To build your quote, find the [retail price](https://shop.trmnl.com/collections/devices) of the devices you want and then follow the values below. Models may also be combined, so for example 5x OG and 5x Model X devices would qualify for a 10x+ device discount. **Gifting (via Partners API)** - $20 below retail price, paid up front in batches of 25+ quantity - $10 below retail, invoiced weekly as devices are provisioned - retail price, paid on-demand with no minimum quantity - at-cost shipping ($7 /package USA, $12 /package international) **Wholesale** - MOQ 150x devices, $92 unit price - MAP $20 below retail - Free shipping to any single location **Group discount** - 10-20x devices @ $20 below retail - 21-50x devices @ $30 below retail - 51-99x devices @ $35 below retail - 100-150x devices @ $37 below retail - 151+ devices @ $40 below retail - EDU or non-profit? Deduct another 10% (+ we accept tax-exempt forms) - Free shipping to any single location or at-cost shipping ($7 /package USA, $12 /package international) **Add-ons** (any use case) - Charging Cable @ $2 per unit (retails for $5) - Battery upgrade @ $2.50 per unit (retails for $10) - Developer Edition upgrade @ $5 per unit (retails for $20) # Customization - Custom graphics on boot screen and loading icon @ $5 per unit - Gift message on packing slip @ $2 per unit # Next steps To generate an invoice, email your requirements (color, quantity, add-ons) from above to . To discuss our (open source) self-hosted solutions, email . --- # Home Assistant Integration Native support: [Home Assistant Screenshot](https://help.trmnl.com/en/articles/13281388-home-assistant-screenshot) Also see community plugins: And these open source projects: - - [https://github.com/gitstua/trmnl-plugin-dev/tree/main/home-assistant-trmnl](https://github.com/gitstua/trmnl-plugin-dev/tree/main/home-assistant-trmnl#home-assistant-trmnl-plugin) - - Finally, if you prefer to roll your own stack, combine the Home Assistant founder's "puppet" library with our Alias plugin: - - --- # Playlist Groups (Legacy) If you're building your first playlist and thinking, "I want my calendar in the morning, but reddit in the afternoon," you're not alone. For this use case we have Playlist Groups. **Note**: documentation below is for our **legacy** **Playlists functionality**. # What is a Playlist? Your [Playlists tab](https://trmnl.com/playlists) is a collection of content being displayed on your device. You can drag/drop items to rearrange the ordering, or build [Mashups](https://help.trmnl.com/en/articles/10168132-mashups) to combine multiple screens. # What is a Playlist Group? At the top of your Playlists tab are some action items: The Add a Group button creates a "bucket" of playlist items, known as a Playlist Group, like so: Each Playlist Group has a few properties: - **name** ("Morning Routine" in the example above) - **display from** (00:00, so midnight above) - **display to** (11:00 above) - **every** (a minute interval, for example 5 or 30 minutes) Once you click to Add a Group at the top, you can begin adding plugins to it. To add plugins, either drag/drop an existing Playlist item from this page, or click "Add a Plugin" at the top, and arrange the new items as you wish. You can rename a Playlist Group by clicking the name in the top left, which will default to "My Playlist." Just click those words, and you can edit inline. # Multiple Playlist Groups Here is what it looks like when your TRMNL Playlist has multiple groups: --- # Netatmo Weather Station Connecting your Netatmo Smart Home Weather Station to TRMNL takes just a few seconds. # What You'll Need - A Netatmo Weather Station (base station + outdoor module) - A Netatmo account with your station registered - Optional: Rain gauge module (NAModule3) for precipitation data - Optional: Wind gauge module (NAModule2) for wind data # Step 1: Add the Plugin Navigate to your and click Add on the Netatmo Weather Station plugin. # Step 2: Connect Your Netatmo Account Click Connect with Netatmo to authorize TRMNL to access your weather station data. You'll be redirected to Netatmo's login page where you'll grant TRMNL read-only access to your station data. The plugin requires only the read_station permission scope. TRMNL cannot modify your Netatmo settings. # Step 3: Configure your plugin After authorization, select which weather station to display from the dropdown menu. If you have multiple Netatmo stations, each will appear by name. # Step 4: Set the Refresh Rate The plugin refreshes once every day by default. You can adjust how often your screen updates in the or configure your own refresh rate in the plugin settings. # Step 5: Save Click Save to complete setup. Your Netatmo weather data will appear on your TRMNL display when your device next requests data, which usually happens from 30 minutes to an hour from now. # Optional Rain and Wind Measurement If your Netatmo Weather Station includes the rain or wind modules, TRMNL will intelligently display them along with the rest of the data. ## Without Rain and Wind Modules ## With Rain and Wind Modules # Graph Mode The Netatmo plugin also has a "graph mode" option where a graph of a metric of your choosing can take up the full space available. To enable graph mode, simply set the Display Mode to "graph" and select the metric you want to see (temperature is default). --- # Parsing plugins with the Sandbox Runtime [Private plugins](https://help.trmnl.com/en/articles/9510536-private-plugins) enforce a maximum 100 kilobyte blob of data from external resources. Endpoints that respond with a larger payload will be rejected. But it's not always possible to reduce a payload with request parameters alone. Introducing the **transformer sandbox runtime**. # Example use case Create a private plugin that sometimes yields a large JSON object. As is, this plugin will soon go into a degraded state when the response is too large (> 100 kb). Visit the Markup Editor > Transform tab and filter the payload with JavaScript (Node v22), returning a new object. Wrap your logic with the `transform()` function like so: function transform(input) { return { // parsed payload goes here } } The `input` variable contains the complete original payload. Trigger a force refresh from the plugin settings screen, then find your new payload object back inside the Markup Editor. # Benefits of using this sandbox In addition to making otherwise disallowed plugin endpoints possible, the `transformer()` strategy can enhance your developer experience overall. ## Cleaner code If your plugin needs just a few bits of data but you find yourself traversing deeply nested nodes to find it, surfacing those values as top level JSON objects can simplify your plugin's markup. ## Deeper functionality TRMNL is committed to making [Liquid templating](https://help.trmnl.com/en/articles/10671186-liquid-101) a formidable tool in plugin development. But let's not pretend JavaScript isn't a lot more powerful. As plugins become more advanced we hope to reduce the learning curve with data parsing inside this sandbox versus alongside your markup. # Usage notes - The Node.js runtime lives inside an [isolated-vm](https://github.com/laverdet/isolated-vm). - Execution timeout is capped at 1 second. - Internet access is disallowed -- sorry, no Underscore.js. # Troubleshooting *How to access other variables, e.g. `###{{ trmnl }}` form field values?* The `trmnl` global variable node is available as `input.trmnl`. To leverage a custom form field value with keyname `genre`, for example: function transform(input) { let userFilter = input.trmnl.plugin_settings.custom_fields_values.genre; return { // parsed payload that leverages userFilter goes here } } *How can I debug my JavaScript?* We suggest retrieving a (large, realistic) payload independently of the TRMNL platform, then writing a parser separately. Enable [debug logs](https://help.trmnl.com/en/articles/11586187-debugging-private-plugins) for lightweight hints at what may be causing issues, for example a missing semi-colon. In the future we hope to expose more native JavaScript exceptions. # Usage examples ## Limiting the Size of an Array of Objects **Data** Inside *Your Variables* you have `###{{ actions }}` (seen below), a top-level key in the JSON you are *polling*; it is an array of objects. There are other top-level keys in the JSON as well, but we only want to *Transform* this one and leave the rest untouched. [ { actionId: 1000 ... }, { actionId: 1001 ... }, ... ] **Transform** - `data` is the name of the new object that will be present after the transform, aka `###{{ data }}`. - `...input` makes sure to include all other key:value pairs. - `input.actions.slice(0, 30)` returns only the first 30 entries in the array. function transform(input) { return { data: { ...input, actions: input.actions.slice(0, 30) } }; } ## Removing a Property **Data** We want to *Transform* by removing the top-level property `synonyms`. { id: 12345, word: "payload", definition: {...}, examples: [{...},{...},{...}], synonyms: [{...},{...},{...}], metadata: {...} } **Transform** - `data` is the name of the new object that will be present after the transform, aka `###{{ data }}`. - `synonyms` is extracted as its own variable, which is ignored. - `...newInput` variable contains the remaining properties. function transform(input) { const {synonyms, ...newInput} = input; return { data: newInput }; } # What's next? JavaScript is the *lingua franca* of web development. Still you may prefer Ruby, its little brother Python, or Java. TRMNL is open to extending this sandbox to more runtimes (languages) to make plugin development feel native to developers of all technical backgrounds. --- # What fonts are used in Framework? TRMNL is commissioning a custom font starting in late 2025 to further enhance readability and design for our displays. When released, this document will be updated. The current fonts are in our public fonts repository are part of our TRMNL [Design Framework](https://trmnl.com/framework/): - BlockKie - **NicoBold** (Regular) - **NicoClean** (Regular) - **NicoPups** (Regular) - dogicapixel - dogicapixelbold Our design [framework](https://trmnl.com/framework/) provides classes, components, and more that help you design amazing private plugins that are compatible with different views or layouts, screen sizes, and more. This is similar to frameworks you are familiar with, such as Tailwind or Bootstrap. --- # Plugin Not Receiving Data from Polling URL # Our Rendering Servers [By design](https://docs.trmnl.com/go/how-it-works), screens for the device are rendered via our web server workers, not the device itself. That means that the experience can differ between your browser and our servers in regards to accessing your plugin's polling URLs. Our servers are hosted on **Hetzner Cloud**. While we try and be good internet citizens when making requests to your endpoints, some web servers may blacklist entire domains or IP blocks, causing the request to fail, leaving the **Your Variables** section of your plugin returning only the internal `###{{ trmnl }}` variable. If you control the server, whitelisting our IPs can help. Worker IP Addresses as of last update to this document (subject to change): IPv4 - 157.230.59.36 - 159.223.182.51 - 176.9.106.45 - 178.63.44.53 - 78.46.67.98 - 88.198.48.93 IPv6 - 2a01:4f8:151:5339::/64 - 2a01:4f8:120:52b6::/64 - 2a01:4f8:120:53b5::/64 - 2a01:4f8:221:439b::/64 Programmatically: GET `https://trmnl.com/api/ips` # Debug Logging *Note: This is a feature available only to **developer license** holders working on **private plugins**. If you're reading this, you are building your own plugins, so you already have that license.* When viewing a plugin's settings page, you can enable **Debug Logging** for 24 hours, which enables more detailed information about each request. Once clicked, the link will change to your account's debug log link, which updates automatically in real-time. Clicking *Force Refresh* will trigger a refresh, and the debug log will populate with additional information. **Note:** This log does not always show errors related to requests, for instance, if the server responds with a valid 200 response and an unformatted "Access Denied" message. *It's one more tool in your troubleshooting arsenal.* # Testing for Blocking Issues Like all troubleshooting, you need to isolate and replicate. If you have your own web server and programming skills, you can create a simple *proxy script* that polls the URL and then outputs the results in the same format (*e.g.,* JSON) when viewed in the browser. You could also try using a third party website, like [reqbin.com](https://reqbin.com/) to see if they can return data from the URL, or if they are blocked as well. **In either case, the goal is to determine that the request *can* or *cannot* work from server-to-server, rather than browser-to-server (or local postman-to-server).** The next steps will vary depending on your testing results. # Adding a Reliable Polling URL Since TRMNL supports [multiple polling URLs](https://help.trmnl.com/en/articles/12385769-missing-data-in-multiple-polling-urls), you can add a reliable URL that always returns randomized (fresh) data. This is a way to confirm that the data request is working properly for your plugin, generally. URL: `https://trmnl.com/custom_plugin_example_data.json` # Cloudflare Configuration If the polling URL is a server you control and that server is being protected by Cloudflare, your configuration may be generating a challenge for our server's worker browsers which is being captured, before your actual site is returned. To test this, you can disable Cloudflare protections for your endpoint (broadly, or using a specific authentication string you designate) and retry refreshing the plugin data. *Configuring Cloudflare is not part of the scope of this article.* # Cloudflare IP Access Rule *This tip was submitted by a member of the community.* Turns out that regular IP whitelisting from cloudflare's "Custom Rules" **WILL NOT** bypass bot traffic protection (aka "bot fight mode"). After going down some [rabbit holes](https://community.cloudflare.com/t/allowlist-ip-address-before-traffic-blocked-by-bot-fight-mode/838578/2) there are basically two options: 1. You can pay for "Super Bot Fight Mode" 2. Use a hidden URL to access a special *IP Access Rule* override that you can't see from the default URL: --- # Syncing Plugins with GitHub **Requirements:** - [TRMNLP](https://github.com/usetrmnl/trmnlp) - [User API Key](https://trmnl.com/account) **Guide:** Credit to `eana` on Discord for creating this guide and workflows. --- # Sound During Screen Refresh *These sounds may be related to the fast refresh update in August 2025, and may improve over time as we continue to tweak our device firmware.* ***Option 1:*** Fully charge your device (plug it in until the green light goes off, approximately 5 hours). This has resolved the issue for many customers. ***Option 2:*** *Adjust the temperature profiles.* **Requires firmware v1.7.1** Access your device's settings in the top-right, click on the gear icon for your device, then select **[Firmware](https://trmnl.com/devices/current/firmware/edit)**, and try the different Temperature Profile settings for a day to see if it resolves **and** does not degrade the visual functionality of the display. Let us know if one profile solved the issue for you (email [support@trmnl.com](mailto:support@trmnl.com?subject=Sound%20Fix%20Temperature%20Profile)). --- The following options will disable *fast refresh* and *2-bit* support from your device. ***Option 3:*** Enable **Maximum Compatibility** mode from your [device settings](https://trmnl.com/devices/current/firmware/edit), *Firmware* section. ***Option 4:*** Returning your device to version 1.5.12 firmware via our online [web flashing tool](https://trmnl.com/flash) can further diminish or remove the sound. *While both options 3 and 4 roll back our advanced fast refresh feature, they maintain the quality and features you were promised upon purchasing your device.* --- # Seeed Studio Devices: Xiao, DIY Kit, and reTerminal # Hardware Troubleshooting If you purchased a device from Seeed Studio and are experiencing issues getting it up and running, please see the following Wiki pages provided by Seeed Studio or reach out to their support to troubleshoot hardware problems: - [XIAO 7.5" ePaper Panel](https://wiki.seeedstudio.com/xiao_075inch_epaper_panel/) - [Seed Studio 7.5" DIY Kit](https://wiki.seeedstudio.com/trmnl_7inch5_diy_kit_main_page/) - [reTerminal E Series](https://wiki.seeedstudio.com/reterminal_e10xx_main_page/) (E1001, E1002) Our help articles are designed for the TRMNL hardware, so they may refer to buttons or switches that differ from Seeed Studio devices. Please refer to the Wiki documentation above for hardware button functionality. ## Green LED Blinking A green LED blinking means you haven't connected the battery. If green LED is on, it means the battery is charging. If it's not on, it means it's fully charged. ## Enable Pairing Mode to add a new Wi-Fi network (Network Reset) - On XIAO 7.5" ePaper Panel: press the Reset button, release it, then press and hold the Boot button for 5 seconds - On Seeed Studio 7.5 DIY Kit: press and hold Key 3 for about 5 seconds - On reTerminal E Series (E1001, E1002): press and hold both navigation buttons (left and right) simultaneously for 2 seconds ## Can not discover device on my network *This is a tip provided by a customer.* With the battery attached, the Seed Studio (OG) DIY Kit goes to sleep before it can be found on my router. What I ended up doing was disconnecting the battery and finally erasing the flash memory. # Using Seeed Studio Software Seeed Studio provides various software with their products, detailing on the product page where you made your purchase. This includes tools for Home Assistant. You do not need to connect to TRMNL to use these features. # Connecting to TRMNL (BYOD License) You will need to purchase a [BYOD license](https://shop.trmnl.com/products/byod) to use **trmnl.com** as your server for your Seeed device. After purchasing your license: 1. Create a BYOD device: 2. Visit your [device settings page](https://trmnl.com/devices/current/edit) to select your [Device Model](https://help.trmnl.com/en/articles/11547008-device-model-faq) and set your DIY device's **MAC address** in the *[Developer Perks](https://trmnl.com/devices/current/developer/edit)* section. When you first boot your Seeed device, it may display a message regarding **"Need to register your MAC Address."** *This should be completed following the steps above.* # Connecting to Your Server (BYOS) You are welcome to [Bring Your Own Server](https://docs.trmnl.com/go/diy/byos) (BYOS), no license required, for any TRMNL compatible device. If using Terminus, you can also refer to our [Terminus setup guide](https://help.trmnl.com/en/articles/12263392-connect-your-device-to-terminus-byos). # Contacting TRMNL Support Our team is happy to assist you with our platform or any issues getting your BYOD license properly connected to your trmnl.com account. Use the online chat or email [support@trmnl.com](mailto:support@trmnl.com?subject=Seeed%20Studio%20BYOD%20Assistance) for assistance. --- # Reusing Markup with Shared The **Shared** tab in the plugin builder does exactly what it says on the tin; any code inside is processed before any View, allowing you to keep logic and templates in one place, and keep layouts focused on the look of things. # DRY Code DRY (**D**on't **R**epeat **Y**ourself) helps keep things organized and reduce the chance of copy/paste mistakes when developing or improving your plugins. It's a best-practice to put [Liquid](https://help.trmnl.com/en/articles/10671186-liquid-101) logic in Shared if it's preprocessing Your Variables. If you put "A" in Shared and then "B" in your Full view, then plugin would display "AB". This is equivalent to copy/pasting all the Share content into each view (exhausting!). # Templates Shared doesn't have to only include logic, it can also include a **Template**, which you can pass key:value pairs to **render** content dynamically, while still limiting duplication. For more info, check out our [plugin docs](https://docs.trmnl.com/go/reusing-markup). TIP: When using a template, Your Variables are not automatically included, you have to pass them in. This is most commonly a "gotcha" when you need the `trmnl` variable's data, your **render** could look like: `{% render "my_template", trmnl: trmnl %}` # Don't leave views empty Our renderer will skip a view that does not contain any markup, so your plugin will display "Full view not available" in this case. If you are using all of your code from Shared, just add some commented code to the empty editor box to avoid seeing errors: --- # Refer & Earn We don't advertise on Facebook or Google, and we don't have any billboards. (We did [design one](https://x.com/useTRMNL/status/1868827604322861386), though.) Instead of giving money to marketers whose job is to make people click buttons, why not reward our community? # Introducing Refer & Earn With TRMNL you can create incentives for yourself and others including: - Give $10, get $10 - Give $15, get $5 - Give $5, get $15 Or even: - Give away our Developer edition perk ($20 value) Once created, this coupon will be available inside your account alongside a real-time redemption tracker so you know if your efforts are working: **If you prefer store credit instead of cash, that's fine too**. We'll top up your online store customer profile 1x per month with recent redemptions. # Apply for a referral code Refer & Earn affiliates are manually approved after confirming a few details: - You're an active TRMNL user (no affiliate marketers please) - You'll represent our brand with integrity (no spam posting on random websites) - You're comfortable answering questions about how the product works If this is you, email a) how you plan to use the code, and b) your preferred give/get incentive from the options above. # Using your referral code Nobody enjoys hard-to-apply coupon codes. Once you have yours, point people to our home page with a "ref" param like so: https://trmnl.com?ref= For example, if your coupon code is "billy10" it would be: https://trmnl.com?ref=billy10 If you've built a Recipe or Public Plugin, you may also append your code to those landing pages like so: https://trmnl.com/recipes/11914?ref= https://trmnl.com/integrations/apple-photos?ref= When your referral lands at checkout the code will be automatically applied: ​ Alternatively, simply point your audience to `https://trmnl.com` and tell them to "input coupon code `my-code` at Checkout," as depicted above. # Tips to share TRMNL We don't need expect anything fancy. Sharing TRMNL in your own words is ideal. But we will provide a few examples (via email) that have worked well for others. From sharing a picture of your device, to posting on X about a custom plugin you're building, or even replying to a comment on Hacker News, we've witnessed dozens of tiny shoutouts drive significant interest to our mission. If you're not a marketer (congrats!), no sweat. We'll help find opportunities to share your referral code so that everyone wins. We'll also share the 1-pager mentioned above with approved affiliates, so go ahead and [apply](https://trmnl.com/account). --- # Recipe best practices Thousands of private plugins have been created by TRMNL users, and 100s have been submitted to the public directory: Until May 2025 we manually reviewed each detail of every submission, from the markup to [custom form fields](https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder), spelling and grammar, and general usability. Then we created **Chef** ([demo](https://www.loom.com/share/e41edefd3c2643668b35e41d74d07020)), a Recipe linting utility. See tips below to make a Recipe that pleases her. And see the latest Chef implementation [here](https://gist.github.com/ryanckulp/fbe5f68c51db1ae214a97da24be4d62b). # Markup uses too many inline styles The following `style` declarations are unnecessary thanks to our Framework design system: display, justify-content, padding, margin, background-color, color, border-radius, text-align, object-fit, font-size For example: - `text-align` => - `background-color` => - `object-fit` => # Custom field has a link that can be optimized for usability Instead of pointing users to external locations like this: Live a little. Use HTML like so: - keyname: api_key name: API Key description: Get your API key from Server Settings. placeholder: asdf-fdsa field_type: string optional: true Which produces: Embedded links make description text shorter, open up in a new tab, and are more obviously clickable with the `underline` CSS class applied. **Is your link just an example, and should not be clickable?** Remove `https://` from the beginning to ignore this check. --- # Plugin in a Degraded State (Reset) If our servers have problems connecting to **polling** endpoints for plugins, we'll automatically retry again later, because sometimes websites temporarily become unavailable on the internet; however, if this happens too frequently, we will stop trying and provide a message on the plugin's settings page stating that refreshing has been stopped. That error message offers a button for resetting the plugin health once you've resolved the problem. Before you get to that state, such as if the issue was intermittent, you'll instead see a warning similar to this: Example reasons: - `invalid URL` - `unable to fetch data from URL`. # Resetting Plugin Health and Removing Warning Message If you've resolved the issues or they have resolved themselves, and the warning message does not clear itself automatically over time, you can manually clear the message using the following method. This is the format of the URL when viewing your plugin's settings. `https://trmnl.com/plugin_settings/###{{pluginId}}/edit` *Example:* `https://trmnl.com/plugin_settings/987654321/edit` Replace `edit` with `reset_health` to clear the warning message: `https://trmnl.com/plugin_settings/###{{pluginId}}/reset_health` *Example:* `https://trmnl.com/plugin_settings/987654321/reset_health` # Why Do You Degrade Plugins? TRMNL promises to provide screen rendering for devices for the life of the device, with a one-time fee ([BYOD](https://docs.trmnl.com/go/diy/byod)) or built-in to the cost of the TRMNL device purchase. **We don't require a subscription**. This means we must maintain a healthy plugin ecosystem, for the benefit of all users and sustainability of the platform itself. To do that, if a plugin is not working as expected, we **inform** users and **limit waste** by degrading the plugin and eventually disabling refreshing a plugin until it is resolved. --- # Customizing TRMNL with Tailor This article is a stub FAQ for using Tailor and will grow as questions arise. --- # Using the Asana plugin Our integration with Asana allows you to view tasks from your tasks list, organized by sections like "Do Today," "Do Later," or any custom sections you've created. ## Step 1: Create the Plugin Navigate to Plugins and search for "Asana." Click to create a new plugin instance. ## Step 2: Connect Your Account Click "Connect with Asana" on the right side of the screen. You'll be redirected to Asana's authorization page. Sign in to your Asana account (if not already signed in) and click "Allow" to grant TRMNL access to your workspaces and tasks. We only use this access to display your tasks on the TRMNL. ## Step 3: Select Your Workspace After authorization, you'll be returned to TRMNL. Select the workspace you want to pull tasks from using the dropdown menu. ## Step 4: Select Sections Choose which sections from your My Tasks to display. You can select multiple sections: - Full view: Up to 3 sections visible - Half & Quadrant views: Up to 2 sections visible ## Step 5: Configure Display Options - Show Project Name: Display which project each task belongs to (useful if tasks span multiple projects) - Show Completed Tasks: Include completed tasks in the display - Sort Tasks: Choose how tasks are ordered within each section: - Default: Asana's native order - Due Date: Earliest due dates first - Alphabetical: A to Z ## Step 6: Save and Activate Click Save to activate the plugin. Your tasks will appear on your next device refresh, or use Force Refresh to load immediately. ## What's Displayed? Each task shows: - Task name - Due date (if set) - Project name (if enabled) Tasks are grouped by section with section headers displayed when viewing multiple sections. But when only one section is displayed, there's no section header ## Limitations The Asana integration focuses on the My Tasks view. The following Asana features are not currently supported: - Project views (only My Tasks sections) - Team/shared task lists - Subtasks - Task comments or attachments - Goals, Portfolios, or Workload views --- # Shopify Connecting your Shopify store to TRMNL requires a bit of concentration and a few minutes of your time, but no coding skills are required. On January 1, 2026 the Shopify team made it more complicated to build private applications, so the tutorial below has been updated with better instructions to guide you through the process. For TRMNL to generate custom dashboards of your Shopify sales, we only need read only access to your last 60 days of Orders, nothing more. ## Step 1 - Visit the Shopify plugin Inside TRMNL, navigate to Plugins > Shopify. Before we can click Save on this screen we need to generate a custom Shopify app and login credentials. This is a one-time operation and you will not need to touch this custom app after initial setup. ## Step 2 - Create a Shopify App Inside your Shopify admin, open Settings at the bottom of your left side navigation. Click the Apps item on the new left side menu. Near the top right click "Develop apps." If you have not already created custom apps, you may need to accept the Terms of Use. This is a 2-page process, so click “Allow” on both screens. Otherwise, click "Build apps in Dev Dashboard" and then "Create app." Name your app, we suggest TRMNL Plugin, then click Create. Scroll down to **Access** and input `read_orders` in the Scopes text area. TRMNL needs access to your Orders scope in order to fetch recent sales data and generate screen content. This access is read only and may be revoked at any time. Now input `https://example.com` inside the Redirect URLs text area. This is dummy data but is important to help us install our app. Click "Release" at the bottom or top of this settings form to save your work. You can ignore the optional version and message fields that will be prompted in a popup. ## Step 3 - Install your Shopify App From your [Shopify Dev Dashboard](https://dev.shopify.com/dashboard) click your app's name from the list, then save your "Client ID" and "Secret" values using the clipboard icon. We'll need both of them in a moment. Next, copy the string of text below into your computer's notepad or text document, replacing the capitalized details. https://YOUR-STORE.myshopify.com/admin/oauth/authorize?client_id=XXX&client_secret=YYY&scopes=read_orders&redirect_uri=https://example.com Here are the values you'll be replacing: - `YOUR-STORE` => your MyShopify admin subdomain, for example `usetrmnl` is ours, because our store is accessible at `usetrmnl.myshopify.com` - `XXX` => the Client ID copied from the previous instruction in Step 3 - `YYY` => the Secret copied from the previous instruction in Step 3 Past this entire (updated) URL into a new browser tab and hit enter. You'll be prompted to select your Shopify store, then approve the permissions we just created in the **Scopes** section. After confirming the installation you'll land on a page like this: Copy the full URL from your web browser into a text note or document: https://example.com/?code=3547b34z22b8f3&hmac=66... We just need the `code` value, which is every character between `code=` and `&hmac`, therefore `3547b34z22b8f3` in the examlpe above. Your code value will be different and a bit longer. ## Step 4 - Generate a permanent access token We're almost done - it's time to combine all these random values of text into a permanent token that TRMNL can use to generate sales dashboards on your device. Here is just 1 more blob of text to customize... curl -X POST \ "https://YOUR-STORE.myshopify.com/admin/oauth/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=XXX" \ -d "client_secret=YYY" \ -d "code=CODE" Like before, replace these values with content we gathered previously: - `YOUR-STORE` => your MyShopify admin subdomain, for example `usetrmnl` is ours, because our store is accessible at `usetrmnl.myshopify.com` - `XXX` => the Client ID copied from the previous instruction in Step 3 - `YYY` => the Secret copied from the previous instruction in Step 3 - `CODE` => the "code=" parameter value we noted in the previous instruction, for example `3547b34z22b8f3` from our example After you update the text blog... - on a Mac computer, open your terminal by hitting cmd+space, then typing `terminal`. After it opens, copy/paste the entire `curl ...` command from above (with your changes already made) and hit enter - on a Windows computer, click the Start icon and then type `Command Prompt`. After it opens, copy/paste the entire `curl ...` command from above and hit enter Regardless of what kind of computer you use, this command should yield a message like this: ​ {"access_token":"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","scope":"read_orders"} ## Step 5 - Configure Plugin Back inside TRMNL > Plugins > Shopify, 1. paste your new `access_token` value from Step 4 inside the Access Token field 2. input your Shopify store's MyShopify subdomain Your access token begins with `shpat_` and is followed by several alphanumeric characters. Do not include any `"` quote marks. Your Store Web Address does not require a full "myshopify.com" URL, only your store's subdomain. For example, if your store is "usetrmnl.myshopify.com" then "usetrmnl" is your subdomain. Click Save to activate this plugin. ## Step 6 - You're Done! Based on your desired refresh settings, TRMNL will begin aggregating order stats from Shopify very soon. Stay focused. --- # The Office Before we begin, a huge thanks to [Akash Rajpurohit](https://akashrajpurohit.com/), creator of [The Office API](https://github.com/AkashRajpurohit/the-office-api), and [emilybr0](https://www.redbubble.com/people/emilybr0/shop), designer of the character avatars (which TRMNL purchased). We built this plugin to make everyone's work day just a bit brighter. ## **Step 1 - Visit the The Office plugin** Inside TRMNL, navigate to Plugins > The Office. Optionally adjust the name of this plugin instance, then click Save. ## **Step 2 - You're Done!** Based on your desired refresh settings, TRMNL will begin showing off funny quotes. [Try to] stay focused. --- # Flammability Ratings for Devices The soft-touch ABS plastic case of the TRMNL (OG) has a **UL94 HB** fire burn rate. ## California Building Codes [CBC] **UL723 (Flame Spread / Smoke Developed Index)**: Not Evaluated. **ASTM E84 (Surface Burn)**: Not Evaluated. --- # Understanding Color Palettes TRMNL e-ink screens show images in shades of gray. Originally, only black and white were supported, but firmware upgrades have unlocked the ability to display 4 shades on both first-party and BYOD hardware. TRMNL X will display 16 shades, and some BYOD screens even go up to 256 grays. Bigger palettes looks nicer, and are generally preferable. They allow the jaggy edges of fonts to be anti-aliased into smooth curves. More levels of information hierarchy can be represented. And of course, photos and screenshots look closer to their originals. These advantages come with some tradeoffs. Here are some things to consider when deciding what color palette to use. ## Smaller Palette (e.g. Black & White) - Pro: Smaller file size, which is quicker to download, prolonging battery life - Pro: Faster transitions between screens - Con: Simpler, less contrasty images ## Larger Palette (e.g. 4 grays, 16 grays) - Pro: More appealing images - Con: Larger file size, which takes longer to download, shortening battery life - Con: Slower, more noticeable transitions between screens ## Changing the Palette The preferred color palette can be adjusted per device, and also per playlist item for ultimate control. The change will take place the next time the plugin is refreshed. For example, you might set the device to B&W for the highest performance most of the time, but override it to 4 shades of gray so your family photo looks its best. To change the default color palette for the device, go to device settings and select "Color Palette". To change the specific color palette for a playlist item, go to the Playlist, click "Adjust Schedule", and select a new palette in the dialog. --- # Plugins::Base.process! -> StandardError: private_plugin Your private plugin isn't generating a screen, even when using *Force Refresh*. So, you troubleshoot by turning on **debug logs** for your plugin. Everything looks good, until after `Generating Screen Now` you see an error message! Here are two possible errors you may be seeing (with your plugin id): - `Plugins::Base.process! -> StandardError: private_plugin 179259 Failed to open TCP connection to 127.0.0.1:4446 (Connection refused - connect(2) for "127.0.0.1" port 4446)` - `Plugins::Base.process! -> StandardError: private_plugin 179259 Failed to decode response from marionette` ## What Does it Mean? Our screen rendering workers are unable to access a URL specified within your plugin markup, most commonly an image `src`. ## I Don't See a Problem in Markup Preview Two reasons: 1. Preview renders in your browser, not our worker boxes, and your browser is much more forgiving with issues than the headless browsers we use to render screens. 2. You may have an issue with data (*e.g.* Your Variables) that is being hidden from you because of `data-overflow` or simply being scrolled beyond the screen, like a prblem with item 15 in an RSS feed, while the preview can only fit the first 5. ## How Do I Fix It? First, find the problem. If you have dynamic URLs, such as from Liquid logic, replace them with a known-good URL placeholder, then *Force Refresh*. If the errors do not show up and the screen is properly rendered, you have pinpointed the source of the problem. Now the hard part, you have to figure out why and how to catch it. If using a Liquid loop, use the filters `offset` and `limit` to find exactly what iteration is causing a problem, then dig, dig, dig. Good luck! --- # Notion Connecting your Notion to TRMNL takes just a few seconds. ## **Step 1 - Visit the Notion Plugin** Inside TRMNL, navigate to Plugins > Notion. ## **Step 2 - Connect Notion** Click "Connect with Notion" on the right side, and you'll be guided to log into Notion and accept our permissions. TRMNL only requires "read" access, we will not update any of your Notion data, and you will have the ability to specify the content you want TRMNL to have access to on the next screen. ## **Step 3 - Configure Plugin** Both types of Notion content, Databases and Pages, are supported through the TRMNL plugin. ### **Databases** When displaying a Notion **"Database"**, you can configure how your database items appear in your **TRMNL**. - **Database**: Select your Notion database (e.g., "Customer Support Tickets", "Product Feedback") - **Item Limit**: Set how many database items to display (1-100, default: 20) - **Title Field**: Choose which property shows as the main title (default: "Name") - **Status Field**: Select a property to display as a prominent badge next to the title - **Labeled Properties**: Add comma-separated properties to show as styled badges - **Listed Properties**: Add comma-separated properties to display as text details - **Sort By**: Choose a property to sort by (or use "created_time"/"last_edited_time") - **Sort Direction**: Ascending or Descending - **Multi-Column Display**: Show items in 1 or 2 columns (full/half-horizontal views) - **Filter:** Allows [Notion custom database](https://developers.notion.com/reference/post-database-query-filter#the-filter-object) filters to be specified. #### Example for Support Database Database: Customer Support Tickets Title Field: Ticket Title Status Field: Priority Labeled Properties: Status, Assignee, Customer Tier Listed Properties: Description, Last Updated, Response Time Sort By: created_time Sort Direction: Descending Filter: (see filter example below) This would display support tickets with: - Ticket title as the main heading - Priority shown as a large badge (e.g., "High", "Medium", "Low") - Status, Assignee, and Customer Tier as smaller alternating-style badges - Description and other details listed below each ticket #### How to build the Notion filter object 1. Find the database column "type" as in the image. 2. [Find](https://developers.notion.com/reference/post-database-query-filter#select) the available fields for the column "type". 3. Optionally use dynamic timestamps `now` or `today` e.g - #{{today}} → Date only: "2025-01-15" in UTC timezone - #{{now}} → Full timestamp: "2025-01-15T10:00:00Z" - #{{today+7}} → Today + 7 days in UTC - #{{now*UTC-7}} → With timezone offset: "2025-01-15T03:00:00-07:00" - #{{now+7*UTC-10}} → Now + 7 days in UTC -10 timezone 4. Build the filter: { "property": "Author", "select": { "equals": "J. R. R. Tolkien" } } ### Page Display Type When displaying a Notion **"Page"**, you show the ordered content blocks available in a page - **Page**: Select your specific Notion page (e.g., "Integration Guide", "Support Team Handbook") - **Item Limit**: Maximum content items to fetch (1-100, default: 20) - **Image Height**: Set maximum height for images in pixels (default: 120) - **Multi-Column Display**: Choose 1 or 2 column layout ## Example for Intercom Documentation Page Page: Support Team Handbook Item Limit: 25 Image Height: 200 Multi-Column Display: 1 This would display: - The full content of your Intercom Integration Guide page - Images sized to maximum 200px height - Content in a single column for better readability - Up to 25 content blocks from the page ## Notion Block Support Details We support all Notion block types with the exceptions listed below. The plugin fetches and displays most content blocks including text, headings, lists, images, and more. - Nested Content - No nested children support - Only the top layer of the page is fetched - Toggle block contents - Children inside toggle blocks are not displayed - Unsupported Types - table_of_contents - table (including all table child blocks) - unsupported types by the Notion API - synced_block - breadcrumb - column_list - Links - Some links may not display properly, particularly links to other Notion pages (dependent on Notion API responses) - Numbered Lists - Numbers are not displayed in numbered lists due to API limitations. Numbered lists will appear as bullet points instead - People Mentions - All people mentions display as "@anonymous" due to API privacy restriction - Emojis - Emojis are stripped from text due to TRMNL platform limitations - This affects Notion's typically emoji-rich content #### Best Practices - Use simple, flat page structures for best results - Avoid use of tables and toggle blocks for content you need displayed - Consider using databases instead of complex page layouts when possible ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Notion content very soon. Stay focused. --- # TRMNL for Android Community member Hossain Khan maintains a couple Android apps, each serving a different purpose. ## TRMNL Buddy Available on Google Play, this makes it easy to manage TRMNL devices on the go. Download/Install: ## TRMNL Display Want to turn an Android tablet into a TRMNL device? Download/Install: ## Open Source If you're a developer and prefer to build these apps yourself, get started here: --- # Workflowy ## Step 1 - Get Your Workflowy API Token - Visit while logged into your Workflowy account - Generate or copy your API token (it will start with wfpak_) ## Step 2 - Configure the Plugin Navigate to Plugins > Workflowy in your TRMNL dashboard and configure the following settings: ### API Token (Required) Paste the API token you generated in Step 1. ### Root Item ID (Optional) By default, the plugin displays the items in your home flow. If you want to treat a different item as the root/homepage, this is the setting you want. 1. Open Workflowy 2. Navigate to the item you want to use as the root 3. Right-click and select "Inspect Element" (or use your browser's developer tools) 4. Look for the projectid attribute in the HTML 5. Copy the UUID value (it will be formatted like `6306832c-5c65-f006-f802-c1b6b05dfc79`) 6. Paste it into this field For more details about finding the item ID, see: ### Show Completed Items Choose whether to display items you've marked as complete in Workflowy: - No (default) - Only shows incomplete items - Yes - Shows both complete and incomplete items (completed items appear with a checkmark and strikethrough) ### Maximum Items Set how many items to display (1-25). Default is 10. Items are sorted by priority as set in Workflowy. Stay focused. --- # Fera A TRMNL dashboard of recent reviews takes just a few seconds to set up. ### Step 1 - Visit the Fera Plugin From the plugins directory, navigate to Fera. Click "Connect with Fera" on the right side. ### Step 2 - Connect Plugin Your browser will redirect to the Fera website where you'll be asked to log in and approve the TRMNL integration. Click "Authorize" to be redirected back to TRMNL. Note that TRMNL **does not** request any permissions to "edit" your data and you may revoke this connection at any time (see "Removing" section below). ## **Step 3 - Configure Plugin** Back inside TRMNL, select the types of reviews you'd like to see. More details about each option are available in [the Fera docs](https://developers.fera.ai/reference/list-reviews). Optionally give this Fera connection a name on the right side, which will be visible in the title bar if you have that enabled. ## Step 4 - You're Done! Within a few moments, TRMNL will begin pulling review data from your Fera account. ## **Removing the connection** If you want to break the TRMNL <> Fera connection, visit . --- # Lunch Money Connecting [Lunch Money](https://lunchmoney.app?fpr=trmnl36) (referral link) with TRMNL takes just a few seconds. ## **Step 1 - Visit the Lunch Money plugin** Inside TRMNL, navigate to Plugins > Lunch Money. ## **Step 2 - Connect Lunch Money** Log into your Lunch Money account and visit [Settings > Developers](https://my.lunchmoney.app/developers). Fil out the "New Access Token" Form with label TRMNL and optionally set your reason to "*I need it for a third-party app.*" This will grant you a long string of characters, which you'll only be able to see one time. Copy this to your clipboard. ## **Step 3 - Configure Plugin** Back inside TRMNL, paste your Lunch Money Access Token into the form beneath the preview image. You may also select which type of financials you'd like to visualize, for example Budgets vs Accounts. Here's an example of the Accounts layout, which incorporates negative values for liabilities and positive values for assets: Click Save. ## **Step 4 - You're Done!** Based on your desired refresh settings, TRMNL will begin aggregating financial metrics from your Lunch Money account very soon. Stay focused. --- # Is Bluetooth Available for TRMNL Devices? There is a Bluetooth module as part of the TRMNL hardware, but it is not in-use or supported by our [firmware](https://github.com/usetrmnl/trmnl-firmware). We welcome any hackers who want to contribute to enabling some functionality with the Bluetooth module; please submit a Pull Request (PR)! --- # Using the TickTick Plugin TickTick's Welcome List in **Task View**. Create a [new instance of the plugin](https://trmnl.com/plugin_settings/new?keyname=ticktick). Click to *Connect with TickTick*: Click *Allow* to approve TRMNL access to your account. Once connected, you will see the **Ticktick Project** list populated with your **Task Lists** from TickTick. The *Inbox* is the default place for unsorted tasks to go. Use it as a staging place to put down whatever comes to mind, then later you can move them to the appropriate list. Select the Project/List you want to display, adjust the plugin's name if you wish, then click *Save* in the top-right. You can wait a minute for the data to automatically load, but if you prefer, you can click *Force Refresh* to render your first screen. Adjust the refresh rate as appropriate for your needs and then head to [your playlist](https://trmnl.com/playlists) to customize when you want it to display! ## Supported TickTick Features Ticktick only exposes their **Task Lists** via their API, so while they have additional features: They [do not extend access](https://www.reddit.com/r/ticktick/comments/1ktgbup/trmnl_integration/) to Calendar, Eisenhower Matrix, Habit Tracker, Pomodoro, or Countdown to 3rd parties, like TRMNL. --- # Unexpected Events Showing in Calendar In recent years, spam (unwanted messages) has migrated from email to SMS to calendar events. *Image Credit: jewels256* While most calendar providers will remove or block these events automatically, some will *only suppress* the events/calendar, rather than removing them. This creates a problem when that calendar is shared with other services, like TRMNL, via an `.ics` file, the standard for sharing calendar information. **If the events are still present in the .ics file, they will show in your TRMNL calendar.** The clearest sign of this problem is if events appear that you do not recognize, especially if they are in a language foreign to you. To resolve this issue, you need to purge these events from your calendar at the source (Apple Calendar, Microsoft, Google, etc.). Once that is complete, you will be able to *Force Refresh* your calendar plugin to get the new, clean .ics file update immediately. --- # Timezone Troubleshooting with Calendars and Other Plugins ## Setting Your Account Timezone We try and automatically set this accurately when your account is created, but there is a lot of nuance in time zones, so you can edit it within your [Account Settings](https://trmnl.com/account): This informs plugins across the TRMNL plugin ecosystem about how to display time information to you. We even offer plugin developers a [guide on timezone conversion](https://help.trmnl.com/en/articles/10693981-advanced-liquid). ### BST vs UTC vs GMT UTC (Coordinated Universal Time) is **the standard for all timezones**, where any timezone is always referenced as how much +/- it is to UTC. GMT (Greenwich Mean Time) is *just a timezone and was formerly the standard used before UTC came along*. Now, to the point. If you set your timezone to UTC and live in the **United Kingdom**, you are only going to be in the accurate timezone for part of the year, because the UK, like many countries, observes Daylight Saving Time, shifting their clocks 1 hour during summer months, technically switching to **BST (British Summer Time)**. Luckily, you don't have to manage this manually, we'll do it for you: If you are in the UK and want to keep your calendars in sync accurately, use the **London** timezone in your account settings. ## The Plugin XXX is not Displaying Time Correctly There are three methods of manipulating date/time information in a plugin: - **Use the account's timezone offset** to display the date/time in the user's local timezone. *This is the **preferred** method, and we provide a [guide to assist plugin developers](https://help.trmnl.com/en/articles/10693981-advanced-liquid).* - Provide a **form field** option in plugin settings. *This method is used when it's beneficial to set a time different from your own account, such as with a world clock.* - **Don't**. Return the date/time information based on the data source. *This method is used when the server time is the source of truth for the information it provides, but also the default behavior without additional effort by plugin creators.* ### A Quick Note about .ICS Files and Calendars When calendar information is shared, it commonly uses the *ICS* file format, which is robust, dynamic, and notoriously varied from calendar provider to calendar provider. **The important note here is that TRMNL data is only as good as the information passed to it**, so if a calendar event is set to the wrong timezone, there are hidden spam events in the calendar, or the calendar server is set to a different timezone than your account and does not include proper information within the ICS file, these are all issues that can cause problems in displaying events *accurately*. --- # Command Palette Do you like hotkeys? We like hotkeys. While logged into the web application, press `cmd+k` (Mac) or `ctrl+k` (Windows/other) to open the command palette. ## Navigate the application Top level pages like Playlists, Devices, Dashboard, Account are accessible with a few leading characters from their page name. After finding a match, hit `enter` on your keyboard to jump to that page. Or use the direction arrows to select a different page. ## Find design documentation Look up Framework docs by keyword or utility name. ## Scoping your queries Page navigation and resource lookups are different behaviors. You may not want search results from one query to clutter your palette with unrelated items. To resolve this, use `option+tab` (Mac) or `alt+tab` (Windows/other) to scope results to a specific "provider." For example, here we're searching `ima` (for image related docs) but our palette includes navigation helpers that we don't need. By navigating to the "Documentation" provider with `option+tab`, note the highlighted resource in the top-right of the palette changes. Now the same query yields more helpful results. ## Nested queries TRMNL is a relatively simple platform with parent and child objects. Plugins have plugin "instances," an Account has multiple Devices. Your Playlist has multiple Items. To navigate to a specific device, you can execute multiple queries in sequence to both scope the results and perform tighter navigation. For example, here we're querying just `dev` in the search bar: But these results include additional items we don't need. Note the "Show all" feature, triggered by the `tab` key. Here's what happens when we press it. This scopes results only to Device records, making it easier to navigate to a specific device. But that's not all. When reviewing a specific device, a new "Switch" feature appears, also triggered by the `tab` key. By first digging into our Devices records to scope results, then inputting a partial device name, we can now set this device as our "current device" without navigating away from the current page. Nested query operations keep you in the same place, while also letting you modify your account. ## Editing plugin settings Coming soon. Similar to nested Device query operations, the intent here is to let you jump directly to plugin instances settings pages, or even perform other operations such as delete, export to Zip, and so on. Stay tuned, and stay focused. --- # ChatGPT Add a dash of artificial intelligence to your TRMNL e-ink display with ChatGPT. ### Step 1 - Visit the ChatGPT Plugin Inside TRMNL, navigate to Plugins > ChatGPT and add a Name. ### **Step 2 - Create a ChatGPT API Key** Visit[this link](https://platform.openai.com/settings/organization/api-keys) and create a new secret API. Give your key a name, add to a project and select All Permissions, then select create key. Copy your API Key. ### **Step 3 - Configure Plugin** Paste your Account API Key and create a prompt to be asked repeatedly. ​Optionally select a model and enable web search for up-to-date responses. ### **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing ChatGPT data very soon. You will need to make sure you are below your rate limits or else TRMNL may show an error from ChatGPT. Details on rate limits [here](https://platform.openai.com/docs/guides/rate-limits?context=tier-one#usage-tiers) and error codes [here](https://platform.openai.com/docs/guides/error-codes/api-errors). Stay focused. --- # Finding your TRMNL Mac Address Every TRMNL has 2 immutable characteristics -- a Mac Address, and a Serial. Mac Addresses are generated by our manufacturer, and [Serial values](https://help.trmnl.com/en/articles/11510216-finding-your-trmnl-serial) are printed directly on the PCB (circuit board). In some cases, TRMNL may need to know one of these values to help debug a connection issue. But instead of [disassembling your device](https://help.trmnl.com/en/articles/10003228-how-to-disassemble-your-trmnl), we suggest looking up your Mac Address instead. ## **Option A - WiFi Router settings** Pending your local network setup, you may have access to a wireless router settings interface or mesh network mobile app. This app will present a list of devices like your phone, television, computers, and so on. If you have access to such an interface, look for a device whose name starts with "ESP_", followed by a few random characters. Select that device and you should see a "MAC" value such as `AB:4D:CA:FF:Q1`. ## **Option B - WiFi Portal Footer** Put your TRMNL in [pairing mode](https://help.trmnl.com/en/articles/11511577-enable-wifi-pairing-mode). ​ Connect to the WiFi network named "TRMNL," and you should see a screen like this: **Using Firmware 1.5.11 (released July 11, 2025)** If you're on the latest firmware (devices update automatically), the bottom of your WiFi portal will include the MAC by default: **Using Firmware 1.5.10 or lower** To expose your MACc, input your WiFi network name (SSID) + password and tap Connect. For 1-2 seconds, your MAC will appear at the bottom like so: This screen disappears quickly, so it's easiest if you take a screenshot when you see the grey bar. You can do this with your phone's screen grab shortcut or on your computer, if you access this WiFi portal from a web browser. --- # Manually flash firmware There are a couple reasons you may want to dictate your device's firmware: 1. Your local network performs better on a specific release 2. You've modified the [native firmware](https://github.com/usetrmnl/firmware) or prefer to use your own If you simply need to update your device's firmware to the latest stable version, you may skip the entire guide below and use [this tool](https://trmnl.com/flash) to flash via a web browser in < 2 minutes. ## **Step 1 - Disable OTA Updates** Inside TRMNL, navigate to your device in the top-right dropdown. On the settings page, scroll down to Developer Perks and disable OTA Updates. Without this turned off, each time your device pings the TRMNL web server it will be instructed to download the latest firmware release. ## **Step 2 - Download the TRMNL Firmware (optional)** If you intend to use or modify TRMNL's firmware, you can download it here: [https://github.com/usetrmnl/trmnl-firmware](https://github.com/usetrmnl/firmware) We recommend using the GitHub CLI (`gh repo clone usetrmnl/firmware`) but you may also download it as a zip folder from the Code dropdown in the top-right corner. ## **Step 3 - Build + Flash Your Device** Follow the steps on our Firmware README to compile the downloaded source code (Step 2) and upload ("flash") it to your device. No coding knowledge is required. [https://github.com/usetrmnl/trmnl-firmware?tab=readme-ov-file#compilation-guide](https://github.com/usetrmnl/firmware?tab=readme-ov-file#compilation-guide) If you don't need the latest firmware version, and instead prefer to flash a different one, simply copy your preferred FW from the `builds` directory: Then paste it into the hidden `.pio` directory. The full path should be `.pio/build/trmnl/firmware.bin`, thus you should rename the copied "FWX.X.X.bin" to be named `firmware.bin`. When you have the preferred FW version (binary file) inside the `.pio/build/`trmnl directory -- either because you copy/pasted from the root `build` directory or chose to "Build" it via the Platform IO extension -- you are ready to upload. The README file continues these instructions: *Note the importance of putting your TRMNL in "[boot mode](https://help.trmnl.com/en/articles/11936721-put-your-trmnl-in-boot-mode)" for the contents to be written to the device.* ## **Step 4 - Set up your Device** You may have already set up your TRMNL device on the web application, which you don't need to do again. Instead, you'll just need to re-pair your local WiFi credentials. Turn off the device (following Step 3 above), wait 5 seconds, then turn the device back on. It should go into the default setup mode, prompting you to connect to TRMNL WiFi as described [here](https://intercom.help/trmnl/en/articles/9416306-how-to-set-up-a-new-device). After pairing your local WiFi network, TRMNL will run your customized firmware for as long as you allow it. To reverse all of the above at any time, just re-enable "OTA Updates" from your device settings inside the TRMNL web application. --- # Screenshot With the Screenshot plugin you can take a snapshot of any public web page, with any type of content. This could be a CCTV video feed, sports scores, or your child's lunch menu. ## Step 1 - Visit the Screenshot plugin Inside TRMNL, navigate to Plugins > Screenshot. First provide a website URL: Next, optionally add Header values in case the website needs to be authenticated. Assign header key/values with = and separate them with `&`. So `authorization=bearer xxx&content-type=application/json` becomes: ​ { "authorization":"bearer xxx", "content-type":"application/json" } If a key/value requires an `=` sign in the value, for example `bearer jwt==`, simply encode it as `%3D`, so `bearer%20jwt%3D%3D`. Finally, give your Screenshot plugin instance a name and click save. ## Step 2 - You're Done! Based on your desired refresh settings, TRMNL will begin generating screenshots with this URL. **Troubleshooting** *The website doesn't look the same as in my browser.* - The TRMNL display resolution is 800x480. In your browser (Chrome/Firefox) hold down shift+ctrl+c (Windows) or shift+cmd+c (Mac) to enter the Inspector mode. Here you can change the viewport to 800x480 to simulate how TRMNL will see the contents. *The website styling is broken*. - TRMNL does not visit this website directly and take a screenshot. Instead, we copy/paste the page's HTML contents into a new browser, then screenshot that result. This HTML will include references to CSS and JavaScript, which in most cases allows the page to be recreated anywhere. - In some cases, however, the web page may references CSS (styling) via relative links such as `"/styles.css"` instead of absolute links like `"https://some-website.com/styles.css"` . - For this situation, TRMNL attempts to convert all relative URLs for CSS + JavaScript to a full URL, granting our generated browser the context it needs to find your styling and interactive elements. This is not a perfect solution however. - If you have control over the web page you are screenshotting, we suggest ensuring that styles and other externally included libraries are available with absolute URLs instead of relative paths. *The screenshot is blank.* - TRMNL has a 5 second timeout for screenshots. If the content doesn't load in that timeframe, the image may appear blank. - Our screenshot generation service is in Germany. Some websites may not anticipate or allow traffic from EU origins. *Images don't load.* - Similar to our note above on relative vs absolute content paths, TRMNL needs embedded images to point to a full URL. So if your web page has an "" tag with a source attribute such as "photo.jpg", TRMNL cannot determine which URL this photo should be loaded from. - While TRMNL attempts to find the image on your website URLs server, this is not a perfect solution. - Image "src" attributes need to contain absolute paths to ensure accuracy during screenshot generation. --- # Applying the screen (anti glare) protector Your TRMNL shipment may include a pre-cut screen protector. This is included in all crowdfunded pre-orders, as well as all orders with the "Clarity Kit" add-on. This small bonus is an **anti-glare film**, designed to reduce light refraction on devices placed in direct sunlight. Applying this accessory is totally optional, and we suggest testing your device in its desired location to determine if you need it at all. To apply the anti-glare film you have a couple options: 1. Carefully place it on your device screen as-is, using the alcohol wipe and micrifiber cloth to remove dust and improve adherence. 2. [Dissassemble your device](https://intercom.help/trmnl/en/articles/10003228-how-to-disassemble-your-trmnl), remove the screen, and apply it with slightly better accuracy The benefit of Option 2 is in removing air bubbles following application. Just like adding a screen protector to your phone, you'll want to apply some (gentle) pressure with a sturdy but dull-eged tool to squeeze out the air. Otherwise you may wish you hadn't applied the protector in the first place. ;) --- # How to replace your kickstand Your kickstand has 2 curved arms that fit tightly in the pockets of the raised area on the back of your device. These arms have a notched side and a round side. The notched side of the arms should face "upward" towards you, like so: ​ To install a kickstand, start on one side, pushing the curved arm into the case's side pocket. We recommend keeping the kickstand as parallel to the back of the device case as possible, since the pocket holes are also fitted with the same half moon shape. After inserting one side, pull/stretch the kickstand across the raised middle area to the other side. Be sure to keep the kickstand as close to the case as possible, but not directly touching it, otherwise you may scratch the raised middle area with the 2nd arm's sharp edge. --- # Translations and Localizations The TRMNL web interface supports 11+ languages, from English to Spanish, simplified Chinese to French, Japanese to Korean, Ukranian and more. In addition to web interface translations, however, we've also begun supporting per-plugin localizations. Which lets you turn something like this: Into this (Korean): ## **Contribute** Since the application is always improving, new translations are often needed. To contribute to this ongoing project go here: (*no coding knowledge required*) **Note**: the web application can be extended to any language you'd like, right now. But plugins are added 1 at a time by our team, so if you don't see what you want please [create an Issue](https://github.com/usetrmnl/localizations/issues/new?title=Plugin%20request%20-%20%3Cinsert%3E). --- # Debugging private plugins When you're developing custom plugins in the Private Plugin editor, a few paradigms might lead to subtle bugs: - how [Polling URL](https://help.trmnl.com/en/articles/9510536-private-plugins#h_9d0511cca2)s work - using Liquid syntax ([101](https://help.trmnl.com/en/articles/10671186-liquid-101), [advanced](https://help.trmnl.com/en/articles/10693981-advanced-liquid)) - building [custom form fields](https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder) One thing that shouldn't feel "new" is **Logs**. TRMNL offers a couple utilities to improve your plugin development workflow. ### **JavaScript Logs** From the Markup Editor, click to expand the JS Logs dropdown beneath Your Variables. This is useful for debugging client-side logic, and is similar to cracking open the Developer Console in a modern browser. In addition to `console.log()` style output however, TRMNL will attempt to decorate these logs with a level of severity. The example above showcases 3 severity levels - default, warn, error. - **default**: white text, includes your own debuggers and [Framework Modulations](https://trmnl.com/framework#modulations) - **warn**: yellow text; actionable status is up to you - **error**: red text; includes `error`, `onerror`, `unhandledrejection`, and `fetch` prefixes ### **Plugin Logs** For a firehose of server-side logs, enable Debug Logs from the plugin's settings page. After activating, a live stream of logs will be enabled for 24 hours. This limits your data exposure to TRMNL and keeps our storage lean. Now you may visit your Logs, either from the Markup Editor > View Logs, or from your Device's settings page (top right dropdown > gear icon) > Logs. Server-side log examples: - API failures to your Polling URL(s) - Image generation exceptions - Server hiccups, e.g. if a screen is generated during a deployment - Liquid `filter` syntax errors ([see more](https://help.trmnl.com/en/articles/10347358-custom-plugin-filters)) If you have any questions or want to recommend another log type, create an **#ideas** thread inside the Developer-only Discord (link in your Account tab). --- # Calculating BYOD and Dev Edition add-ons TRMNL started with a single native device model and a Developer Edition add-on that unlocks the API + custom plugin development. Our ecosystem later expanded to DIY hardware, jailbreaks for [Kindle](https://trmnl.com/guides/turn-your-amazon-kindle-into-a-trmnl) / [Kobo](https://github.com/usetrmnl/trmnl-kobo) / [Android](https://github.com/usetrmnl/trmnl-android) / [Raspberry Pi](https://trmnl.com/guides/turn-your-amazon-kindle-into-a-trmnl), and open source server clients known as [BYOS](https://docs.trmnl.com/go/diy/byos). Below are scenarios to help you figure out which licenses are required for your desired outcome. ### Multiple (native) devices on the same account If you want a TRMNL on your desk and kitchen counter, you only need 1x Developer Edition add-on, on 1 of your devices. This lets you build custom plugins and connect to [Recipes](https://trmnl.com/recipes) or [Third Party](https://help.trmnl.com/en/articles/10546870-compare-custom-plugin-types) plugins, which you can add to your non- Developer Edition device on the Playlists tab. ### **Multiple (native) devices on different accounts** Perhaps you're getting TRMNLs for yourself and a few colleagues, or as a gift to someone who doesn't live with you. This may require 1 or more licenses. One TRMNL account may have unlimited devices, however we do not support "team" access with multiple user accounts. So depending on your relationship with other device owners, you may prefer not to share a password or allow them access to your connected plugins (personal data). The spirit of our Developer Edition add-on is that it is per-device, since most orders for 2+ devices are for different people, located in different places. However if you simply wish to grant devices to colleagues, and mirror your parent device's content (ex: sales dashboards, non personal content) to their devices, you can forego additional Developer Edition add-ons and use our [native mirroring feature](https://help.trmnl.com/en/articles/10530871-mirroring-a-device). ### **Combination of native + BYOD devices on the same account** First, let's define BYOD as DIY hardware *or* jailbroken 3rd party devices like Kindle. Interacting with the TRMNL `/api/display` endpoint ([docs](https://docs.trmnl.com/go/private-api/fetch-screen-content)) requires an API Key to be set as the `access-token` HTTP header. API Keys are provisioned per device, and each device has its own unique playlist of content. API Keys are available to: - Native TRMNLs with Developer Edition add-on - BYOD devices with the BYOD license The `/api/display` endpoint that our [OSS firmware](https://github.com/usetrmnl/firmware) pings for new content has a couple important features on the server-side: 1. Each GET to `/api/display` auto-advances your Playlist to the next item, based on your playlist's settings and display logic rules 2. GET requests are rate-limited (as of June 2025: ~5 per 5 minutes) From a UX perspective, if you have a Playlist with items A-B-C, and 2x devices (native or BYOD) sharing the same API key, then Device 1 will render Item A, then Device 2 will render Item B, followed by Device 1 rendering Item C. This may be confusing if your intent is to see the same content on each device. To mirror the same content *without* additional Dev Edition or BYOD licenses, update the `/api/display` endpoint in your device's firmware (or jailbreak binary, e.g. [trmnl-display](https://github.com/usetrmnl/trmnl-display/blob/f8f314935943780f866d11c0de908b043087e712/trmnl-display.go#L374) for Raspberry Pi) to `/api/current_screen` ([docs](https://docs.trmnl.com/go/private-api/fetch-screen-content#current-screen)). Alternatively, to grant each device a flexible playlist with native mirroring support, purchase another Dev Edition add-on (for native TRMNLs) or BYOD license (DIY TRMNLs) on each of your devices. Then you can [enable mirroring](https://help.trmnl.com/en/articles/10530871-mirroring-a-device) in 1 click, no code changes required. ### **Partial OSS approach (native device + BYOS server client)** If you've purchased a TRMNL but want to point it to your local network or private cloud, you don't need any Developer Edition or BYOD licenses. Most [BYOS server clients](https://docs.trmnl.com/go/diy/byos) already support device auto-provisioning, which is a fancy way to say that they detect your TRMNL's MAC address and allow you to control its API key. You may notice that your TRMNL web account hides your API key if you don't have Developer Edition enabled, but this can be safely ignored. ### **Full OSS approach (native device + BYOS server client)** If you intend to build a device and leverage one of our many [BYOS servers](https://docs.trmnl.com/go/diy/byos), you don't need to pay TRMNL anything. You can flash our OSS firmware to your board, point it to your local network or private cloud via the Captive Portal > Custom Server UI, and enjoy. We don't need your data, and you don't need our permission. --- # Using Google Sheets with Private Plugins You've got data, and you want to put it on TRMNL. If the content fits nicely on a website, [Screenshot](https://help.trmnl.com/en/articles/10302121-screenshot) might do the trick. If it's live at a JSON endpoint, Private Plugins with a Polling URL work great. But what if you don't code, or don't *want* to code, and it's easier to put the content inside a Google Sheet? ## **Step 1 - Test Drive** If you know what you're doing, skip to Step 2. ### **Chart / Graph** Download our [demo plugin zip file](https://drive.google.com/file/d/1IzN5sUbRcXqovYjpovRJKOq2SLhQXof8/view?usp=sharing) and import it to your account via Plugins > [Private Plugin > View All](https://trmnl.com/plugin_settings?keyname=private_plugin) > Import new. **Note**, you need at least 1x Private Plugin to exist before this screen will appear. After importing, click Edit Markup in the top right corner to open the live preview. Scroll down inside the code editor to see how it works. The raw data is visible [here](https://docs.google.com/spreadsheets/d/1x3Rbtl2b_PBSsnM4FXrZLHvklzOwnf-ENiQjIDLGbz0/edit?usp=drive_link). You may also adjust "ColumnChart" to "LineChart" or "BarChart" on Line ~68. ### **Table / List** Download our [demo plugin zip file](https://drive.google.com/file/d/1fRL3WnsLyUIwe2OnH2JNynEDrdXZC06n/view?usp=sharing) and import it to your account via Plugins > [Private Plugin > View All](https://trmnl.com/plugin_settings?keyname=private_plugin) > Import new. **Note**, you need at least 1x Private Plugin to exist before this screen will appear. After importing, click Edit Markup in the top right corner to open the live preview. Scroll down inside the code editor to see how it works. The raw data is visible [here](https://docs.google.com/spreadsheets/d/1jpL3aa_j7ywHnaFyXXFwxehxaadiuPP_hPu8SdYa8s8/edit?usp=sharing). Hint: play with features like `label--inverted` and `label--outline` ([docs here](https://trmnl.com/framework/label)) to make your new table more visually appealing. ## **Step 2 - Point to real data** From your Google Sheet, click Share and add the "Anyone with a link can view" permission. Next, adjust the spreadsheet URL like so: https://docs.google.com/spreadsheets/d/spreadsheet-id-goes-here/export?format=csv **Note on the sheet (ID) of data you want to import** The URL formatting example above assumes your data is on the first tab of your Google Sheet. To import from a specific non-first tab: 1. Visit that tab of data by clicking it inside your Google Sheet 2. Note the addition of `/edit?gid=XXX` at the end of your current browser URL 3. Extract this GID value, which will likely be a ~10 digit number 4. Append it to your URL above as `&gid=XXX` An example URL would now look like so: https://docs.google.com/spreadsheets/d/spreadsheet-id-goes-here/export?format=csv&gid=xxx Inside TRMNL > Plugins, create a new Private Plugin. Input your modified spreadsheet URL into the "Polling URL" field and leave all other settings alone. Click to Save in the top right, then click "Force Refresh" to expedite TRMNL's data fetch. Navigate to the Markup Editor, and find your spreadsheet data inside Your Variables > Data. Raw data from your Google Sheet lives inside a node called `data`, as an array of arrays. Each array element represents a row of data in your spreadsheet. If you wish to ignore the header row, for example to build a chart, the following Liquid syntax will repurpose your `#{{ data }}` collection to only contain rows with actual information. // remove header (1st) row from spreadsheet data with slice {% assign formatted_data = data | slice: 1, data.size - 1 %} Now you may iterate through `formatted_data` inside any charting library (JavaScript), or within the HTML itself. Import the [demo plugin](https://drive.google.com/file/d/1IzN5sUbRcXqovYjpovRJKOq2SLhQXof8/view?usp=sharing) for a live example. --- # Mirroring a Device Suppose you have 3x TRMNL devices. One is your "main" TRMNL, in your office. The other 2x are in your bedroom and kitchen. You may want to control most of the content via the office device, while still allowing the bedroom and kitchen to show unique content, and with unique settings. For this scenario we offering mirroring. More use cases are described at the bottom of this page. ### How to Enable Mirroring **Step 1** Navigate to your parent (master) device settings via the top-right dropdown > gear icon beside your device name. Scroll down to Visibility, and set it as Sharable. Next, copy the Device ID to your clipboard. This is near the top of your device settings page. **Step 2** Switch to your "child" device from the same top-right dropdown. On its settings page, scroll down and input the 6 digit Device ID of your parent device that was made Sharable a moment ago. ​ Click Mirror, and you'll see a copy of the parent device plugins appear on your Playlist tab. The child device may still have its own unique plugins and settings. **Step 3 (optional)** Sharing a device is a one-time behavior. If you make dramatic changes to the parent device Playlist, just click "Sync Screens" from the child device settings page. ### How does Mirroring Work? 1. Once a device is set up to be Sharable instead of Standalone, anyone can mirror the device if they know the 6 digit `friendly_id`. 2. Mirroring a device copies the plugins in the master device's playlist at the start. If the master device adds more plugins later, the mirrored device will not automatically receive them. 3. If the master device changes the visibility setting to "Standalone" from "Sharable", TRMNL will delete all the mirrored plugins from child devices that have the master device's plugins. This ensures the master device can always modify the mirrorable settings. However, independently installed plugins in child device would remain unaffected. 4. A child device can install its own plugins independently, and nothing will be shared with the master device. 5. If the master device has shared 5 plugins, the child device can delete 2 and retain only 3. However, deleting plugins from the child device will not affect the plugins on the master device. ### Mirroring use case - Mirroring is useful when doing a Team purchase, where one Team Member acts as a master device and OAuth's the B2B dashboard eg (Shopify, Google Analytics, Mailchimp, etc) without necessarily having to share the credentials with the rest of the team. The Child device will get the B2B dashboard while still being able to install their own plugins - Sharing a plugin with your grandparents / parents who are not technically sound. You can install a shared family calendar / note board / birthday reminder on the parent device and the child device will syndicate them automatically. --- # How refresh rates work As illustrated in our [architecture docs](https://docs.trmnl.com/go/how-it-works), TRMNL has 1 paradigm that underscores how content is displayed on your device. To summarize: **Devices ping the server. Not the other way around**. Practically this means: - You cannot click a button on our website and 'force' new content to appear on your device. Your device must ask for it. - Your device will not "know" when new content has been synced, or new screen images have been generated. The device must ask for it. Of course, it's natural to want to skip ahead to new content, or refresh a generated screen while building custom plugins. So that is possible, but you'll leverage both the web interface and your device’s skip button to do it. Below are more details about how TRMNL refresh rates work. 1. [Plugin refresh rates](#h_64269842e4) 2. [Overriding plugin refresh rates](#h_6bc016b817) 3. [Device refresh rates](#h_854b46ae51) 4. [Mashup refresh rates](#h_8943c22098) 5. [Lazy refreshing device content](#h_7094fc0e29) 6. [Determining "when" (hh:ss) a refresh occurs](#h_ead6e5ebd0) ## Plugin refresh rates Every native plugin has a default refresh rate. For example our "Days left this year" refreshes just 1x per day, because it would be pointless to re-generate the same screen intra-day. Meanwhile our GitHub Commit Graph updates every 30 minutes. The random Wikipedia article generator refreshes every 4 hours. And our Product Hunt plugin refreshes hourly. These are just defaults, however. You are welcome to override them on your own account, on a per-plugin basis. ## Overriding a plugin's refresh rate After setting up a plugin in your TRMNL account, you'll notice a refresh rate dropdown on the right side of the settings page: Depending on which plugin you're editing, this dropdown will have different options. When it's time to generate new screen content, **TRMNL will follow whichever refresh rate is faster** -- yours, or the system default. However, **you cannot refresh faster than the system allows**. So for example, since Shopify has a max speed of every 10 mins, you can refresh every 10, 15, etc minutes. But since our RSS plugin refreshes at a max speed of 60 mins, you cannot refresh faster than 60 minutes. We are open to decreasing refresh intervals on a case by case basis. These limitations are simply in place to predict our server resource requirements and maintain free, lifetime usage for all of your TRMNL devices. ## Device (playlist) refresh rates Adjacent to your plugin refresh rates is your device's refresh rate. This is simply how often your TRMNL wakes up, requests new content, renders it, then goes back to sleep. You may configure your device's refresh (fetch) rate from either the Playlists or Devices tab. If you edit from the Playlists tab, you get even more control, because you can change this interval throughout the day. For example, you may want to switch between your calendar and the weather every 30 minutes in the morning, but then switch between reddit, your glucose meter, and stock quotes every 5 minutes throughout the afternoon. It's important to note that the screen generation process, governed by your plugin settings, happens *independently* of your device wakeup / fetch interval. In other words, **TRMNL does not sync content + generate graphics on-demand when your device pings the server**. This would be too compute intensive, drain your device battery (to stay awake for fresh content), and have unexpected results in the case that new content doesn't exist or the 3rd party plugin is non-responsive. ## Lazy refreshing device content Above we describe how plugin content may be requested every *N* minutes, based on your preferences on the plugin settings screen. However it is possible (and common) that when TRMNL requests new data from a plugin, the content has not changed since our last request. For example, if your connect Google Calendar and set it to refresh every 15 minutes, TRMNL will sync new calendar events every 15 minutes. But if the events returned to us have the same title, description, start/end date, and so on, TRMNL will not generate a new graphic for your device. Here's an example of that in action. This is due to TRMNL's timer-based content generation strategy. Every minute, TRMNL loops through every plugin that is due to be refreshed and attempts to create new screens. This screen creation attempt has 2 steps: 1. **sync** new content 2. **render** new content When TRMNL syncs content, but it matches the previously synced content (from 15+ minutes ago), the system stops and does not re-*render* (generate a screen) the content. This saves compute power, without changing your device experience. You will still see the calendar graphic if it's up next on your Playlist, but the image itself may have been generated 15+ minutes ago. Thus on your plugin settings screen might look like this: Here we have a 1 hour refresh set on our Shopify plugin, but the "Refreshed" timestamp shows 6 hours ago. That implies we have not received a new order in 6 hours. But rest assured, TRMNL looks for new orders every hour based on our preferences. ## **Mashup refresh rates** A Mashup is any combination of 2 or more plugins on a single screen, using one of these layouts (excluding the top left option): Since Mashups are composed of several underlying Playlist items (plugin settings), we sync new data for each of your plugin settings when generating Mashup screens. Mashups update at the speed of the fastest underlying plugin. So if you have a 3x quadrant Mashup, where the underlying plugins refresh every 15 minutes, hourly, 1x/day, then your entire Mashup will update every 15 minutes. ## **Determining "when" a refresh occurs** New screens are generated relative to the moment in time that you establish a refresh rate, except 1x / day plugins. 1x / day plugins always refreshes at 00:15 local time. 1x / day mashups always refreshes at 00:30 local time. If you enable Google Calendar at 14:00 (2p) local time, and the default refresh rate is 1x per hour, then it will refresh daily at 15:00 (3p) your local time. If you later change your Google Calendar plugin to refresh 4x /day, and you make this change at 18:00 (6p) local time, then new screens will be generated at 18:00, 00:00, 06:00, and 12:00 in your local time. You may easily manipulate this refresh interval to a more predictable moment, for example every 15 minutes at HH:15, HH:30, and so on, by clicking "Force Refresh" on the right side of your plugin settings screen, at that moment. Just note that over time, your plugin's refresh interval will slip by a few seconds, possibly every day. This is due to fluctuations in our background queue's number of jobs, server deployments that pause the queue, and so on. ## **In summary** We're linking this help guide from a few places in-app to clarify how refreshing works, but the main points are: 1. TRMNL's website does not 'ping' your device, only your device can ping the TRMNL website 2. Your plugin <> device refresh intervals, when combined with our lazy loading generation strategy, may seem to indicate that plugins are not generating on time. But TRMNL does follow your timing preferences, with exceptions mentioned above. If you have additional questions, send us an email or live chat at any time. Stay focused. --- # Apple Calendar Connecting your Apple Calendar to TRMNL takes just a few seconds. ## **Step 1 - Visit the Apple Calendar Plugin** Inside TRMNL, navigate to Plugins > Apple Calendar. ## **Step 2 - Connect Apple Calendar** **Note**: *Enabling this plugin requires publishing your Apple calendar to an anonymized "ICS" style format. This is similar to publishing an unlisted YouTube video or "view with link" permission Google Docs file. If your calendar has sensitive data you may limit the published permissions to only expose timestamps, not event details.* **From an iPhone / iPad** (only suggested if you don't use a Mac computer) 1. Open the Calendar app 2. Tape Calendars, then tap the (i) next to the iCloud calendar you want to share 3. Turn on Public Calendar, then tap Share Link to copy or send the URL ([source](https://support.apple.com/guide/iphone/share-icloud-calendars-iph7613c4fb/ios)) **From a Mac computer** First open your Apple Calendar client. Ctrl-click on the calendar and check the Public Calendar box. After a moment (you may need to navigate away, then re-open this pane) a URL will appear below. Copy this long URL value. ## **Step 3 - Configure Plugin** Back inside TRMNL, paste your published calendar link into the ICS input field. Important: replace the "webcal" text with "https" so your full URL looks like `https://p44-caldav.icloud.com/published....`. Next you may set a few preferences. **Time Format** 24 hours vs 12 hour (AM/PM) style. **Include Description** If "Yes," a truncated preview of the event's description field will be shown beneath the event title. Note that on the "default" Layout (explained below), setting this option to "No" will ~double the number of events shown thanks to saved space. **First Day of Week** Applies only to the "month" view. Similar to native Outlook Calendar (month view), this allows you to set which day of the week is represented by the first column, furthest to the left. Defaults to Sunday. **Ignored Phrases** Supports a list of words that will be used to ignore events based on title or description content. **Layout** - Default (upcoming events for today, tomorrow, next day) - Week (5-7 days upcoming) - Month (this calendar month) Note that only "Week" and "Month" layouts support multi-day events, but every layout supports all-day events. **Example** - Week layout with multi-day event ("Michael OOO") **Example** - Default layout with all-day event ("Michael OOO") **Example** - Month layout with multi-day event: ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Apple Calendar events very soon. Note: while we fetch data according to your refresh interval, TRMNL only generates new screen renders if the data fetched contains changes. Thus setting your layout to "Default" (which shows 2-3 days) but updating an event 4+ days in the future will not trigger a screen refresh. Stay focused. --- # Using Google Docs with Private Plugins You've got data, and you want to put it on TRMNL. If the content fits nicely on a website, [Screenshot](https://help.trmnl.com/en/articles/10302121-screenshot) might do the trick. If it's live at a JSON endpoint, Private Plugins with a Polling URL work great. If it lives in a spreadsheet, [Google Sheets](https://help.trmnl.com/en/articles/11400219-using-google-sheets-with-private-plugins) are great. But what if you don't code, or don't *want* to code, and it's easier to put the content inside a Google Document? ## Step 1 - Test Drive If you know what you're doing, skip to Step 2. Otherwise, download our [demo plugin zip file](https://drive.google.com/file/d/1ZY4uO2h8q0zrKkkVtCZcEz0v_21RReAE/view?usp=sharing) and import it to your account via Plugins > [Private Plugin > View All](https://trmnl.com/plugin_settings?keyname=private_plugin) > Import new. **Note**, you need at least 1x Private Plugin to exist before this screen will appear. After importing, click Edit Markup in the top right corner to open the live preview. Scroll down inside the code editor to see how it works. The raw data is visible [here](https://docs.google.com/document/d/1JD73yljlhTWTh_LAsdezFSoCS1LesHL_3ryUvBYzwbY/edit?usp=sharing). The trick with Google Docs as a data source is that the content will arrive inside TRMNL as plain text, and you'll need to use Liquid syntax to manipulate it into structured data. Read our guides to Liquid plugin development [here](https://help.trmnl.com/en/articles/10671186-liquid-101) and [here](https://help.trmnl.com/en/articles/10693981-advanced-liquid). ## **Step 2 - Point to real data** From your Google Doc, click Share and add the "Anyone with a link can view" permission. Next, adjust the spreadsheet URL like so: https://docs.google.com/document/d/doc-id-goes-here/export?format=txt Inside TRMNL > Plugins, create a new Private Plugin. Input your modified Doc URL into the "Polling URL" field and leave all other settings alone. Click to Save in the top right, then click "Force Refresh" to expedite TRMNL's data fetch. Navigate to the Markup Editor, and find your spreadsheet data inside Your Variables > Data. Now you may iterate through `data` with Liquid's `split` and `for X in Y` features to structure, parse, and manipulate plain text into a functional plugin. Import the [demo plugin](https://drive.google.com/file/d/1ZY4uO2h8q0zrKkkVtCZcEz0v_21RReAE/view?usp=sharing) for a live example. --- # Weather Your own weather station is just a few clicks away, thanks to data generously provided by our friends at [Tempest](https://shop.tempest.earth/products/tempest) ([learn more](https://trmnl.com/blog/weather-powered-by-tempest)). ## Step 1 - Visit the Weather Plugin Inside TRMNL, navigate to Plugins > Weather. ## Step 2 - Configure Plugin Give your plugin a **name**, in case you want to track forecasts in multiple locations. This name is shown in the bottom corner of your device screen. Next, determine a **Data Provider**. We suggest Tempest, with a fallback to WeatherAPI if there are no nearby weather stations. Input **Lat/Long** if you chose Tempest, which you can [determine here](https://www.latlong.net/) in just a few seconds. Input a **location** if you chose WeatherAPI. Note that you may need to try a few combinations, for example "Atlanta, GA" (city, 2-digit province). See [more examples here](https://www.weatherapi.com/docs/#intro-request) underneath Request Parameters. Now it's time for general preferences: - **Temperature Unit**, either Fahrenheit or Celsius - **Forecast Headings**, to dictate how dates appear - **Wind Unit**, such as miles per hour or bft (whatever that means) - **Precipitation Unit**, for example in centimeters or inches ## Step 3 - You're Done! Based on your device's refresh settings, you'll begin seeing weather reports very soon. Note: while we fetch data according to your refresh interval, TRMNL only generates new screen renders if the data fetched has changes. So if you set the weather to refresh every 15 mins, but the forecast is the same for 4 hours, the 'refreshed at' timestamp will remain the same. Stay focused (and dry). --- # Language Learning The TRMNL language learning plugin is simple -- 1 new word, every 24 hours, with an example sentence. Setting it up takes just a few seconds. ## **Step 1 - Visit the Language Learning Plugin** Inside TRMNL, navigate to Plugins > Language Learning. Choose a target language from the dropdown, then click Save. ## **Step 2 - You're Done!** Based on your device's refresh settings, you'll begin seeing new words and sentences very soon. Stay focused. --- # Fastmail Calendar Connecting your Fastmail Calendar to TRMNL takes just a few seconds. ## **Step 1 - Visit the Fastmail Calendar Plugin** Inside TRMNL, navigate to Plugins > Fastmail Calendar. ## **Step 2 - Connect Fastmail Calendar** **Note**: *Enabling this plugin requires publishing your Fastmail calendar to an anonymized "ICS" style format. This is similar to publishing an unlisted YouTube video or "view with link" permission Google Docs file. If your calendar has sensitive data you may limit the published permissions to only expose timestamps, not event details.* Inside Fastmail, navigate to Settings > Calendar. Beside the calendar you wish to integrate with TRMNL, click the Edit & Share button. In the **Publish** section and click the toggle button next to Full event details or Free/busy information, depending on what information you wish to share. ([source](https://www.fastmail.help/hc/en-us/articles/360060590793-Calendar-sharing)) Click "Copy to clipboard" below this long URL. ## **Step 3 - Configure Plugin** Back inside TRMNL, paste your published calendar link into the ICS input field. Important: replace the "webcal" text with "https" so your full URL looks like `https://user.fm/calendar/v1-....`. Next you may set a few preferences. **Time Format** 24 hours vs 12 hour (AM/PM) style. **Include Description** If "Yes," a truncated preview of the event's description field will be shown beneath the event title. Note that on the "default" Layout (explained below), setting this option to "No" will ~double the number of events shown thanks to saved space. **First Day of Week** Applies only to the "month" view. Tis allows you to set which day of the week is represented by the first column, furthest to the left. Defaults to Sunday. **Ignored Phrases** Supports a list of words that will be used to ignore events based on title or description content. **Layout** - Default (upcoming events for today, tomorrow, next day) - Week (5-7 days upcoming) - Month (this calendar month) Note that only "Week" and "Month" layouts support multi-day events, but every layout supports all-day events. **Example** - Week layout with multi-day event ("Michael OOO") **Example** - Default layout with all-day event ("Michael OOO") **Example** - Month layout with multi-day event: ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Fastmail Calendar events very soon. Note: while we fetch data according to your refresh interval, TRMNL only generates new screen renders if the data fetched contains changes. Thus setting your layout to "Default" (which shows 2-3 days) but updating an event 4+ days in the future will not trigger a screen refresh. Stay focused. --- # Nextcloud Calendar Connecting your Nextcloud Calendar to TRMNL takes just a few seconds. ## **Step 1 - Visit the Nextcloud Calendar Plugin** Inside TRMNL, navigate to Plugins > Nextcloud Calendar. ## **Step 2 - Connect Nextcloud Calendar** **Note**: *Enabling this plugin requires publishing your Nextcloud calendar to an anonymized "ICS" style format. This is similar to publishing an unlisted YouTube video or "view with link" permission Google Docs file. If your calendar has sensitive data you may limit the published permissions to only expose timestamps, not event details.* Inside your Nextcloud Calendar, open the share menu and click the "+" next to the "Share link" option ([source](https://docs.nextcloud.com/server/19/user_manual/pim/calendar.html#publishing-a-calendar)). Copy this URL to your clipboard. ## **Step 3 - Configure Plugin** Back inside TRMNL, paste your published calendar link into the ICS input field. Your full URL should begin with "https". You can attempt to visit it in your web browser -- it should download an ".ics" file to your computer which you can safely delete. Next you may set a few preferences. **Time Format** 24 hours vs 12 hour (AM/PM) style. **Include Description** If "Yes," a truncated preview of the event's description field will be shown beneath the event title. Note that on the "default" Layout (explained below), setting this option to "No" will ~double the number of events shown thanks to saved space. **First Day of Week** Applies only to the "month" view. Tis allows you to set which day of the week is represented by the first column, furthest to the left. Defaults to Sunday. **Ignored Phrases** Supports a list of words that will be used to ignore events based on title or description content. **Layout** - Default (upcoming events for today, tomorrow, next day) - Week (5-7 days upcoming) - Month (this calendar month) Note that only "Week" and "Month" layouts support multi-day events, but every layout supports all-day events. **Example** - Week layout with multi-day event ("Michael OOO") **Example** - Default layout with all-day event ("Michael OOO") **Example** - Month layout with multi-day event: ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Nextcloud Calendar events very soon. Note: while we fetch data according to your refresh interval, TRMNL only generates new screen renders if the data fetched contains changes. Thus setting your layout to "Default" (which shows 2-3 days) but updating an event 4+ days in the future will not trigger a screen refresh. Stay focused. --- # Outlook Calendar Connecting your Outlook 365 Calendar to TRMNL takes just a few seconds. ## **Step 1 - Visit the Outlook Calendar Plugin** Inside TRMNL, navigate to Plugins > Outlook Calendar. ## **Step 2 - Connect Outlook Calendar** **Note**: *Enabling this plugin requires publishing your Outlook calendar to an anonymized "ICS" style format. This is similar to publishing an unlisted YouTube video or "view with link" permission Google Docs file. If your calendar has sensitive data you may limit the published permissions to only expose timestamps, not event details.* First log in to your Outlook email at [outlook.office365.com](https://outlook.office365.com). Visit your calendar on the left side navigation, then click the settings gear cog in the top right corner. Click the "Shared calendars" link, then underneath the "Publish a calendar" heading you can select a calendar + set permissions. Click Publish, then click to copy the 2nd link labeled ICS. **Troubleshooting** If you don't see the ability to Share or Publish a calendar, first make sure that calendar sharing is enabled at the administrator level. The following view is accessible from your Office 356 admin area: After confirming that users may share their calendars, retry the beginning of Step 2 above. ## **Step 3 - Configure Plugin** Back inside TRMNL, paste your published calendar link into the ICS input field. Next you may set a few preferences. **Time Format** 24 hours vs 12 hour (AM/PM) style. **Include Description** If "Yes," a truncated preview of the event's description field will be shown beneath the event title. Note that on the "default" Layout (explained below), setting this option to "No" will ~double the number of events shown thanks to saved space. **First Day of Week** Applies only to the "month" view. Similar to native Outlook Calendar (month view), this allows you to set which day of the week is represented by the first column, furthest to the left. Defaults to Sunday. **Ignored Phrases** Supports a list of words that will be used to ignore events based on title or description content. **Layout** - Default (upcoming events for today, tomorrow, next day) - Week (5-7 days upcoming) - Month (this calendar month) Note that only "Week" and "Month" layouts support multi-day events, but every layout supports all-day events. **Example** - Week layout with multi-day event ("Michael OOO") **Example** - Default layout with all-day event ("Michael OOO") **Example** - Month layout with multi-day event: ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Outlook Calendar events very soon. Note: while we fetch data according to your refresh interval, TRMNL only generates new screen renders if the data fetched contains changes. Thus setting your layout to "Default" (which shows 2-3 days) but updating an event 4+ days in the future will not trigger a screen refresh. Stay focused. --- # Parcel The [Parcel](https://parcelapp.net/) app for iOS and macOS conveniently tracks packages across many different delivery providers. Connecting the plugin to your Parcel account only takes a few steps. A premium subscription to Parcel is required. ## Step 1 - Visit the Parcel Plugin Inside TRMNL, navigate to Plugins > Parcel. ## Step 2 - Log in to Parcel Web Access In a new tab, visit and follow the instructions to sign in. ## Step 3 - Generate the API Key Click on "API" to bring up the dialog box. Then click "Create a Key" to generate a new API key. Copy the API key into the clipboard. ## Step 4 - Configure Plugin Back in the TRMNL dashboard, paste in the API key, and select what type of deliveries to display. You're done! --- # Liquid 101 When building [Private Plugins](https://help.trmnl.com/en/articles/9510536-private-plugins) in TRMNL, we need a way to display the data gathered by the plugin in our TRMNL screen. This is where "Liquid" comes in -- it's a templating language that bridges the gap between raw data and the visual layout of your plugin. ## **Quickstart** Let's say we have a plugin that receives data about a task to be done, and this is represented by the following JSON data structure: ​ { "name": "Walk the dog", "date": "2025-02-24", "late": true, "location": { "city": "New York" } } To display this data in our TRMNL's screen, we can write a simple HTML snippet like below and the `#{{ name }}` and `#{{ date }}` would automatically be replaced by the corresponding values on the JSON.

Task "#{{ name }}" is due on #{{ date }}

Result: Task "Walk the dog" is due on 2025-02-24 With this small example, we have just learned the most basic concept of Liquid. Enclosing any property of our JSON document between `#{{` and `}}` is how we output the value of a property or a variable. We can also do what's called "dot notation" to navigate on the JSON structure and display deeply nested properties like this `location.city` example:

Task "#{{ name }}" is due on #{{ date }} in #{{ location.city }}

Result: Task "Walk the dog" is due on 2025-02-24 in New York ## **Variables** You can also create variables in Liquid and assign values to them for later display. Usually we employ variables when we need to modify the JSON data of the plugin, for example by doing some math with one the properties. To create a variable we use the special keyword `assign` like below: {% assign my_variable = "my value" %} We can now display this value by doing `#{{ my_variable }}` like we did before with the JSON properties. Also notice how Liquid code is written between the delimiters `{%` and `%}`. ## **Conditionals** We can create conditional expressions in Liquid that are evaluated against our data and decide if a block of code is executed or not. Let's say we want to show a warning emoji (⚠️) next to the task's date if it's running late. For this we can create an `if` expression that evaluates if the value of the `late` property equals `true`. {% if late == true %}

Task "#{{ name }}" is due on ⚠️ #{{ date }} ⚠️

{% else %}

Task "#{{ name }}" is due on #{{ date }}

{% endif %} Result: Task "Walk the dog" is due on ⚠️ 2025-02-24 ⚠️ Conditionals do not need to check for equality only. Liquid supports all the standard logical operators to check for difference, greater than, less than or equal, and so on. ## **Handling Lists** There are many cases where the data we want to display is actually a list of things. Let's reimagine our task example as a list of tasks represented by the following JSON structure: { "tasks": [ { "name": "Walk the dog", "date": "2025-02-24", "late": true, }, { "name": "Half-Life 3 Release", "date": "2033-10-08", "late": false, } ] } We now have a property called `tasks` that holds a list with two tasks inside. To display all the tasks on our TRMNL we need to loop through the list and display the properties of each task individually. This can be achieved with iteration, or a `for` loop as we usually call it. Rewriting our code to handle the list and print each item looks like this: {% for task in tasks %}

Task "#{{ task.name }}" is due on #{{ task.date }}

{% endfor %} Result: Task "Walk the dog" is due on 2025-02-24 Task "Half-Life 3 Release" is due on 2033-10-08 As you can see, the code `for task in tasks` actually creates a temporary variable called `task` that holds all the properties of the current task being iterated. That's why we are able to display the property values using `#{{ task.name }}` instead of just `#{{ name }}` as we did before. ## **Putting it all together** Now that we already know how to apply conditionals and handle lists, mixing both of these concepts is where all the fun begins! Let's say we only want to display the tasks that are currently late, we can combine `for` and `if` to loop through the list and filter only the tasks we're interested in: {% for task in tasks %} {% if task.late == true %}

Task "#{{ task.name }}" is due on #{{ task.date }}

{% endif %} {% endfor %} Another way of accomplishing this would be using a "filter", as we'll see on the next section. ## **Filters** Filters are a very powerful construct in Liquid as they allow us to manipulate and transform our data in many different ways. Let's start with a very simple example, where we would like to display our task name with uppercase letters. We can employ the `upcase` filter for this:

Task "#{{ task.name | upcase }}" is due on #{{ task.date }}

Result: Task "WALK THE DOG" is due on 2025-02-24 Now let's rewrite the example above that showed only the late tasks to use the `where` filter instead. This filter acts upon a list of objects, and returns a new list with only the objects that pass a certain criteria: {% assign late_tasks = tasks | where: "late", true %} {% for task in late_tasks %}

Task "#{{ task.name }}" is due on #{{ task.date }}

{% endfor %} Using filters is just a matter of "piping" variables into them. This means **you can chain filters together** to achieve the desired result in just one line of code. Check out this example that calculates a percentage: {% assign value = 20.0 %} {% assign total = 50.0 %} {% assign percentage = value | divided_by: total | times: 100 | round %}

#{{ value }} is #{{ percentage }}% of #{{ total }}

Result: 20 is 40% of 50 Liquid has more than 50 filters available to help us with all kinds of data manipulation, doing math, handling dates, filtering lists and a lot more. Be sure to check the resources below for the full documentation of Liquid and some of TRMNL's own custom filters. ## **More Resources** - [Official Liquid Documentation](https://shopify.github.io/liquid/): the official Liquid documentation, it covers all the topics on this guide in detail and also has the full list of supported filters. - [TRMNL Custom Liquid Filters](https://help.trmnl.com/en/articles/10347358-custom-plugin-filters): a growing list of custom Liquid filters developed by and available only on TRMNL. --- # Importing and exporting private plugins There's a rich ecosystem of user-developed plugins outside of our [official integrations](https://trmnl.com/integrations), and it's very simple to share and utilize these handcrafted private plugins. We've developed a standard ZIP file format that plugin developers (dare we say, artisans) can follow. It conveniently combines plugin settings, custom fields, and markup into one archive. ## Exporting a private plugin Exporting is as simple as clicking **Export** on the settings page for the specific plugin. A ZIP file will be downloaded with the entire contents of your plugin, including its settings, custom fields, and markup. ## Importing a private plugin On the [Private Plugin settings page](https://trmnl.com/plugin_settings?keyname=private_plugin), simply click **Import new** and select the ZIP file to import. A new plugin will automatically be created and added to your playlist. ## Plugin ZIP file contents The ZIP archive of a private plugin contains the following flat list of files (no subdirectories): settings.yml full.liquid half_horizontal.liquid half_vertical.liquid quadrant.liquid `settings.yml` is required. The `.liquid` files correspond to the individual [Liquid HTML templates](https://docs.trmnl.com/go/private-plugins/templates) for each viewport. The maximum template file size is 1 MB. ## settings.yml See [this article](https://intercom.help/trmnl/en/articles/10513740-custom-plugin-form-builder) for details on configuring `custom_fields`. --- # required: name: My Awesome Plugin strategy: polling # polling | webhook | static refresh_interval: 60 # minutes; 15 | 60 | 360 | 720 | 1440 # polling strategy only: polling_url: https://example.com polling_headers: '' polling_verb: GET # GET | POST # static strategy only: static_data: '{"foo": "bar"}' # JSON string # generic options: no_screen_padding: 'no' # 'no' | 'yes' (quotes are necessary!) dark_mode: 'no' # 'no' | 'yes' (quotes are necessary!) # custom plugin form fields (optional, see docs): custom_fields: - keyname: units name: Units field_type: select options: - Metric - Imperial --- # Eight Sleep --- # Frequently Asked Questions **How much does TRMNL cost?** Customers pay a 1-time fee to buy a device, then get lifetime access to software updates and plugins. **Is there a recurring subscription fee?** Nope. Buy a TRMNL device once, then use it free forever. Every device enables lifetime access to our web portal, but you can also leverage our open source platforms to point devices at your own servers if desired. **How big is a TRMNL device?** The screen is 7.5 inches diagonal and the full device is around 4.5x7 inches. It sits at a 15 degree angle on your desk or counter, or can be hung directly on a wall. **What can I do with TRMNL?** We provide dozens of native plugins for personal and professional users. Whether you want to view your calendar, upcoming weather, business KPIs, or just a random Wikipedia article, TRMNL's growing plugin directory has everything you need. Users with our developer mode enabled may also build plugins, either for themselves or publicly listed in our directory. --- # GitHub Contributors For a quick snapshot of your team's interactions with a given repository, the GitHub Contributors plugin can be set up in just a couple minutes. ## **Step 1 - Visit the GitHub Contributors plugin** Inside TRMNL, navigate to Plugins > GitHub Contributors. ## **Step 2 - Configure Plugin** First provide the repository you'd like to monitor, along with a "Fine Grained Access Token" from your GitHub account. This access token only needs the "Read Repository Metadata" scope and can be created here: https://github.com/settings/tokens?type=beta After generation your GitHub should look like this: The access token will be visible only once, at the top of this screen. Provide it inside TRMNL and click Save. ## **Step 3 - You're Done!** Based on your device's refresh settings, you'll begin seeing contributor stats very soon. Stay focused. ​ --- # RSS Feed Setting up your favorite RSS Feed is easy with TRMNL. ## Step 1 - Visit the RSS Feed Plugin Inside TRMNL, navigate to Plugins > RSS and create a plugin Name. ## **Step 2 - Add RSS Feed URL** Drop in the URL of the RSS Feed you'd like to point to. XML files [like this one](https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=100003114) will work best. ## **Step 3 - Press Save and You're Done!** Based on your device's refresh settings, you'll begin seeing RSS feed data very soon. ## **Popular RSS Feeds** We curated a list of some of the most popular RSS feeds on the internet, [check it out here](https://docs.google.com/spreadsheets/d/1EyQdmrFG9DHTK1VcGPV3BHi2jM4rtSjde95OR_shDHk/edit?gid=0#gid=0). Stay focused. --- # Google Analytics Connecting Google Analytics to TRMNL just takes a few seconds and doesn’t require any coding. ## Step 1 - Visit the Google Analytics plugin Inside TRMNL, navigate to Plugins > Google Analytics. There you need to: - Link your Google Analytics account - Name your plugin (usually the name of your website) - Choose a [Google Property ID](https://developers.google.com/analytics/devguides/reporting/data/v1/property-id) - Select a lookback period (Type) of 7, 30, or 90 days ## Step 2 - You're Done! Based on your desired refresh settings, TRMNL will begin showcasing analytics data from account very soon. Stay focused. --- # Gumroad Connecting your Gumroad to TRMNL takes just a few seconds. ## Step 1 - Visit the Gumroad Plugin Inside TRMNL, navigate to Plugins > Gumroad. ## **Step 2 - Connect Gumroad** Click to "Connect with Gumroad" on the right side of the screen, review TRMNL applications then select "Authorize" ## **Step 3 - Configure Plugin** Give your channel plugin a name then select a "Lookback Period." This is simply the number of days you prefer TRMNL to tally sales count and sales value. ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Gumroad data very soon. Stay focused. --- # Email Meter Measuring how you leverage email requires 2 things: a free or paid Email Meter account, and a little bit of (safe, ethical) web browser hacking. To make a free Email Meter account, visit [emailmeter.com](https://emailmeter.com/). Then follow along below. ## **Step 1 - Visit the Email Meter Plugin** Inside TRMNL, navigate to Plugins > Email Meter. ## **Step 2 - Find your Email Meter API Token** Now for some web hacking. Don't worry, no coding experience is required. Using either Chrome or Firefox (web browser), log into Email Meter and visit your dashboard. It will look something like this: Next, open your browser's Developer Tools via the browser's toolbar, navigating to View > Developer > Developer tools like so: A new section should appear in your browser. It may be along the bottom, the right side, or even popped out as a new tab. Click on the "Network" tab of this new pane. Now, reload the web page. You should see a lot of text populate this Network tab. After the page loads, type "team-labels" in the Filter field: Click on the result with the (orange) graphic, which will also have a Method value of "GET + Preflight" in one of the table columns to the right. On the new 'Headers' tab, scroll down to the Request Headers section and find the "Authorization" row. Copy the long string of characters after the word "Token" from this row. Note that you may need to click open to expand the Request Headers section if you don't see these rows of content. ## **Step 3 - Configure Plugin** Back inside TRMNL, paste your API token into the API Token field: Optionally give your Email Meter plugin a name, then click Save. ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing Email Meter analytics very soon. **Note**: the Email Meter team is aware of our integration and they are considering a simpler solution to connecting your account. Also, your API token may expire automatically for security purposes. If you notice incorrect stats, repeat Steps 2-3 above. Stay focused. --- # YouTube Analytics Connecting your YouTube channel to TRMNL takes just a few seconds and is done securely via our adherence to [Google's API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy). ## **Step 1 - Visit the YouTube Analytics Plugin** Inside TRMNL, navigate to Plugins > YouTube Analytics. ## **Step 2 - Connect YouTube** Click to "Connect with YouTube" on the right side of the screen, then select an email account that owns your preferred channel and approve access to TRMNL. Note: only channel "owners" can authorize a 3rd party app. Shared YouTube Studio accounts will not appear during this step. ## **Step 3 - Configure Plugin** Give your channel plugin a name label (in case you want to connect multiple channels), then select a "Lookback Period." This is simply the number of days you prefer TRMNL to tally into summary stats such as views, comments, and subscribers gained. ## **Step 4 - You're Done!** Based on your device's refresh settings, you'll begin seeing YouTube Analytics very soon. Stay focused. --- # Statuspage --- # Salesforce --- # Shopping List Don't forget a single thing (especially toothpaste) on your next grocery run with our native Shopping List plugin. ## Step 1 - Visit the Shopping List Plugin Inside TRMNL, navigate to Plugins > Shopping List. ## Step 2 - Configure Plugin Add as many items as you'd like. We actually encrypt these by the way. [**Coming Soon** - TRMNL CLI with command `trmnl go shopping_list add milk` (to add milk to your Shopping List, from your machine's native terminal!] ## Step 3 - You're Done! Based on your device's refresh settings, you'll see changes to your Shopping List very soon. Stay focused. --- # Todo List Stay focused on important tasks with our native Todo List, a simple 2-column list of "Doing" and "Done" tasks. ## Step 1 - Visit the Todo List Plugin Inside TRMNL, navigate to Plugins > Todo List. ## Step 2 - Configure Plugin Input a few todos into the "Doing" list, separate by a hard return (line break). Optionally provide "Done" tasks to get this deep work party started. ## Step 3 - You're Done! As the days pass by, feel free to come back and move items from "Doing" to "Done" to remind yourself of everything you've accomplished. Based on your device's refresh settings, you'll see changes to your Todo List very soon. Stay focused. --- # Upcoming Movies A casual feed of upcoming movies is just a couple clicks away. Credit to TMDB ([The Movie Database](https://www.themoviedb.org/)) for providing this data. ## Step 1 - Visit the Upcoming Movies Plugin Inside TRMNL, navigate to Plugins > Upcoming Movies. ## **Step 2 - Configure Plugin** First set your region / country on the right hand side to filter movies that are coming out in your area. Next set a "Filter By" preference. If you choose "Now Playing," movies that come out as recently as yesterday will be returned on your device. If you choose "Upcoming," movies that come out today or later will be shown. ## Step 3 - You're Done! Based on your device's refresh settings, you'll begin seeing new movies soon. Stay focused. --- # beehiiv Connecting beehiiv to TRMNL just takes a few seconds and doesn’t require any coding. ## Step 1 - Visit the beehiiv plugin Inside TRMNL, navigate to Plugins > beehiiv. There you need to - Name your plugin (for example the name of your newsletter) - Add beehiiv API key - Add beehiiv Publication Id ## Step 2 - Connect beehiiv Inside beehiiv, navigate to Settings > Integrations > API. There you will see Publication ID and API Keys ## Step 3 - Create new API Key To create a new API just select New API key then Copy the New API Key ## Step 4 - Configure Plugin Back inside TRMNL, paste this API key in the configuration form then paste your API V2 key give this plugin a name. Click Save. ## Step 5 - You're Done! Based on your desired refresh settings, TRMNL will begin showcasing useful highlights from your beehiiv account very soon. Stay focused. --- # Readwise Connecting Readwise to TRMNL takes just a few seconds and does not require any coding. ## **Step 1 - Visit the Readwise plugin** Inside TRMNL, navigate to Plugins > Readwise. ## **Step 2 - Connect Readwise** Inside Readwise, navigate to [Access Tokens](https://readwise.io/access_token). Click "Get Access Token" (or copy an existing one, if available) and you'll be presented a long string of characters. Copy this to your clipboard. ## Step 3 - Configure Plugin Back inside TRMNL, paste this value in the configuration form and (optionally) give this plugin a name. Click Save. ## **Step 4 - You're Done!** Based on your desired refresh settings, TRMNL will begin showcasing useful highlights from your Readwise account very soon. Stay focused. --- # Simple Analytics Connecting your privacy-first [Simple Analytics](https://www.simpleanalytics.com/?referral=guwiq) (referral link) account to TRMNL takes just a few seconds. ## **Step 1 - Visit the Simple Analytics plugin** Inside TRMNL, navigate to Plugins > Simple Analytics. ## **Step 2 - Connect Simple Analytics** Log into your Simple Analytics account and visit [Account Settings > API](https://dashboard.simpleanalytics.com/account#api). Click "Create a key," then set your company name if you haven't already. A key will be created and will look like this: Copy your key beginning with `sa_api_key_***` and head back to TRMNL. ## **Step 3 - Configure Plugin** 1. Paste your Simple Analytics API Key into the "API Key" field of the plugin settings 2. Add your website's "domain." This is visible from your Simple Analytics dashboard, and is likely just your website without any "http" or "www" prefix (below) 3. Choose a lookback period, between 1 day and 1 month, for data aggregation Click "Save" to activate this plugin. ## **Step 4 - You're Done!** Based on your desired refresh settings, TRMNL will begin aggregating useful stats from Simple Analytics including a pageview histogram, unique visitor count, and [median time on page](https://docs.simpleanalytics.com/explained/time-on-page) very soon. Stay focused. --- # Days Left (This Year) Enabling our "days left" progress bar to visualize the time left in the current year is a simple and fun way to remind yourself of what's important. Inside TRMNL, head to Plugins > Days Left (This Year), enable the plugin, and hit save. Going forward, TRMNL will produce a progress bar like this one below to reflect the percentage completion between January 1st of this year and December 31st. --- # GitHub Commit Graph Connecting GitHub to TRMNL is a simple and fun way to visualize your coding progress. Inside TRMNL, head to Plugins > GitHub Commit Graph and input your GitHub "slug" username (ex: github.com/**ryanckulp**), then enable the plugin and hit save. Going forward, TRMNL will produce a graph like this one below to reflect your coding frequency and intensity. The darker the square, the more commits you made on that day. If you have any questions about this plugin, let us know in the chat widget or via email.