The Hidden Contract Between Type Systems and UI Themes

Jaeyeol Lee

Hatched by Jaeyeol Lee

Jun 02, 2026

9 min read

34%

0

A Small CSS Line That Reveals a Big Design Truth

What do a programming language and a tooltip theme have in common? At first glance, almost nothing. One decides whether code is valid, the other decides whether a popup looks light or dark. Yet both are built on the same quiet idea: the system is only as flexible as the contracts it allows you to express.

That is the real tension hiding behind both type relationships and theme imports. Developers often ask for power in the wrong place. They want more features, more customization, more room to bend the rules. But the deeper question is not whether a system can do more. It is whether the system can make more things possible without making the meaning of anything fuzzy.

A single line like import 'tippy.js/themes/light.css'; looks trivial. It is not. It reveals an approach to extensibility where behavior stays stable while presentation changes. That is the same kind of boundary a well designed type system tries to create: let one layer vary, but keep another layer trustworthy. The surprise is that subtyping and theming are both answers to the same problem: how to add variation without losing coherence.


Variation Without Chaos

Every software system faces the same pressure. Users want to customize it, extend it, and combine it with other things. But the more freedom a system grants, the more likely it is to become ambiguous, brittle, or impossible to reason about. A type system and a design system each solve this in their own domain by drawing a line around what counts as compatible.

In code, compatibility is about what values can safely stand in for other values. In UI, compatibility is about what styles can wrap around a component without breaking its structure. A light theme, a dark theme, a compact theme, each is not just decoration. Each is a promise that the component will still function while its appearance shifts.

This is why the tiny import statement matters. It implies that theming is not a rewrite, not a fork, and not a special case. It is a plug in relationship. The component knows how to accept a theme the way a type knows how to accept a subtype. The outer form changes, the inner expectations remain intact.

Good abstractions do not erase differences. They make differences safe.

That sentence could sit above both a language spec and a UI architecture guide. The best systems are not the ones that force everything into sameness. They are the ones that define a precise corridor in which variation can happen predictably.


The Real Question: What Must Stay True?

The common mistake is to treat flexibility as a binary. Either a system is rigid, or it is open. Either it has strict types, or it lets anything through. Either it has a fixed theme, or it allows endless visual customization. But that framing misses the point.

The real design question is: what must stay true even when the surrounding surface changes?

For subtyping, the answer is something like this: the subtype must preserve the guarantees the parent type depends on. If a function expects a bird, and a penguin is passed in, the question is not whether the penguin is a bird in some abstract sense. The question is whether the function can still safely do what it promised to do with a bird.

For theming, the answer is parallel. If a component expects a light theme, the question is not whether the theme is visually pleasing in isolation. The question is whether the component’s layout, contrast, spacing, and interaction states still make sense under that theme.

This reveals a useful mental model: compatibility is not identity, it is preserved intent. A subtype preserves the behavioral intent of a type. A theme preserves the interaction intent of a component. The shape can differ, but the role must remain legible.

Consider a button component. Its role is to invite action, signal affordance, and remain legible under different contexts. A theme may alter the color palette, border radius, font weight, or shadow. But if those changes make the button look disabled when it is not, or make the label unreadable, the theme has broken the contract. It has not merely changed presentation. It has altered meaning.

This is exactly where many systems fail. They allow extension at the level of appearance or inheritance, but they do not define the invariants that keep the extension honest.


When Flexibility Becomes a Lie

The seductive trap in both software design and language design is to confuse “can be made to work” with “is truly compatible.” A system can often be coerced into accepting almost anything. That does not mean the system remains understandable.

A poorly designed subtype relationship can smuggle in broken assumptions. A function may rely on a capability that the subtype does not really support. Suddenly the abstraction fractures, and the code still compiles in spirit but fails in reality. In the UI world, a poorly scoped theme can do the same thing visually. It can satisfy the import statement but violate the component’s underlying structure.

Imagine a tooltip that needs clear boundaries, readable contrast, and a predictable offset from the target element. A theme that changes the typography so aggressively that the popup overflows its container is not a mere style variation. It is an incompatible behavior hidden behind a visual layer. The system has accepted the theme, but the user experience has not survived it.

This is why the best abstraction boundaries are often less glamorous than the most flexible ones. They say no in precise ways. They do not try to model every possibility. Instead, they define a narrow set of transformations that are known to preserve the core purpose.

That narrowness is not a weakness. It is what makes composition trustworthy.

