Color scheme icon <l-color-scheme-icon>
A sun that morphs into a moon.
<l-color-scheme-icon><l-color-scheme-icon scheme="dark"></l-color-scheme-icon>The two glyphs are one shape: the disc grows, a mask slides in to carve the crescent, and the rays retract in a ripple. Nothing is added or removed, so the change reads as a single object turning rather than two icons swapping.
Its home is an icon-only button in a header — the button owns the role, the name and the pressed state; the glyph shows which scheme is in effect.
Code
<header
class="flex w-full items-center justify-between gap-4 rounded-xl border px-4 py-3 border-[var(--l-color-border)] bg-[var(--l-color-surface)]"
>
<div class="flex items-center gap-6">
<span class="text-sm font-semibold text-[var(--l-color-text-primary)]">Acme</span>
<nav class="flex gap-4 text-sm text-[var(--l-color-text-secondary)]">
<a href="#">Dashboard</a>
<a href="#">Reports</a>
<a href="#">Settings</a>
</nav>
</div>
<button
class="l-button"
data-icon-only
aria-pressed="false"
aria-label="Dark theme"
>
<l-color-scheme-icon></l-color-scheme-icon>
</button>
</header>It is presentational only — it shows a scheme, it never chooses or stores one. With no scheme it follows colorScheme.current, so it stays in step with every other control on the page without wiring; set scheme to pin it.
The example above uses inline handlers to stay readable. In an application, the click and the pressed state are what you wire — the glyph needs nothing:
import { colorScheme } from 'luxen-ui/color-scheme';
const button = document.querySelector('[aria-label="Dark theme"]');
button.addEventListener('click', () => colorScheme.toggle());
// Called once with the current scheme, then on every change — no initial pass.
colorScheme.subscribe((scheme) => {
button.setAttribute('aria-pressed', String(scheme === 'dark'));
});Which scheme does the glyph show?
The one in effect, not the one a click would move to: dark mode shows the moon. That is what makes it correct inside a role="switch" thumb or beside an aria-pressed button, where the state is already announced and the glyph should agree with it.
To advertise the destination instead — a sun meaning "switch to light" — pass the opposite of your current scheme. Pick one convention and hold it across the app; mixing them is how a toggle stops being readable.
Options
Scheme
Leave scheme unset and the icon follows the page's color scheme — a stored override, else the OS preference — updating itself when it changes anywhere, including in another tab.
Set scheme and the store is out of the picture: the glyph shows what you tell it and nothing else moves it. That is the hook for an app that already owns its light/dark state — a framework store, a class on <html> written by the server, a user setting fetched from an API — and also what pins a glyph in a legend or a preview.
// Your state, your rules. The icon just draws it.
theme.subscribe((value) => (icon.scheme = value));scheme="light"
scheme="dark"
follows the page
Code
<div class="flex items-center gap-8">
<div class="flex flex-col items-center gap-2">
<l-color-scheme-icon
scheme="light"
class="[--size:2rem]"
></l-color-scheme-icon>
<code class="text-xs text-[var(--l-color-text-secondary)]">scheme="light"</code>
</div>
<div class="flex flex-col items-center gap-2">
<l-color-scheme-icon
scheme="dark"
class="[--size:2rem]"
></l-color-scheme-icon>
<code class="text-xs text-[var(--l-color-text-secondary)]">scheme="dark"</code>
</div>
<div class="flex flex-col items-center gap-2">
<l-color-scheme-icon class="[--size:2rem]"></l-color-scheme-icon>
<code class="text-xs text-[var(--l-color-text-secondary)]">follows the page</code>
</div>
</div>Size & color
--size sets width and height (default 1em, so it scales with the surrounding text). --color defaults to currentColor.
Code
<div class="flex items-center gap-8">
<l-color-scheme-icon scheme="dark"></l-color-scheme-icon>
<l-color-scheme-icon
scheme="dark"
class="[--size:2rem]"
></l-color-scheme-icon>
<l-color-scheme-icon
scheme="dark"
class="[--size:3rem] [--color:var(--l-color-text-info)]"
></l-color-scheme-icon>
</div>Accessible name
The icon is decorative by default and hidden from assistive tech, which is what you want inside a row or button that already carries the name. Set label to make it meaningful — it then exposes role="img" with that name.
Both icons below look identical; the difference exists only in the accessibility tree.
Code
<div class="flex items-center gap-8">
<button
class="l-button"
data-icon-only
aria-label="Dark theme"
>
<l-color-scheme-icon></l-color-scheme-icon>
</button>
<l-color-scheme-icon
label="Currently in dark theme"
scheme="dark"
class="[--size:2rem]"
></l-color-scheme-icon>
</div>Examples
In a menu row
Put it in a type="checkbox" dropdown item and the row is the control: whole row clickable, menuitemcheckbox role, menu stays open. The icon stays decorative.
Add check-placement="end" so the check moves to the trailing edge and leaves the leading column free — the glyph then lines up with every other row's icon.
Code
<header
class="flex w-full items-center justify-between gap-4 rounded-xl border px-4 py-3 border-[var(--l-color-border)] bg-[var(--l-color-surface)]"
>
<div class="flex items-center gap-6">
<span class="text-sm font-semibold text-[var(--l-color-text-primary)]">Acme</span>
<nav class="flex gap-4 text-sm text-[var(--l-color-text-secondary)]">
<a href="#">Dashboard</a>
<a href="#">Reports</a>
</nav>
</div>
<l-dropdown placement="bottom-end">
<l-avatar
slot="trigger"
interactive
name="Jane Cooper"
aria-label="Account"
class="[--appearance:circle] [--color:var(--l-color-blue-200)]"
></l-avatar>
<div
slot="header"
class="flex flex-col px-2 py-1.5"
>
<span class="text-sm font-medium text-[var(--l-color-text-primary)]">Jane Cooper</span>
<span class="text-xs text-[var(--l-color-text-secondary)]">jane.cooper@acme.com</span>
</div>
<l-divider></l-divider>
<l-dropdown-item
type="checkbox"
check-placement="end"
value="theme"
>
<l-color-scheme-icon slot="prefix"></l-color-scheme-icon>
Dark theme
</l-dropdown-item>
<l-dropdown-item
type="checkbox"
check-placement="end"
value="compact"
checked
>
<iconify-icon
slot="prefix"
icon="lucide:rows-3"
></iconify-icon>
Compact rows
</l-dropdown-item>
<l-divider></l-divider>
<l-dropdown-item value="signout">
<iconify-icon
slot="prefix"
icon="lucide:log-out"
></iconify-icon>
Sign out
</l-dropdown-item>
</l-dropdown>
</header>Here the row carries the state, so checked is what you keep in step:
import { colorScheme } from 'luxen-ui/color-scheme';
const item = document.querySelector('l-dropdown-item[value="theme"]');
document.querySelector('l-dropdown').addEventListener('select', (event) => {
if (event.item === item) colorScheme.set(item.checked ? 'dark' : 'light');
});
colorScheme.subscribe((scheme) => (item.checked = scheme === 'dark'));Do not put a switch in a menu row instead: a role="menu" may only own menuitem, menuitemcheckbox and menuitemradio children, and a nested control gives the row two competing click targets.
Accessibility
Criteria
- Decorative by default
With no
labelthe icon isaria-hidden— the surrounding row or button carries the accessible name, so nothing is announced twiceWCAG1.1.1RGAA1.2- Meaningful when named
Setting
labelexposesrole=imgwith that name, for the rare case where the glyph stands aloneWCAG1.1.1RGAA1.1- Reduced motion
The morph collapses to 0ms under
prefers-reduced-motion; the glyph still changes, it just does not animateWCAG2.3.3
API reference
Importing
import 'luxen-ui/color-scheme-icon';Properties
schemeColorScheme | undefinedProperty- Which glyph to show. Leave it unset and the icon follows the page's scheme
(
colorScheme.current— a stored override, else the OS preference), so it stays in step with every other control without wiring. Set it to pin the glyph, for a legend or a preview that must not move. labelstring | undefinedProperty- Accessible label. When set, the glyph becomes meaningful (
role="img"); when absent it is decorative and hidden from assistive tech — the right default inside a row or button that already carries the name.
CSS custom properties
--sizedefault:1emCustom property- Width and height of the glyph.
--colordefault:currentColorCustom property- Glyph color.
CSS parts
basePart- The root
<svg>, for overriding the morph's timing or tilt.