The request always arrives in the same shape. A campaign section carrying a partner brand’s identity, in the middle of a page that already has its own theme. Or a warm editorial block inside a product that is grey and blue. The piece looks different from the rest of the page, but on the inside it is the same buttons, the same fields and the same cards.

The usual answer from the design system is a parallel stylesheet. Classes along the lines of --on-dark appear, plus a button variant for dark backgrounds, another one for form fields, and a context rule for every component that might end up inside the block. In practice that means maintaining two versions of every component, reviewing every change twice, and finding out in the third quarter that the campaign variant never got the focus state that was fixed in the main one.

The alternative is to change where the theme lives. If the theme is a property of a container rather than of the page, the campaign block does not need different components: it needs to declare that a different colour contract applies inside it. I call that container a theme island.

Two theme islands side by side on the design system page. The one on the left, labelled theme="promo", is dark brown with a golden accent; the one on the right, labelled theme="editorial", is beige with a brown accent. Each shows its three surfaces, base, subtle and accent, plus three buttons and a form field. The surfaces and the field take the theme colours; the Primary and Accent buttons keep the ones from the base scheme, and the Accent button comes out lime in both islands.

An island is a subtree with its own colour contract

The definition is short: an element marks in the DOM which theme applies from there on, and everything hanging below it adopts that theme without knowing it sits inside an island. A button inside the campaign island paints itself with the campaign colours and its source code does not change by a single line. Neither does the card containing it, nor the form field next to it.

What makes the model useful is that it introduces no new mechanism. The global theme of this site is exactly the same mechanism applied to the <html> element, which in this repository goes by the island name page. The page is island number zero. When the «Café Ácido» section of my profile page declares its editorial theme, it is not using an exception to the system: it is doing what the root does, one level further down. Islands nest, and each one only redefines what it declares; the rest it inherits from the island above.

That uniformity has a practical consequence you notice while debugging. There is no code path for “the theme of the page” and another one for “the theme of this block”, so there are no two places where they can drift apart.

The «Café Ácido» section of the About page, captured twice: on the left with the page in light theme, on the right with the page in dark theme. The section looks identical in both, with its beige background, brown heading and black button, while the text and background around it do change from one case to the other.

What stops costing money

What justifies the pattern to whoever pays for the hours is the maintenance line item that disappears.

It starts with context variants. A system with forty components that solves dark backgrounds by duplicating variants has eighty pieces to maintain, and every fix has to be applied twice or explicitly waived. With islands, the figure goes back to forty. That saving is invisible on the day the pattern ships; it shows up in every fix over the following two years, split into pieces too small to appear in any report.

It continues with who executes the request. Adding an island theme means writing a block of sixteen colour declarations and one line in the theme registry. No component file is opened, so the change cannot break anything outside the pages that use it. That moves the task: it stops being a modification of the design system, with its review and its queue, and becomes configuration.

Contrast, which is the part that usually degrades in campaign work, gets decided once. In each island theme, the background and the text of a surface are declared together, in the same block and in plain sight. Verification happens over those pairs when the theme is created, not over every screen where the block will appear. It is still mandatory work, but it is a bounded ten-minute task instead of an audit per campaign.

And white labelling turns out to be the same mechanism. A product sold with the client’s visual identity, or a widget embedded on a third party’s site, pose the same problem as the campaign: a subtree with a different colour contract. If the pattern is already in place for the promotional section, multi-brand support stops being a project and becomes one more token file.

A new theme in an afternoon

Of the whole setup, the part that changed how I work the most is the test loop. This site carries a theme panel that walks the DOM looking for islands and builds a selector for each one. From there you can change the theme of any block on the page live, the root included, and see straight away how the real content looks with its own text and images.

Below the list there are two more things. A JSON object with the theme each island currently has, which is the configuration of the view ready to copy, and a theme builder with one colour picker per each of the sixteen tokens. When you are done, the panel hands back the CSS block of the theme to paste into the island themes file.

The theme panel open over the design system page, with the two example islands visible behind it. The panel lists the six islands in the view, page, pb theme panel, pb mobile menu, ds theme promo, ds theme editorial and footer, each with its theme selector. Below, a JSON block with the current theme of each island and the start of the theme builder. The panel is captured on the Spanish version of the site, so its labels read in Spanish.

With that tool, trying out a campaign identity over real content no longer needs a branch, a preview deployment and a meeting. It happens on the page itself, the resulting block gets copied, and the engineering work is pasting it. What used to be argued over a static mockup gets decided on the page that will actually ship.

Underneath there is a small component and a token chain

The component that activates an island takes little more than fifty lines and its runtime work is none. It takes the theme, resolves two data attributes and writes them onto the element wrapping the content:

<ThemeIsland theme="editorial" name="cafe-acido" as="section">
  <!-- any content: it takes the theme on its own -->
</ThemeIsland>

Out of that comes a <section> with data-theme="light" and data-island-theme="editorial". There is no client JavaScript, no framework context and no state to synchronise: the whole component reduces to those two attributes written into the HTML at render time.

The real work happens underneath, in the four-layer token architecture I described in «When a token system needs four layers». An island theme touches neither the primitive layer nor the semantic one: it reassigns role tokens only, which are the ones built to switch. Because components consume roles and never values, the reassignment reaches the last nested element without any of them being aware.

The concrete model is base scheme plus surfaces. The island theme declares whether it starts from light or dark, and with that single declaration it inherits color-scheme, the action family, the focus ring and the rest of the system, all already verified. On top of that base, one rule overrides the surfaces only: background, text, icon and border of the base, subtle and accent variants, plus the modal dialog. Sixteen tokens, and that is where the whole customisation surface of a theme ends.

How the cascade resolves, why the adoption rule uses inherit as its fallback value and how the flash on load with a stored theme is avoided is enough material for its own article, and I will write that one separately.

What it costs and where it stops being worth it

The decision that shapes the result the most is that one, limiting the island theme to surfaces. The alternative was letting each theme declare the full set of role tokens, actions and focus included, so a button inside the campaign island could carry the partner’s colour. I discarded it because it turns every new theme into a full accessibility review: hover and active states, contrast of the text over each button variant, and visibility of the focus ring over the new background. Multiplied by every campaign, no team sustains that.

The price of having discarded it is concrete, and worth stating before whoever builds the first campaign discovers it: inside an island, buttons come out with the colours of the base scheme, not those of the theme. It is visible in the first screenshot: the accent button is lime in both islands, and the primary one only changes because the scheme changes, light on the brown and black on the cream. Neither of them takes the golden colour of the theme. When a campaign genuinely needs its own action colour, two exits remain and both are deliberate work: use the component layer in that specific view, or extend the island token registry and take on the verification of states after all.

The limit of validity is the usual one for this kind of architecture. A product with a single theme, no campaigns and no clients wanting their brand on top needs none of this; well-named semantic tokens and a rule banning hexadecimals in components will do. Islands start paying for themselves when the second block with its own identity appears, and they become the cheap option when that block has to coexist with both system themes.

The test I apply to decide fits in one question: if looking at the design forces you to ask “and this button, what colour is it here?”, the theme is no longer a property of the page.