Toggle
toggleuiTwo-state press-and-latch button (Radix Toggle): controlled pressed or uncontrolled defaultPressed with pressedChange, aria-pressed + data-state, default / outline variants and three sizes. Doubles as a form control.
Installation
$pnpm dlx uipkge-ng@latest add toggle$npx uipkge-ng@latest add toggle$yarn dlx uipkge-ng@latest add toggle$bunx uipkge-ng@latest add toggleManifest:
https://uipkge.dev/r/angular/toggle.jsonInstalls to:src/app/components/ui/toggle/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''outline' | default | optional |
size | 'default''sm''lg' | default | optional |
pressedControlled pressed state. Leave unbound for uncontrolled use with `defaultPressed`. | boolean | — | optional |
defaultPressed | boolean | false | optional |
pressedChange | EventEmitter<boolean> | — | optional |
disabled | boolean | false | optional |
class | string | — | optional |
npm dependencies
Used by
Files installed (3)
src/app/components/ui/toggle/toggle.component.ts4 kB
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, booleanAttribute, forwardRef, inject, signal, ChangeDetectionStrategy, } from '@angular/core' import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms' import { cn } from '@/lib/utils' import { toggleVariants, type ToggleVariants } from './toggle.variants' export type ToggleVariant = NonNullable<ToggleVariants['variant']> export type ToggleSize = NonNullable<ToggleVariants['size']> /** * Angular port of UIPKGE Toggle (React `Toggle` on Radix Toggle). Radix semantics: controlled * `pressed` or uncontrolled `defaultPressed` + `pressedChange`, `aria-pressed`, * `data-state="on" | "off"`, `data-disabled` and native `disabled` (so the `disabled:*` * variant styles apply and a disabled toggle is skipped by Tab). Put it on a `<button>` * (`<button ui-toggle>`, which also gets `type="button"`); the `<ui-toggle>` custom element * gets button semantics via role + tabindex + Enter / Space. Also a ControlValueAccessor, * so `[formControl]` / `ngModel` bind the pressed state. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-toggle, [ui-toggle]', standalone: true, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiToggleComponent), multi: true }], host: { '[attr.type]': 'isButton ? "button" : null', '[attr.role]': 'isButton ? null : "button"', '[attr.tabindex]': 'isButton ? null : disabled ? -1 : 0', '[attr.aria-pressed]': 'pressed', '[attr.data-state]': 'pressed ? "on" : "off"', '[attr.data-disabled]': 'disabled ? "" : null', '[attr.disabled]': 'disabled && isButton ? "" : null', '[attr.aria-disabled]': 'disabled && !isButton ? "true" : null', '[attr.data-uipkge]': '""', '[attr.data-slot]': '"toggle"', '[class]': 'hostClass', '(click)': 'toggle()', '(keydown)': 'onKeydown($event)', }, template: `<ng-content />`, }) export class UiToggleComponent implements ControlValueAccessor { // Zoneless-safe: form writes happen outside template events, so schedule a repaint. private readonly cdr = inject(ChangeDetectorRef) private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement readonly isButton = this.el.tagName === 'BUTTON' /** Internal state; null until set (then `defaultPressed` applies). */ private readonly state = signal<boolean | null>(null) private controlled = false /** Controlled pressed state. Leave unbound for uncontrolled use with `defaultPressed`. */ @Input({ transform: booleanAttribute }) set pressed(v: boolean) { this.controlled = true this.state.set(v) } get pressed(): boolean { return this.state() ?? this.defaultPressed } @Input({ transform: booleanAttribute }) defaultPressed = false @Output() pressedChange = new EventEmitter<boolean>() @Input() variant?: ToggleVariant @Input() size?: ToggleSize @Input({ transform: booleanAttribute }) disabled = false @Input('class') className?: string private onChange: (v: boolean) => void = () => {} private onTouched: () => void = () => {} get hostClass(): string { return cn(toggleVariants({ variant: this.variant, size: this.size }), this.className) } toggle(): void { if (this.disabled) return const next = !this.pressed if (!this.controlled) this.state.set(next) this.onChange(next) this.onTouched() this.pressedChange.emit(next) } onKeydown(event: KeyboardEvent): void { if (this.isButton) return if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() this.toggle() } } writeValue(v: boolean | null): void { this.state.set(!!v) this.cdr.markForCheck() } registerOnChange(fn: (v: boolean) => void): void { this.onChange = fn } registerOnTouched(fn: () => void): void { this.onTouched = fn } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled this.cdr.markForCheck() } } export { toggleVariants, type ToggleVariants }src/app/components/ui/toggle/toggle.variants.ts1.6 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming Vue SFCs can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const toggleVariants = cva( // Focus ring intentionally subtle: 1px ring on :focus-visible only, no // border swap. Keyboard users still get a clear focused state; mouse // users don't see a loud 3px halo after click (which lingers because // the button retains focus). Matches the segmented-control density. "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:ring-ring/50 focus-visible:ring-1 outline-none transition-colors duration-200 whitespace-nowrap", { variants: { variant: { default: 'bg-transparent', outline: 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground', }, size: { default: 'h-9 px-2 min-w-9', sm: 'h-8 px-1.5 min-w-8', lg: 'h-10 px-2.5 min-w-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export type ToggleVariants = VariantProps<typeof toggleVariants>src/app/components/ui/toggle/index.ts0.1 kB
export { UiToggleComponent, toggleVariants, type ToggleVariants, type ToggleVariant, type ToggleSize, } from './toggle.component'
Raw manifest:https://uipkge.dev/r/angular/toggle.json