Back to flin
flin

The Theme Toggle Bug

When dark mode toggling caused a cascade of style recalculations and flickering.

Thales & Claude | March 25, 2026 7 min flin
flinbugthemedark-modetogglecss

Some bugs resist elegant solutions. You approach them with architectural ideas, compiler modifications, and runtime enhancements. You implement complex solutions. They do not work. You try different approaches. They fail too. And then, after five to seven sessions of escalating complexity, you discover that the fix is two lines of code that use nothing but standard web APIs.

The theme toggle bug in FLIN's modern-notes application was this kind of bug. It consumed more engineering time than any single feature in the application, and the final fix required zero changes to the compiler, zero changes to the VM, and zero changes to the renderer. The solution was hiding in plain sight, disguised by its simplicity.

The Symptom

The modern-notes application had three interactive features:

FeatureStatusMechanism
Language switchWorkingdata-flin-bind attribute
Hero dismissWorkingdata-flin-if conditional
Theme toggleBrokendata-theme attribute on root div

The language switch and hero dismiss worked because they used FLIN's client-side reactivity system. When a variable changed, the runtime's _updateBindings() function scanned the DOM for elements with data-flin-bind and data-flin-if attributes and updated them.

The theme toggle did not work because it relied on a different mechanism entirely: a data-theme attribute on the root <div> element. Clicking the moon icon was supposed to change data-theme="light" to data-theme="dark", which would trigger CSS rules that swap the color scheme. But _updateBindings() had no concept of attribute bindings -- it only handled text content and visibility.

The Architecture Gap

The root of the problem was that FLIN's client-side reactivity was limited to two patterns:

  1. Text bindings: <span data-flin-bind="variable">value</span> -- when variable changes, the text content updates.
  2. Conditional visibility: <div data-flin-if="condition">content</div> -- when condition is false, the element is hidden.

The theme toggle needed a third pattern: attribute bindings -- when a variable changes, an attribute on an element updates. This pattern did not exist in FLIN's runtime.

The obvious solution was to implement it: add data-flin-attr-* markers during server-side rendering, then extend _updateBindings() to scan for these markers and update the corresponding attributes. This would be a clean, systematic solution that generalizes to any attribute binding.

We tried it. It did not work as simply as expected.

The Failed Approaches

Attempt 1: Ternary Expressions in Component Props

The first approach was to use conditional rendering to show different icons:

flin<WeatherIcons name={theme == "light" ? "moon" : "sun"} />

This ran into complex scope propagation issues. The ternary expression needed to evaluate at render time with the current theme value, but component props were evaluated in a different scope than the page. The debugging took two days and produced no working result.

Attempt 2: Semicolon-Separated Statements

The second approach tried to combine variable assignment with DOM manipulation in a single click handler:

flinclick={theme = "dark"; document.querySelector('.flin-theme').dataset.theme = "dark"}

This failed immediately: FLIN's lexer does not support semicolons in inline expressions. The event handler attribute is parsed as a single expression, and semicolons are not valid within expressions.

Attempt 3: querySelector with Quotes

The third approach tried direct DOM manipulation without semicolons:

flinclick={document.querySelector('.flin-theme').dataset.theme = theme = "dark"}

This also failed: the single quotes inside querySelector('.flin-theme') broke the lexer. FLIN's lexer treats single quotes as the start of a character literal, not as string delimiters within expressions. The parser could not make sense of the attribute value.

Each failed approach took hours of investigation, testing, and debugging. Three different strategies, three different failure modes, zero working solutions.

The Breakthrough: Assignment Chaining Without Quotes

The working solution arrived not from a clever architectural insight but from a close reading of what FLIN's lexer could actually handle. The constraints were:

  1. No semicolons in expressions
  2. No single quotes in expressions
  3. No multi-statement handlers

But FLIN's lexer did support: - Dot notation property access (a.b.c.d) - Assignment chaining (a = b = "value")

The solution was to navigate to the target element without querySelector and use assignment chaining to update both the FLIN variable and the DOM attribute in a single expression:

flin// The working fix
click={document.body.firstElementChild.dataset.theme = theme = "dark"}

This single expression does three things: 1. Sets theme = "dark" (FLIN runtime variable) 2. Returns "dark" (assignment expression value) 3. Assigns "dark" to document.body.firstElementChild.dataset.theme (DOM attribute)

No semicolons. No quotes. No querySelector. Just dot notation and assignment chaining -- both of which the FLIN lexer handles correctly.

The .flin-theme div is the first child of <body>, so document.body.firstElementChild is equivalent to document.querySelector('.flin-theme') in this layout. The full implementation uses two handlers, one for each direction:

flin<span class="icon-light"
  click={document.body.firstElementChild.dataset.theme = theme = "dark"}>
    <WeatherIcons name="moon" size="20" stroke="2" />
</span>

<span class="icon-dark"
  click={document.body.firstElementChild.dataset.theme = theme = "light"}>
    <WeatherIcons name="sun" size="20" stroke="2" />
</span>

Both icons are always present in the DOM. CSS controls which one is visible based on the current theme:

css.icon-light, .icon-dark { display: none; }
[data-theme="light"] .icon-light { display: flex; }
[data-theme="dark"] .icon-dark { display: flex; }

How It All Connects

The complete flow when a user clicks the moon icon:

User clicks moon icon
    |
    v
click handler executes:
  document.body.firstElementChild.dataset.theme = theme = "dark"
    |
    v
1. theme = "dark" (FLIN runtime state updated)
2. data-theme = "dark" (DOM attribute updated)
    |
    v
CSS [data-theme="dark"] rules activate:
  - .icon-dark becomes visible (sun icon)
  - .icon-light becomes hidden (moon icon)
  - CSS variables switch to dark theme colors
    |
    v
Theme change is instant, no page reload

The Irony of Simplicity

The final fix modified exactly two lines of code in one file. Zero compiler changes. Zero VM changes. Zero renderer changes. Zero new opcodes. Zero new runtime functions.

After five to seven sessions of attempting complex solutions -- ternary expression evaluation, attribute binding systems, scope propagation fixes -- the answer was to use JavaScript's existing assignment chaining with the browser's existing DOM API. The FLIN lexer already supported everything needed; we just had to work within its constraints instead of trying to expand them.

This is not always the right approach. For FLIN v2, implementing proper data-flin-attr-* attribute bindings would provide a cleaner, more general solution. Developers should not need to know about document.body.firstElementChild to toggle a theme. But for v1.0, the quick fix achieved the same result with zero risk of introducing compiler regressions.

The Broader Lesson

The theme toggle bug teaches a lesson about the relationship between language constraints and creative solutions.

FLIN's lexer has limitations: no semicolons in expressions, no single quotes, no multi-statement handlers. These limitations exist for good reasons -- they keep the lexer simple, the parser predictable, and the syntax consistent. But they also create constraints that force developers (including us, the language creators) to find alternative approaches.

The constraint of "no semicolons in expressions" forced us away from multi-statement handlers and toward assignment chaining -- which is actually a more elegant solution. The constraint of "no single quotes" forced us away from querySelector and toward DOM traversal -- which is actually more robust (it does not depend on class names).

Every programming language imposes constraints. The quality of a language is partly measured by whether its constraints guide developers toward better solutions or merely frustrate them into worse workarounds. In this case, FLIN's constraints guided us to a solution that was simpler, more robust, and more portable than any of the "proper" solutions we attempted.

Sometimes the best fix is the one that requires zero changes to the system and instead works with what already exists.


This is Part 165 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [164] Fixing Library Function Resolution - [165] The Theme Toggle Bug (you are here) - [166] The Entity .get() Method Bug

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles