We Taught a $3 Chip to Run CSS

arbayi3 pts1 comments

We Taught a $3 Chip to Run CSS — GEA Blog

Skip to content

← Blog<br>Engineering

We Taught a $3 Chip to Run CSS

Armağan Amcalar · Jun 19, 2026 · 9 min read

front

back

right

left

top

base

This is real CSS from a gea example app:

@keyframes cube-spin {<br>0% { transform: rotateX(-18deg) rotateY(24deg) rotateZ(0deg); }<br>45% { transform: rotateX(44deg) rotateY(188deg) rotateZ(5deg); }<br>100% { transform: rotateX(342deg) rotateY(384deg) rotateZ(0deg); }

.cube-app {<br>background-image: linear-gradient(135deg, #12130f 0%, #171b14 45%, #251a15 100%);<br>perspective: 155vh;<br>perspective-origin: 50% 30%;

It spins a 3D cube — perspective projection, backface culling, translucent faces, and a grid floor<br>drawn with repeating linear gradients — at 42 frames per second on an AMOLED display.

ESP32 · 3D CSS cube · 60 fps

It runs on an ESP32-S3: a dual-core, 240 MHz microcontroller with 512 KB of internal SRAM that costs<br>a few dollars.

The real hardware: a Waveshare ESP32-S3-Touch-AMOLED-2.06 — a 2.06″, 410×502 CO5300 AMOLED<br>driven by an ESP32-S3. This is the chip the cube runs on.

This is the first post on the gea blog, so some context first. These boards are cheap and their<br>displays are good, but the firmware they usually ship with does not make the most of them: a typical<br>stock UI repaints the entire panel on every change and clocks it out over a slow link, so the screen<br>visibly redraws from the top down. That isn't a UI; it's a progress bar pretending to be one. The<br>bottleneck is rarely the chip. It is how the display is driven.

What gea is

gea is a framework for building apps for small devices — smartwatch-sized AMOLED screens, round<br>rotary dials, 7-inch panels — the way you build them for the web. You write TSX components with<br>reactive state, style them with plain CSS files, and the toolchain compiles the whole thing to native<br>C++. The binary flashed to the chip holds your app's logic as machine code, plus a rendering pipeline<br>closer in structure to a game engine than to a browser.

A modern browser engine is tens of millions of lines of code. gea's entire style and layout engine is<br>about six thousand lines of C++. It stays that small by implementing a focused slice of CSS — chosen<br>for broad impact, not for completeness. And that slice is compiled, not interpreted:<br>don't interpret CSS, compile it.

What ships to the chip

There is no CSS file on the device, and no CSS parser in the render loop. At build time, geatsc —<br>gea's TypeScript-to-C++ compiler — and the gea plugin read your stylesheets and emit a registration<br>for every rule; at boot, each value is parsed once into typed storage. From then on a rule isn't text,<br>it's typed fields. The .cube-app gradient, for one, lands in the node's<br>ComputedStyle as:

// linear-gradient(135deg, #12130f 0%, #171b14 45%, #251a15 100%)<br>uint16_t bg_gradient_from_color; // #12130f -> RGB565<br>uint16_t bg_gradient_mid_color; // #171b14<br>uint16_t bg_gradient_to_color; // #251a15<br>uint16_t bg_gradient_mid_stop; // 450 (45%, permille)<br>uint16_t bg_gradient_to_stop; // 1000 (100%)<br>int16_t bg_gradient_angle; // 1350 (135.0 deg, tenths)<br>uint8_t bg_gradient_from_alpha; // 255

Three colors packed to the panel's native RGB565, two stops in permille, one angle in tenths of a<br>degree — no linear-gradient(...) string on the device, and nothing to re-parse when a<br>frame is drawn.

That ComputedStyle is a fixed-layout struct of about 520 bytes — every supported property<br>pre-allocated as a typed field. Reading border-radius is a struct field read; setting<br>opacity writes a uint8_t. There is no per-property allocation, which matters<br>on a chip where the largest free block of internal SRAM at render time can be 8 KB.

From .cube-app to the chip: geatsc folds every CSS rule into the generated<br>program at build time, and each value is parsed once, at boot, into a typed field — no<br>stylesheet and no CSS parser ever ship to the device.

What the runtime supports

Handling color and width and calling it CSS support would be easy. The goal<br>was a subset large enough to build real interfaces — the CSS you actually reach for. Here is what the<br>runtime supports today:

Flexbox , implemented from scratch in about 1,300 lines: direction, wrap,<br>grow/shrink, gap, justify-content, align-items, the lot.<br>Plus a small grid mode (up to 8 tracks per axis), absolute/fixed positioning, and<br>z-index.

Selectors , matched at runtime against the live tree: classes, element names,<br>descendant and direct-child combinators, ::before/::after. Parsed<br>selectors are cached so matching never re-tokenizes a string.

Animations and transitions : @keyframes with delay, duration,<br>iteration counts, direction, fill modes, and cubic-bézier easing. A small animation engine ticks<br>once per frame and interpolates colors, angles, and scalars directly into the typed style fields.

Transforms in 2D and 3D : rotate/rotateX/Y/Z,<br>translate, scale, transform-origin, perspective,<br>perspective-origin, backface-visibility. This is what makes the cube a<br>cube and...

chip cube from perspective typed uint16_t

Related Articles