Switch

switch.svelte

A binary toggle switch with text, box, and track variants.

Demo

Usage
001 <script lang="ts">
002 import Switch from '$lib/components/switch.svelte';
003
004 let opt1 = $state(true);
005 let opt2 = $state(false);
006 let opt3 = $state(true);
007 </script>
008
009 <div class="flex flex-col gap-3">
010 <Switch variant="text" label="Vim keybindings" bind:checked={opt1} />
011 <Switch variant="track" label="Hardware acceleration" bind:checked={opt2} />
012 <Switch variant="box" label="Auto-save buffers" bind:checked={opt3} />
013 <Switch variant="text" label="Disabled setting" checked={false} disabled />
014 </div>
015
switch.svelte
001 <script lang="ts" module>
002 import type { HTMLButtonAttributes } from 'svelte/elements';
003
004 export type SwitchVariant = 'text' | 'box' | 'track';
005
006 export type SwitchProps = Omit<HTMLButtonAttributes, 'onchange'> & {
007 ref?: HTMLButtonElement | null;
008 checked?: boolean;
009 variant?: SwitchVariant;
010 label?: string;
011 onchange?: (checked: boolean) => void;
012 };
013 </script>
014
015 <script lang="ts">
016 let {
017 ref = $bindable(null),
018 checked = $bindable(false),
019 variant = 'text',
020 label,
021 disabled = false,
022 class: className = '',
023 onchange,
024 ...restProps
025 }: SwitchProps = $props();
026
027 function toggle() {
028 if (disabled) return;
029 checked = !checked;
030 onchange?.(checked);
031 }
032
033 function handleKeydown(e: KeyboardEvent) {
034 if (e.key === ' ' || e.key === 'Enter') {
035 e.preventDefault();
036 toggle();
037 }
038 }
039 </script>
040
041 <button
042 type="button"
043 bind:this={ref}
044 role="switch"
045 aria-checked={checked}
046 {disabled}
047 class="switch-wrap {variant} {className}"
048 onclick={toggle}
049 onkeydown={handleKeydown}
050 {...restProps}
051 >
052 {#if variant === 'text'}
053 <span class="switch-box">
054 [<span class="state-on" class:active={checked}>ON</span><span class="sep">│</span><span
055 class="state-off"
056 class:active={!checked}>OFF</span
057 >]
058 </span>
059 {:else if variant === 'box'}
060 <span class="switch-box">
061 [{checked ? 'X' : ' '}]
062 </span>
063 {:else if variant === 'track'}
064 <span class="switch-box">
065 [{checked ? '===●' : '●==='}]
066 </span>
067 {/if}
068
069 {#if label}
070 <span class="switch-label">{label}</span>
071 {/if}
072 </button>
073
074 <style>
075 .switch-wrap {
076 display: inline-flex;
077 align-items: center;
078 gap: 1ch;
079 height: var(--line);
080 line-height: var(--line);
081 font-family: var(--font-family-mono);
082 font-size: var(--font-size);
083 background: transparent;
084 color: var(--text-primary);
085 border: 0;
086 padding: 0;
087 cursor: pointer;
088 outline: 0;
089 user-select: none;
090 vertical-align: baseline;
091 box-sizing: border-box;
092 }
093
094 .switch-wrap:disabled {
095 opacity: 0.4;
096 cursor: not-allowed;
097 }
098
099 .switch-wrap:focus-visible .switch-box {
100 box-shadow: inset 0 0 0 1px var(--focus-ring);
101 background: color-mix(in srgb, var(--focus-ring) 15%, transparent);
102 }
103
104 .switch-box {
105 display: inline-flex;
106 align-items: center;
107 height: var(--line);
108 line-height: var(--line);
109 color: var(--button-muted);
110 padding: 0 0.5ch;
111 }
112
113 .state-on.active {
114 color: rgb(34, 197, 94);
115 font-weight: bold;
116 }
117
118 .state-off.active {
119 color: var(--text-primary);
120 font-weight: bold;
121 }
122
123 .state-on:not(.active),
124 .state-off:not(.active) {
125 opacity: 0.35;
126 }
127
128 .sep {
129 opacity: 0.4;
130 padding: 0 0.25ch;
131 }
132
133 .switch-wrap.track .switch-box {
134 color: var(--focus-ring);
135 }
136
137 .switch-label {
138 color: var(--text-primary);
139 }
140 </style>
141