Swap
A CSS-first state indicator with checkbox and application-controlled contracts
Swap
Swap replaces one visual state indicator with another while keeping a stable control footprint. It does not manage application state and does not replace Switch, Toggle Button, or Theme Controller.
@import "@duskmoon-dev/core/components/swap";
Checkbox-driven state
The native checkbox is authoritative. Give the input a stable accessible name; keep icons decorative and do not nest interactive controls inside either slot.
Menu and close indicator
<label class="swap">
<input class="swap-input" type="checkbox" aria-label="Toggle navigation" />
<span class="swap-off" aria-hidden="true">☰</span>
<span class="swap-on" aria-hidden="true">×</span>
</label>Rotating light and dark indicator
<label class="swap swap-rotate">
<input class="swap-input" type="checkbox" aria-label="Use dark appearance" />
<span class="swap-off" aria-hidden="true">☀️</span>
<span class="swap-on" aria-hidden="true">🌙</span>
</label>Disabling .swap-input applies the disabled presentation and prevents native activation.
Application-controlled button
For a button, aria-pressed is authoritative. JavaScript must update it; CSS never updates ARIA.
Controlled mute button
<button
class="btn btn-icon swap swap-rotate"
id="demo-swap-mute"
type="button"
aria-label="Mute notifications"
aria-pressed="false"
>
<span class="swap-off" aria-hidden="true">🔔</span>
<span class="swap-on" aria-hidden="true">🔕</span>
</button>
<script>
(() => {
const button = document.getElementById('demo-swap-mute');
if (!button || button.dataset.ready) return;
button.dataset.ready = 'true';
button.addEventListener('click', () => {
const pressed = button.getAttribute('aria-pressed') !== 'true';
button.setAttribute('aria-pressed', String(pressed));
button.setAttribute('aria-label', pressed ? 'Unmute notifications' : 'Mute notifications');
});
})();
</script>Changing the accessible name here describes the action the button will perform. For a stable preference label, use a labelled group and expose the current state separately.
Presentation-only state
.swap-active forces the on slot for a noninteractive presentation. It takes visual precedence over checkbox or aria-pressed state, so do not use it on an interactive control where it could contradict semantic state.
<span class="swap swap-active" aria-label="Synchronization complete">
<span class="swap-off" aria-hidden="true">…</span>
<span class="swap-on" aria-hidden="true">✓</span>
</span>
API
| Class | Purpose |
|---|---|
.swap |
Stable overlay grid for both state slots |
.swap-input |
Native checkbox state source |
.swap-off |
Content visible for unchecked or aria-pressed="false" state |
.swap-on |
Content visible for checked or aria-pressed="true" state |
.swap-rotate |
Rotation transition modifier |
.swap-active |
Presentation-only forced-on state |
Inactive slots are hidden from pointer interaction and visibility. Keep slots limited to indicators or labels: nested links, buttons, inputs, and panels create misleading focus and accessibility behavior. Transitions are removed for reduced-motion users.