Here is a useful rule:

  1. If the extension changes what the system means, it is not extension. It is mutation.
  2. If the extension changes only how the system appears or which valid operations it supports, it may be safe.
  3. If you cannot state the invariant, you cannot tell the difference.

This rule applies equally to type hierarchies and theme APIs. The absence of a clear invariant turns flexibility into guesswork.


A Better Framework: Meaning, Behavior, Skin

To connect these ideas more deeply, it helps to separate software into three layers: meaning, behavior, and skin.

Meaning is what the thing is for. A tooltip explains, a button invites, a type promises a contract.

Behavior is what the thing can do while remaining itself. A button can be clicked, focused, disabled, or styled in different ways. A subtype can be passed where its parent is expected, as long as it preserves the promised operations.

Skin is the surface layer, the presentation, the exact visual or structural expression that can change without rewriting purpose.

A healthy theme system operates mostly at the skin layer. A healthy subtype relationship operates at the behavior layer. Both become dangerous when they start leaking into meaning.

This framework explains why the small import statement is a clue rather than a triviality. It suggests that theming is intentionally isolated. You do not rewrite the tooltip logic when you want a light appearance. You add a stylesheet that changes the skin while leaving behavior intact.

That is also the ideal aspiration of subtyping. The subtype should not force the program to reinterpret what the abstraction means. It should only refine or specialize the behavior in ways the caller can still rely on.

A concrete analogy helps here. Think of a restaurant menu. The dish name is meaning, the preparation is behavior, and the plating is skin. A vegan substitute may preserve the meal’s role as a satisfying entrée if it keeps the same timing, temperature, and portion logic, even if the ingredients differ. But if the dish is smaller, colder, or unexpectedly spiced, it may no longer serve the same purpose. The plate may look similar, yet the contract has changed.

Systems fail when they confuse skin with behavior, or behavior with meaning. Good design keeps these layers distinct enough to evolve independently.

The goal is not to eliminate change. The goal is to localize change so the right things can move without everything else becoming uncertain.


The Hidden Economy of Trust

Why does this matter so much? Because software is ultimately an economy of trust. Every abstraction asks the next layer to trust it. A caller trusts the type. A component trusts the theme. A team trusts the shared interface. A future maintainer trusts the contract written today.

This trust is not emotional, it is mechanical. If the contract is honest, engineers can build faster because they do not need to inspect every implementation detail. If the contract is vague, every integration becomes a detective story.

This is why seemingly tiny design decisions have outsized effects. A theme API that is just a CSS import may seem boring, but boring is often the sign of a stable contract. It says: apply this layer, and the rest still holds. Likewise, a subtype system that makes it easy to reason about substitution reduces cognitive load. It tells you what can safely stand in for what, without requiring a manual audit of every call site.

The most powerful abstraction is not the one that does the most. It is the one that makes the rest of the system easier to trust.

From this perspective, the attraction of both subtyping and theming is the same. They offer controlled promiscuity. They let a system accept variants, but only variants that preserve enough structure to remain predictable.

And that is the deeper insight: flexibility is valuable only when it is bounded by a clear promise. Without that promise, variation becomes noise.


Key Takeaways

  1. Separate meaning from skin. Decide what a system is, what it does, and what may change purely for presentation.
  2. Name the invariant explicitly. Before allowing a subtype or theme, state what must remain true for the abstraction to still work.
  3. Treat compatibility as preserved intent, not similarity. Two things can look alike and still be incompatible, or look different and still be safe substitutes.
  4. Prefer narrow, trustworthy extension points. The best APIs and type relationships constrain variation so composition stays predictable.
  5. Use breakage as a diagnostic. If a theme or subtype causes confusion, ask whether it changed behavior or meaning, not just appearance.

Designing for Safe Change

The next time you add a theme, introduce a subtype, or design an extension point, ask a more precise question than “Does this work?” Ask: what is the contract I am asking others to rely on? If you cannot answer that clearly, the system is probably borrowing trust it has not earned.

That is why the connection between type relationships and UI themes is more than a technical curiosity. Both are about making change legible. Both try to create a world where things can vary without becoming unrecognizable. And both succeed only when they protect the core promise while allowing the surface to evolve.

In the end, the most elegant systems are not those with the most freedom. They are the ones that know exactly where freedom belongs. A theme can change the lightness of a tooltip. A subtype can refine a concept without betraying it. In both cases, the real achievement is the same: the system remains itself while still making room for difference.

That is not just a design principle. It is a philosophy of trustworthy complexity.

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