Switch
A toggle switch for boolean settings.
A toggle switch for boolean settings, styled per platform.
Installation
npx touchcn add switch
Usage
<tcn-switch [(checked)]="enabled" />import { TcnSwitch } from '@/components/ui/switch';
<TcnSwitch checked={enabled} onCheckedChange={setEnabled} /><script setup lang="ts">
import { TcnSwitch } from '@/components/ui/switch';
</script>
<template>
<TcnSwitch v-model="enabled" />
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
checked |
boolean |
false |
Toggle state (two-way bound). |
onCheckedChange |
(checked: boolean) => void |
— | Change callback (React). |
disabled |
boolean |
false |
Disable the switch. |
On React, TcnSwitch forwards Radix Switch props.
Source · Angular
export * from './tcn-switch';import { booleanAttribute, Component, input, model } from '@angular/core';
@Component({
selector: 'tcn-switch',
template: `
<button
type="button"
role="switch"
class="tcn-switch"
[attr.aria-checked]="checked()"
[disabled]="disabled()"
(click)="toggle()"
>
<span class="tcn-switch-thumb if-md flex items-center justify-center">
@if (checked()) {
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 12l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
}
</span>
<span class="tcn-switch-thumb if-ios"></span>
</button>
`,
host: { class: 'inline-flex' },
})
export class TcnSwitch {
readonly checked = model(false);
readonly disabled = input(false, { transform: booleanAttribute });
protected toggle(): void {
if (!this.disabled()) {
this.checked.update((value) => !value);
}
}
}Source · React
export * from './tcn-switch';import { forwardRef, useState } from 'react';
import type { ComponentPropsWithoutRef } from 'react';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { cn } from '@touchcn/core';
export interface TcnSwitchProps
extends Omit<ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, 'asChild' | 'children'> {}
/**
* Toggle switch. Radix supplies the accessible `button[role="switch"]` with
* `aria-checked` and keyboard handling; the two thumbs replicate the platform
* fork — the MD thumb morphs and shows a checkmark when on, the iOS thumb
* slides. The active state is mirrored locally so the checkmark can be rendered
* conditionally (same markup as the Angular component) while still supporting
* both controlled and uncontrolled usage.
*/
export const TcnSwitch = forwardRef<HTMLButtonElement, TcnSwitchProps>(function TcnSwitch(
{ checked, defaultChecked, onCheckedChange, className, ...props },
ref,
) {
const [internalChecked, setInternalChecked] = useState(defaultChecked ?? false);
const isControlled = checked !== undefined;
const currentChecked = isControlled ? checked : internalChecked;
const handleChange = (next: boolean) => {
if (!isControlled) {
setInternalChecked(next);
}
onCheckedChange?.(next);
};
return (
<SwitchPrimitive.Root
ref={ref}
className={cn('tcn-switch', className)}
checked={currentChecked}
onCheckedChange={handleChange}
{...props}
>
<span className="tcn-switch-thumb if-md flex items-center justify-center">
{currentChecked && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5 12l4 4L19 7"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</span>
<span className="tcn-switch-thumb if-ios" />
</SwitchPrimitive.Root>
);
});Source · Vue
<script setup lang="ts">
import { SwitchRoot } from 'reka-ui';
defineProps<{ disabled?: boolean }>();
/**
* Two-way bound on/off state. reka-ui `SwitchRoot` supplies the accessible
* `button[role="switch"]` with `aria-checked` and keyboard handling; the two
* thumbs replicate the platform fork — the MD thumb morphs and shows a
* checkmark when on, the iOS thumb slides. The bound value drives the
* conditional checkmark (same markup as the Angular / React component) while
* supporting both controlled (`v-model`) and uncontrolled usage.
*/
const model = defineModel<boolean>({ default: false });
</script>
<template>
<SwitchRoot v-model="model" :disabled="disabled" class="tcn-switch">
<span class="tcn-switch-thumb if-md flex items-center justify-center">
<svg v-if="model" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5 12l4 4L19 7"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span class="tcn-switch-thumb if-ios" />
</SwitchRoot>
</template>export { default as TcnSwitch } from './TcnSwitch.vue';