Using CSS media queries to target monochrome e-paper

arbayi1 pts0 comments

Using CSS media queries to target monochrome e-paper — GEA Blog

Skip to content

← Blog<br>Engineering

Using CSS media queries to target monochrome e-paper

Armağan Amcalar · Jun 23, 2026 · 7 min read

Counter

Reset

Monochrome

Here is a counter. It counts up, it counts down, and you can reset it. It is the most boring app I can<br>think of, and I picked it on purpose.

Because the interesting thing is not the app. The interesting thing is that the exact same app —<br>the same TSX, the same styles.css — is running on two devices that have almost nothing in<br>common. One is a full-color Linux panel: emissive, backlit, touch-driven. The other is a monochrome<br>e-paper screen: reflective, no backlight, no touch at all. You operate that one with the physical buttons<br>on the board.

Two pieces of glass that could not be more different. One codebase. No fork, no #ifdef, no<br>second project. That is the whole point of this post.

This is the reactive-counter example, and it is the follow-up I promised in<br>the last one — where I argued that you don't interpret CSS on a tiny chip, you compile<br>it. Same idea here, pointed at a harder question: what happens when the chip's screen changes out from<br>under your app?

The component owns its state and its behavior

At the heart of gea is a reactive component. It owns its state. It owns its behavior. You write everything<br>in TypeScript and it compiles to native C++ — no interpreter, no VM left on the device. Here is the whole<br>counter, near enough verbatim:

import { Display, ReactiveComponent, mount } from 'gea-embedded'<br>import './styles.css'

// E-paper refresh policy (no-op on boards without an e-paper panel). The<br>// counter has no animation — every update uses the slow, crisp vendor waveform.<br>Display.setEpaperRefreshConfig({ fastStreakWindowMs: 0 })

export class App extends ReactiveComponent {<br>count = 0<br>increment() { this.count = this.count + 1 }<br>decrement() { this.count = this.count - 1 }<br>reset() { this.count = 0 }

keydown(keyCode: number) {<br>// Hardware buttons (e-paper BOOT/PWR) and keyboards: ArrowUp / ArrowDown.<br>if (keyCode === 38) this.increment()<br>else if (keyCode === 40) this.decrement()

template() {<br>return (<br>"counter-app" onKeyDown={event => this.keydown(event.keyCode)}><br>"counter-title">Counter<br>"counter-card">"counter-count">{this.count}<br>"counter-controls"><br>"counter-button counter-button--minus" onClick={() => this.decrement()}><br>"counter-minus-label">-

"counter-button counter-button--plus" onClick={() => this.increment()}><br>"counter-plus-label">+

"counter-reset" onClick={() => this.reset()}><br>"counter-reset-label">Reset

mount(App)

That's the app. count is reactive state — assign to it and the view that depends on it<br>re-renders. increment, decrement and reset are the behavior, living<br>right on the component. The template() is JSX. Nothing in this file knows or cares what kind<br>of screen it will land on. That ignorance is the feature.

The look is defined entirely in CSS

The look lives entirely in styles.css. gea supports any TrueType font, so the counter loads<br>Oswald straight from a .ttf with @font-face — no system fonts assumed, no<br>special pipeline:

@font-face {<br>font-family: 'Oswald';<br>src: url('../../assets/fonts/Oswald-Regular.ttf');

.counter-app {<br>font-family: 'Oswald';<br>background-color: #07111f;<br>padding: clamp(10px, 3.5vh, 54px);<br>/* flex column, vh/vw sizing throughout */

.counter-count {<br>font-size: clamp(46px, 17vh, 220px);<br>color: #67e8f9;

.counter-card {<br>background-color: #0f172a;<br>border-color: #1e293b;

This is the part people miss about responsive design on hardware. The sizes are not pixel constants; they<br>are clamp() over vh and vw. So the layout doesn't target one board<br>— the same stylesheet fits a watch-sized AMOLED, a 7-inch panel, and the e-paper device, because the type<br>and the padding scale to whatever viewport the panel reports. One layout, every screen. No per-device fork<br>to keep the proportions right.

On a backlit color display this reads exactly as you'd want: cyan numerals on a near-black field, a card<br>with a faint border. Tap plus, the count goes up; tap reset, it's zero. So far there is nothing to write<br>home about — it's just a UI.

Now move it to the e-paper.

On e-paper, one media query flips the theme

An e-ink panel has no backlight. It builds its image out of reflected light, the way ink on paper does,<br>and these panels are 1-bit: black or white, nothing in between. A dark theme is the wrong instinct here.<br>Cyan on near-black would dither into a field of gray speckle, and the whole thing would read muddy and<br>soft instead of sharp.

You have two honest options. You can fork the project and maintain an e-paper-flavored counter forever. Or<br>you can tell the truth about where the app is running and let the stylesheet adapt. gea evaluates media<br>queries at runtime against the real panel — including (monochrome). So you write the<br>override right next to the dark theme:

/* 1-bit panels (e-paper): pure black-on-white reads crisply with no dither<br>speckle,...

counter paper count reset monochrome color

Related Articles