Switch
switch.astro
---
import type { HTMLAttributes } from "astro/types";
import "./switch.css";
interface Props extends HTMLAttributes<"input"> {
label?: string;
}
const { id = "my-switch", label, ...rest } = Astro.props;
---
<div class="switch">
{label && <label for={id}>{label}</label>}
<button>
<div class="track">
<input type="checkbox" id={id} {...rest} />
<span class="knob"></span>
</div>
</button>
</div>
<script>
function onClick(event: Event) {
const input = (event.currentTarget as HTMLButtonElement).querySelector(
"input"
)!;
const newState = !input.checked;
input.checked = newState;
input.value = newState.toString();
}
function init() {
const switches = document.querySelectorAll(".switch button");
for (const button of switches) {
button.addEventListener("click", onClick);
}
}
document.addEventListener("DOMContentLoaded", init);
document.addEventListener("astro:after-swap", init);
document.addEventListener("astro:page-load", init);
</script>
switch.tsx
import { createRef, type JSX } from "preact";
import "./switch.css";
interface Switch extends JSX.HTMLAttributes<HTMLInputElement> {}
export default function Switch(props: Switch) {
const { id = "my-switch", label, onChange, ...rest } = props;
const ref = createRef<HTMLInputElement>();
function onClick() {
const input = ref.current!;
const newState = !input.checked;
input.checked = newState;
input.value = newState.toString();
if (typeof onChange === "function")
onChange({
target: input,
} as unknown as JSX.TargetedEvent<HTMLInputElement>);
}
return (
<div class="switch">
{label && <label for={id}>{label}</label>}
<button onClick={onClick}>
<div class="track">
<input type="checkbox" id={id} ref={ref} {...rest} />
<span class="knob" />
</div>
</button>
</div>
);
}
switch.css
.switch {
--padding: 0.5em;
--width: 100%;
--height: 2em;
--knob-width: 1em;
--knob-height: 1em;
--accent: #007bff;
--accent-lighter: #4dabf7;
--mid: #6c757d;
--lighter: #a0a0a0;
--lightest: #fff;
}
.switch button {
position: relative;
display: inline-block;
border: none;
background-color: transparent;
padding: 0;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch .track {
position: relative;
height: var(--height, auto);
min-width: 3.5em;
max-width: var(--width, 3.5em);
}
.knob {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--mid);
border: 2px solid var(--lighter);
border-radius: var(--knob-width);
}
.knob:before {
position: absolute;
content: "";
width: var(--knob-width);
height: var(--knob-height);
left: var(--padding);
bottom: 50%;
transform: translateY(50%);
background-color: var(--lightest);
border-radius: 50%;
}
.switch input:checked + .knob {
background-color: var(--accent);
border-color: var(--accent-lighter);
}
.switch input:focus + .knob {
box-shadow: 0 0 0.5em var(--accent);
}
.switch input:checked + .knob:before {
left: auto;
right: var(--padding);
}