Switch
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
Privacy protection
import { Switch } from "@/components/react/styled/switch";
import { DemoWrapper } from "@/demos/react/demoWrapper";
const label = "Privacy protection";
export const SwitchDemo = ({ code }: { code: string }) => {
return (
<DemoWrapper code={code}>
<div className="flex gap-2 not-prose items-center align-middle ">
<p>{label}</p>
<Switch id="privacyQ" label={label} />
</div>
</DemoWrapper>
);
};
Install Instructions
Copy and paste the code into that file
import { type ComponentProps, useCallback, useRef } from "react";
type Size = "xs" | "sm" | "md" | "lg" | "xl";
export interface SwitchProps extends Omit<ComponentProps<"div">, "id"> {
id: string;
label: string;
defaultChecked?: boolean;
checked?: boolean;
onChecked?: (v: boolean) => void;
size?: Size;
hoveringTimeout?: number;
disabled?: boolean;
}
const getSize = (s: Size) => {
switch (s) {
case "xs":
return 1.125;
case "sm":
return 1.25;
case "md":
return 1.5;
case "lg":
return 1.875;
case "xl":
return 2.25;
}
};
export const Switch = ({
id,
label,
defaultChecked,
checked,
onChecked,
size = "md",
className = "flex",
hoveringTimeout = 225,
disabled = false,
...props
}: SwitchProps) => {
const timeout = useRef<number | null>(null);
const elRef = useRef<HTMLLabelElement | null>(null);
/** Clears the timeout incase it exits */
const clear = useCallback(() => {
if (timeout.current === null) return;
clearTimeout(timeout.current);
timeout.current = null;
}, []);
/** Starts the timeout */
const onPointerEnter = useCallback(() => {
if (disabled) return;
clear();
timeout.current = setTimeout(() => {
elRef.current?.setAttribute("data-hovering", "true");
}, hoveringTimeout) as unknown as number;
}, [hoveringTimeout, clear, disabled]);
/** Clears the timeout and resets the element */
const onPointerLeave = useCallback(() => {
if (disabled) return;
clear();
elRef.current?.setAttribute("data-hovering", "false");
}, [clear, disabled]);
return (
<div className={className} {...props}>
<input
className={[
"hidden",
"[&:checked~label]:!bg-primary-300",
"dark:[&:checked~label]:!bg-primary-300",
"[&:checked~label]:after:bg-primary-500",
"dark:[&:checked~label]:after:bg-primary-500",
"motion-safe:[&:checked~label]:after:[clip-path:inset(0_0_0_46%_round_theme(borderRadius.3xl))]",
"motion-safe:[&:checked~label[data-hovering=true]]:after:[clip-path:inset(0_0_0_35%_round_theme(borderRadius.3xl))]",
].join(" ")}
id={id}
type="checkbox"
defaultChecked={defaultChecked}
checked={checked}
disabled={disabled}
onChange={(e) => onChecked && onChecked(e.target.checked)}
/>
<label
ref={elRef}
style={{
width: `${getSize(size) * 1.75}rem`,
height: `${getSize(size)}rem`,
}}
data-disabled={disabled}
onPointerCancel={onPointerLeave}
onPointerLeave={onPointerLeave}
onPointerEnter={onPointerEnter}
data-hovering={false}
className={[
"data-[disabled=true]:cursor-not-allowed",
"data-[disabled=true]:opacity-75",
"inline-block",
"overflow-hidden",
"rounded-[calc(theme(borderRadius.3xl)_-_.125rem)]",
"transition-all",
"select-none",
"text-[0]",
"relative",
"data-[disabled=false]:hover:cursor-pointer",
"bg-neutral-300",
"data-[disabled=false]:hover:bg-neutral-200",
"focus-visible:data-[disabled=false]:bg-neutral-200",
"dark:bg-neutral-800",
"dark:data-[disabled=false]:hover:bg-neutral-700",
"dark:data-[disabled=false]:focus-visible:bg-neutral-700",
"after:absolute",
"after:transition-all",
"after:top-[.125rem]",
"after:left-[.125rem]",
"after:w-[calc(100%_-_.25rem)]",
"after:h-[calc(100%_-_.25rem)]",
"after:bg-neutral-500",
"after:dark:bg-neutral-500",
"motion-reduce:after:opacity-85",
"motion-reduce:after:[clip-path:inset(0_0_0_0_round_theme(borderRadius.3xl))]",
"motion-safe:after:[clip-path:inset(0_46%_0_0_round_theme(borderRadius.3xl))]",
"motion-safe:after:data-[hovering=true]:[clip-path:inset(0_35%_0_0_round_theme(borderRadius.3xl))]",
].join(" ")}
htmlFor={id}
>
{label}
</label>
</div>
);
};