Theming
01

AuraProvider

Sets the four data-* attributes that drive the whole token stylesheet, and exposes the current values plus setters. Token overrides go through vars as inline custom properties (CSSOM), so a strict style-src CSP is respected.

plain markup — no library on the page
rendered by aura-react
rendered by aura-react
rendered by aura-react
rendered by aura-react
This subtree is pinned by its own provider: dark / iris

Code

index.html
<!-- No provider component needed: the four axes are attributes on the root,
     and every token below re-resolves from them. -->
<html data-theme="dark" data-accent="iris" data-corner="rounded" data-density="comfortable">
  <button class="aura-btn aura-btn--primary">Now iris, on a dark canvas</button>
</html>

<script>
  // Switching a theme is one attribute write — no re-render, no context.
  document.documentElement.setAttribute('data-accent', 'sunset');
</script>
Demo.tsx
import { AuraProvider, Button, Card, Stack, Typography, useAura } from '@ilomee/aura-react';

function ThemeReadout() {
  const { theme, accent, setTheme } = useAura();
  return (
    <Stack gap={3}>
      <Typography variant="caption" tone="muted">
        This subtree is pinned by its own provider: {theme} / {accent}
      </Typography>
      <Button variant="primary" onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle just this card
      </Button>
    </Stack>
  );
}

export default function Demo() {
  // A provider scopes theming to a subtree — the page around it is unaffected.
  return (
    <AuraProvider defaultTheme="dark" defaultAccent="iris">
      <Card elevation="raised">
        <ThemeReadout />
      </Card>
    </AuraProvider>
  );
}

useAura() returns { theme, accent, corner, density, setTheme, setAccent, setCorner, setDensity } and throws outside a provider.

Demo.vue
<script setup lang="ts">
import { ref } from 'vue';
import { AuraProvider, Button } from '@ilomee/aura-vue';

const theme = ref<'light' | 'dark'>('light');
</script>

<template>
  <AuraProvider v-model:theme="theme" default-accent="iris">
    <Button variant="primary">Themed</Button>
  </AuraProvider>
</template>

All four axes support v-model — more symmetric than React, which lacks corner/density callbacks. useAura() is available via the exported AuraInjectionKey.

Demo.svelte
<script lang="ts">
  import { AuraProvider, Button } from '@ilomee/aura-svelte';

  let theme = $state<'light' | 'dark'>('light');
</script>

<AuraProvider bind:theme accent="iris">
  <Button variant="primary">Themed</Button>
</AuraProvider>

useAura() returns { theme, accent, corner, density, setTheme, setAccent, setCorner, setDensity }. The setters write through the bindable props, so bind:theme on the caller still sees the change.

Used as [auraTheme] — a directive you put on your own element.

demo.component.ts
import { inject } from '@angular/core';
import { AuraTheme, AuraThemeService, AuraButton } from '@ilomee/aura-angular';

@Component({
  standalone: true,
  imports: [AuraTheme, AuraButton],
  // Unbound axes track the service, so setTheme() below repaints the subtree.
  template: `
    <div auraTheme>
      <button auraButton variant="primary" (click)="toggle()">Themed</button>
    </div>
  `,
})
export class DemoComponent {
  private readonly svc = inject(AuraThemeService);

  toggle(): void {
    this.svc.theme.set(this.svc.theme() === 'light' ? 'dark' : 'light');
  }
}

A directive plus a root-provided AuraThemeService of signals. Each axis follows the service unless you bind that input, which pins it — and each emits when it moves, so [(theme)] writes back.

Props

Plain markup has no props — the classes below are the whole API. The four packages do nothing more than set these same classes for you.
NameTypeDefaultNotes
theme'light' | 'dark'Controlled.
defaultTheme'light' | 'dark''light'
onThemeChange(t: Theme) => void
accentAccentOne of the 10 accents.
defaultAccentAccent'aqua'
onAccentChange(a: Accent) => void
corner'rounded' | 'sharp' | 'pill'
defaultCorner'rounded' | 'sharp' | 'pill''rounded'
onCornerChange(c: Corner) => void
density'comfortable' | 'compact'
defaultDensity'comfortable' | 'compact''comfortable'
onDensityChange(d: Density) => void
varsRecord<`--${string}`, string>Token overrides applied to the wrapper.
as'div' | 'section' | 'main''div'
NameTypeDefaultNotes
themetwo-wayThemeTwo-way via v-model:theme.
accenttwo-wayAccent
cornertwo-wayCorner
densitytwo-wayDensity
defaultThemeTheme'light'
defaultAccentAccent'aqua'
defaultCornerCorner'rounded'
defaultDensityDensity'comfortable'
varsRecord<string, string>
asstring'div'
update:themeevent(t: Theme) => void
update:accentevent(a: Accent) => void
update:cornerevent(c: Corner) => void
update:densityevent(d: Density) => void
NameTypeDefaultNotes
themetwo-wayTheme'light'$bindable.
accenttwo-wayAccent'aqua'
cornertwo-wayCorner'rounded'
densitytwo-wayDensity'comfortable'
varsRecord<string, string>
as'div' | 'section' | 'main''div'
NameTypeDefaultNotes
themetwo-wayThemeLeave unset to follow AuraThemeService.
accenttwo-wayAccentLeave unset to follow AuraThemeService.
cornertwo-wayCornerLeave unset to follow AuraThemeService.
densitytwo-wayDensityLeave unset to follow AuraThemeService.
varsRecord<string, string>Token overrides written to the host as custom properties.
themeChangeevent(t: Theme) => void
accentChangeevent(a: Accent) => void
cornerChangeevent(c: Corner) => void
densityChangeevent(d: Density) => void

Emitted classes

Every framework emits the same classes — that is what makes the four libraries look identical. You can use them directly, without any component library.

ClassRoleNotes
[data-theme]baseRequired. A modifier does nothing without it.
[data-accent]baseRequired. A modifier does nothing without it.
[data-corner]baseRequired. A modifier does nothing without it.
[data-density]baseRequired. A modifier does nothing without it.