Skip to content

cv-switch

Toggle control that represents an on/off state, visually distinct from a checkbox.

Headless: createSwitch

Usage

View source
html
<div class="switch-demo-shell" data-demo="switch">
  <section class="switch-demo-hero" aria-labelledby="switch-demo-title">
    <div class="switch-demo-copy">
      <span class="switch-demo-kicker">Switch pattern</span>
      <h3 id="switch-demo-title">Immediate settings with an explicit on/off state.</h3>
      <p>
        Use switches for settings that apply as soon as the user changes them. Pair ambiguous settings with
        help text instead of relying on the label alone.
      </p>
    </div>

    <div class="switch-demo-rack" aria-label="Primary switch examples">
      <cv-switch checked size="large">
        Hardware bridge
        <span slot="help-text">Connects the vault to a trusted local device.</span>
      </cv-switch>

      <cv-switch checked>
        Encrypted sync
        <span slot="help-text">Runs after the next vault update.</span>
      </cv-switch>

      <cv-switch>
        Decoy hints
        <span slot="help-text">Shows non-sensitive onboarding labels.</span>
      </cv-switch>
    </div>
  </section>

  <section class="switch-demo-section" aria-labelledby="switch-demo-states">
    <div class="switch-demo-section-header">
      <span class="switch-demo-kicker">States</span>
      <h4 id="switch-demo-states">Default, checked, disabled, and loading variants</h4>
    </div>

    <div class="switch-demo-state-grid">
      <div class="switch-demo-cell">
        <span class="switch-demo-label">Off</span>
        <cv-switch>Mask previews</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">On</span>
        <cv-switch checked>Relay pairing</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">Disabled off</span>
        <cv-switch disabled>Remote unlock</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">Disabled on</span>
        <cv-switch checked disabled>Policy enforced</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">Loading off</span>
        <cv-switch loading>Saving visibility</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">Loading on</span>
        <cv-switch checked loading>Sync in progress</cv-switch>
      </div>
    </div>
  </section>

  <section class="switch-demo-section" aria-labelledby="switch-demo-density">
    <div class="switch-demo-section-header">
      <span class="switch-demo-kicker">Density</span>
      <h4 id="switch-demo-density">Size scale and row rhythm</h4>
    </div>

    <div class="switch-demo-size-grid">
      <div class="switch-demo-cell">
        <span class="switch-demo-label">Small</span>
        <cv-switch size="small" checked>Compact row</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">Medium</span>
        <cv-switch checked>Default row</cv-switch>
      </div>

      <div class="switch-demo-cell">
        <span class="switch-demo-label">Large</span>
        <cv-switch size="large" checked>Primary setting</cv-switch>
      </div>
    </div>
  </section>

  <section class="switch-demo-section" aria-labelledby="switch-demo-content">
    <div class="switch-demo-section-header">
      <span class="switch-demo-kicker">Content</span>
      <h4 id="switch-demo-content">Help text, attribute help, and track slots</h4>
    </div>

    <div class="switch-demo-content-grid">
      <div class="switch-demo-panel">
        <cv-switch help-text="Reduces non-essential surface detail after sunset"> Night mode </cv-switch>

        <cv-switch checked>
          Network access
          <span slot="help-text">Slot content overrides the help-text attribute path.</span>
        </cv-switch>
      </div>

      <div class="switch-demo-panel">
        <cv-switch checked size="large">
          Sound
          <span class="switch-demo-track-text" slot="toggled">ON</span>
          <span class="switch-demo-track-text" slot="untoggled">OFF</span>
        </cv-switch>

        <cv-switch size="large">
          Visibility
          <span class="switch-demo-track-text" slot="toggled">YES</span>
          <span class="switch-demo-track-text" slot="untoggled">NO</span>
        </cv-switch>
      </div>
    </div>
  </section>

  <section class="switch-demo-section" aria-labelledby="switch-demo-form">
    <div class="switch-demo-section-header">
      <span class="switch-demo-kicker">Form grouping</span>
      <h4 id="switch-demo-form">Switches inside a fieldset</h4>
    </div>

    <cv-fieldset class="switch-demo-fieldset">
      <span slot="legend">Vault behavior</span>
      <span slot="description">Grouped settings preserve native fieldset semantics.</span>

      <cv-switch name="unlock-notice" checked>Warn before opening hidden vault</cv-switch>
      <cv-switch name="local-only">Keep notes on this device only</cv-switch>
      <cv-switch name="admin-policy" checked disabled>Organization policy lock</cv-switch>
    </cv-fieldset>
  </section>
</div>

Anatomy

<cv-switch> (host)
└── <div part="base">
    ├── <div part="control" role="switch">
    │   ├── <span part="toggled" hidden>
    │   │   └── <slot name="toggled">
    │   ├── <span part="untoggled" hidden>
    │   │   └── <slot name="untoggled">
    │   └── <span part="thumb">
    │       └── <span part="loader" aria-hidden="true"> (when loading)
    ├── <span part="label">
    │   └── <slot>
    └── <span part="help-text" id="{idBase}-help-text">
        └── <slot name="help-text">

When checked is true, the toggled wrapper is visible and untoggled is hidden. When checked is false, the opposite applies. Both wrappers are always in the DOM; visibility is toggled via CSS or the hidden attribute.

The help-text part is rendered only when the help-text attribute is set or the help-text slot is populated.

Attributes

AttributeTypeDefaultDescription
checkedBooleanfalseOn/off state
disabledBooleanfalsePrevents interaction and disables the form-associated value
loadingBooleanfalseShows pending state and blocks interaction without disabling value
sizeString"medium"Size: small | medium | large
help-textString""Descriptive text displayed below the switch

Sizes

Size--cv-switch-width--cv-switch-height--cv-switch-thumb-size
small36px20px14px
medium44px24px18px
large52px28px22px

Slots

SlotDescription
(default)Label text displayed beside the switch
toggledContent shown inside the track when checked (e.g., icon, text)
untoggledContent shown inside the track when unchecked (e.g., icon, text)
help-textDescriptive text below the switch; overrides the help-text attribute

CSS Parts

PartElementDescription
base<div>Root layout wrapper (contains control + label + help-text)
control<div>Track/oval with role="switch"
thumb<span>Sliding knob inside the control
loader<span>CSS-only pending spinner rendered inside thumb when loading
toggled<span>Wrapper around the toggled slot (visible when checked)
untoggled<span>Wrapper around the untoggled slot (visible when unchecked)
label<span>Wrapper around the default slot
help-text<span>Wrapper around the help-text slot or attribute text

CSS Custom Properties

PropertyDefaultDescription
--cv-switch-width44pxMinimum inline size of the control track
--cv-switch-height24pxBlock size of the control track
--cv-switch-thumb-size18pxSize of the thumb knob
--cv-switch-gapvar(--cv-space-2, 8px)Spacing between control track and label
--cv-switch-track-content-gap4pxSpacing between thumb and toggled/untoggled slot
--cv-switch-loader-sizecalc(var(--cv-switch-thumb-size) * 0.58)Size of the loading spinner inside the thumb
--cv-switch-loader-stroke2pxStroke width of the loading spinner
--cv-switch-help-text-colorvar(--cv-color-text-muted, #9aa6bf)Color of the help text
--cv-switch-help-text-font-size0.85emFont size of the help text

Visual States

Host selectorDescription
:host([checked])Primary-tinted track, thumb translated to end position; toggled part visible, untoggled part hidden
:host([loading])Progress cursor and loader inside the thumb; current checked visual state remains visible
:host([disabled])Reduced opacity (0.55), cursor: not-allowed
:host([size="small"])Small size overrides
:host([size="large"])Large size overrides

Reactive State Mapping

cv-switch is a visual adapter over headless createSwitch.

UIKit PropertyDirectionHeadless Binding
checkedattr -> actionactions.setOn(value)
disabledattr -> actionactions.setDisabled(value)
loadingattr -> actionactions.setLoading(value)
help-textattr -> optionWhen present, generates an id for the help-text element and passes it as ariaDescribedBy in createSwitch(options)
Headless StateDirectionDOM Reflection
state.isOn()state -> attr[checked] host attribute
state.isDisabled()state -> attr[disabled] host attribute
state.isLoading()state -> attraria-busy, aria-disabled, and tabindex on [part="control"]
  • contracts.getSwitchProps() is spread onto the inner [part="control"] element to apply role, aria-checked, aria-disabled, aria-busy, tabindex, and keyboard/click handlers.
  • When help text is present (via attribute or slot), the component generates the id {idBase}-help-text for the help-text element and passes it as the ariaDescribedBy option to createSwitch, which produces the aria-describedby attribute in getSwitchProps().
  • UIKit dispatches cv-input and cv-change events by observing isOn changes triggered by user activation (not by controlled setOn).
  • loading blocks click, host .click(), Enter, and Space activation, but controlled checked updates, form reset, form restore, and form-associated value submission remain available.
  • Toggled/untoggled slot visibility is purely visual (CSS-driven); no headless state or ARIA changes are involved.
  • Toggled/untoggled slot content participates in track sizing; --cv-switch-width is a minimum, not a fixed width.
  • UIKit does not own toggle or keyboard logic; headless state is the source of truth.

Events

EventDetailDescription
cv-input{checked: boolean}Fires on toggle interaction when not disabled/loading
cv-change{checked: boolean}Fires when checked state commits from user activation

ChromVoid UIKit documentation