AvesraAvesrabeta

Switch

A toggle switch component for boolean states

Import

import {
  AvSwitchImports,
  AvSwitchGroupComponent,
} from '@avesra/angular';

Usage

Enable notifications
import { Component } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-basic-demo',
  imports: [AvSwitchImports],
  template: `<div av-switch>
      <span av-switch-control>
        <span av-switch-thumb></span>
      </span>
      <span av-switch-content>Enable notifications</span>
    </div>`,
})
export class SwitchBasicDemo {}

Anatomy

Compose Switch with attribute selectors on av-switch, av-switch-control, av-switch-thumb, and av-switch-content. Optional av-switch-icon, av-description, and layout via av-switch-group.

<!-- Single switch -->
<div av-switch>
  <span av-switch-control>
    <span av-switch-thumb>
      <!-- Optional icon -->
      <span av-switch-icon></span>
    </span>
  </span>
  <span av-switch-content>
    Enable notifications
    <!-- Optional — field-level help text -->
    <p av-description></p>
  </span>
</div>

<!-- Grouped switches -->
<av-switch-group>
  <div av-switch>
    <span av-switch-control>
      <span av-switch-thumb></span>
    </span>
    <span av-switch-content>Option 1</span>
  </div>
  <div av-switch>
    <span av-switch-control>
      <span av-switch-thumb></span>
    </span>
    <span av-switch-content>Option 2</span>
  </div>
</av-switch-group>

Sizes

Small
Medium
Large
import { Component } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-sizes-demo',
  imports: [AvSwitchImports],
  template: `<div class="flex gap-6">
      <div av-switch size="sm">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Small</span>
      </div>
      <div av-switch size="md">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Medium</span>
      </div>
      <div av-switch size="lg">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Large</span>
      </div>
    </div>`,
})
export class SwitchSizesDemo {}

With Icons

Project icons into av-switch-icon inside the thumb. Bind [(selected)] to swap icons by state.

import { Component, signal } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-switch-with-icons-demo',
  imports: [
    AvSwitchImports,
    AppIconComponent,
  ],
  template: `<div class="flex flex-wrap gap-3">
      <div
        av-switch
        size="lg"
        aria-label="check"
        class="group"
        [(selected)]="check"
      >
        <span av-switch-content>
          <span
            av-switch-control
            class="group-data-[selected=true]:bg-green-500/80"
          >
            <span av-switch-thumb>
              <span av-switch-icon>
                <app-icon
                  [icon]="check() ? 'solar:check-circle-bold' : 'solar:power-linear'"
                  size="12"
                  class="opacity-100"
                  [class.opacity-70]="!check()"
                />
              </span>
            </span>
          </span>
        </span>
      </div>
      <div
        av-switch
        size="lg"
        aria-label="darkMode"
        class="group"
        [(selected)]="darkMode"
      >
        <span av-switch-content>
          <span av-switch-control>
            <span av-switch-thumb>
              <span av-switch-icon>
                <app-icon
                  [icon]="darkMode() ? 'solar:sun-bold' : 'solar:moon-bold'"
                  size="12"
                  class="opacity-100"
                  [class.opacity-70]="!darkMode()"
                />
              </span>
            </span>
          </span>
        </span>
      </div>
      <div
        av-switch
        size="lg"
        aria-label="microphone"
        class="group"
        [(selected)]="microphone"
      >
        <span av-switch-content>
          <span
            av-switch-control
            class="group-data-[selected=true]:bg-red-500/80"
          >
            <span av-switch-thumb>
              <span av-switch-icon>
                <app-icon
                  [icon]="microphone() ? 'solar:microphone-3-bold' : 'solar:microphone-3-linear'"
                  size="12"
                  class="opacity-100"
                  [class.opacity-70]="!microphone()"
                />
              </span>
            </span>
          </span>
        </span>
      </div>
      <div
        av-switch
        size="lg"
        aria-label="notification"
        class="group"
        [(selected)]="notification"
      >
        <span av-switch-content>
          <span
            av-switch-control
            class="group-data-[selected=true]:bg-purple-500/80"
          >
            <span av-switch-thumb>
              <span av-switch-icon>
                <app-icon
                  [icon]="notification() ? 'solar:bell-bold' : 'solar:bell-off-linear'"
                  size="12"
                  class="opacity-100"
                  [class.opacity-70]="!notification()"
                />
              </span>
            </span>
          </span>
        </span>
      </div>
      <div
        av-switch
        size="lg"
        aria-label="volume"
        class="group"
        [(selected)]="volume"
      >
        <span av-switch-content>
          <span
            av-switch-control
            class="group-data-[selected=true]:bg-blue-500/80"
          >
            <span av-switch-thumb>
              <span av-switch-icon>
                <app-icon
                  [icon]="volume() ? 'solar:volume-loud-bold' : 'solar:volume-cross-bold'"
                  size="12"
                  class="opacity-100"
                  [class.opacity-70]="!volume()"
                />
              </span>
            </span>
          </span>
        </span>
      </div>
    </div>`,
})
export class SwitchWithIconsDemo {
  readonly check = signal(true);
  readonly darkMode = signal(true);
  readonly microphone = signal(true);
  readonly notification = signal(true);
  readonly volume = signal(true);
}

Disabled

Enable notifications
import { Component } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-disabled-demo',
  imports: [AvSwitchImports],
  template: `<div av-switch disabled>
      <span av-switch-control>
        <span av-switch-thumb></span>
      </span>
      <span av-switch-content>Enable notifications</span>
    </div>`,
})
export class SwitchDisabledDemo {}

Without Label

Omit label text and pass aria-label on av-switch for an accessible name.

import { Component } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-without-label-demo',
  imports: [AvSwitchImports],
  template: `<div av-switch aria-label="Enable notifications">
      <span av-switch-content>
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
      </span>
    </div>`,
})
export class SwitchWithoutLabelDemo {}

With Description

Add av-description inside av-switch-content for field-level help text.

Public profile

Allow others to see your profile information

import { Component } from '@angular/core';
import {
  AvDescriptionComponent,
  AvSwitchImports,
} from '@avesra/angular';

@Component({
  selector: 'app-switch-with-description-demo',
  imports: [
    AvSwitchImports,
    AvDescriptionComponent,
  ],
  template: `<div class="max-w-sm">
      <div av-switch>
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>
          Public profile
          <p av-description>Allow others to see your profile information</p>
        </span>
      </div>
    </div>`,
})
export class SwitchWithDescriptionDemo {}

Default Selected

Enable notifications
import { Component } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-default-selected-demo',
  imports: [AvSwitchImports],
  template: `<div av-switch default-selected>
      <span av-switch-control>
        <span av-switch-thumb></span>
      </span>
      <span av-switch-content>Enable notifications</span>
    </div>`,
})
export class SwitchDefaultSelectedDemo {}

Controlled

Bind [(selected)] for two-way control over the on/off state.

Enable notifications

Switch is off

import { Component, signal } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-controlled-demo',
  imports: [AvSwitchImports],
  template: `<div class="flex flex-col gap-4">
      <div av-switch [(selected)]="isSelected">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Enable notifications</span>
      </div>
      <p class="text-sm text-muted">Switch is {{ isSelected() ? 'on' : 'off' }}</p>
    </div>`,
})
export class SwitchControlledDemo {
  readonly isSelected = signal(false);
}

Label Position

Reorder av-switch-control and av-switch-content to place the label before or after the track.

Label after
Label before
import { Component } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-label-position-demo',
  imports: [AvSwitchImports],
  template: `<div class="flex flex-col gap-4">
      <div av-switch>
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Label after</span>
      </div>
      <div av-switch>
        <span av-switch-content>Label before</span>
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
      </div>
    </div>`,
})
export class SwitchLabelPositionDemo {}

Group

Use av-switch-group to lay out multiple switches (vertical by default).

Allow Notifications
Marketing emails
Social media updates
import { Component } from '@angular/core';
import {
  AvSwitchImports,
  AvSwitchGroupComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-switch-group-demo',
  imports: [
    AvSwitchGroupComponent,
    AvSwitchImports,
  ],
  template: `<av-switch-group>
      <div av-switch name="notifications">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Allow Notifications</span>
      </div>
      <div av-switch name="marketing">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Marketing emails</span>
      </div>
      <div av-switch name="social">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Social media updates</span>
      </div>
    </av-switch-group>`,
})
export class SwitchGroupDemo {}

Group Horizontal

Set orientation="horizontal" on av-switch-group.

Notifications
Marketing
Social
import { Component } from '@angular/core';
import {
  AvSwitchImports,
  AvSwitchGroupComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-switch-group-horizontal-demo',
  imports: [
    AvSwitchGroupComponent,
    AvSwitchImports,
  ],
  template: `<av-switch-group orientation="horizontal" class="overflow-x-auto">
      <div av-switch name="notifications">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Notifications</span>
      </div>
      <div av-switch name="marketing">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Marketing</span>
      </div>
      <div av-switch name="social">
        <span av-switch-control>
          <span av-switch-thumb></span>
        </span>
        <span av-switch-content>Social</span>
      </div>
    </av-switch-group>`,
})
export class SwitchGroupHorizontalDemo {}

Form Integration

Use name and value for native form submission. Switch also implements ControlValueAccessor for reactive forms via formControlName.

Enable notifications
Subscribe to newsletter
Receive marketing updates
import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvSwitchImports,
  AvSwitchGroupComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-switch-form-demo',
  imports: [
    AvSwitchGroupComponent,
    AvSwitchImports,
    AvButtonComponent,
  ],
  template: `<form class="flex flex-col gap-4" (submit)="onSubmit($event)">
      <av-switch-group>
        <div av-switch name="notifications" value="on">
          <span av-switch-control>
            <span av-switch-thumb></span>
          </span>
          <span av-switch-content>Enable notifications</span>
        </div>
        <div av-switch default-selected name="newsletter" value="on">
          <span av-switch-control>
            <span av-switch-thumb></span>
          </span>
          <span av-switch-content>Subscribe to newsletter</span>
        </div>
        <div av-switch name="marketing" value="on">
          <span av-switch-control>
            <span av-switch-thumb></span>
          </span>
          <span av-switch-content>Receive marketing updates</span>
        </div>
      </av-switch-group>
      <button av-button class="mt-4" size="sm" type="submit" variant="primary">
        Submit
      </button>
    </form>`,
})
export class SwitchFormDemo {
  onSubmit(event: Event): void {
    event.preventDefault();
    const form = event.target as HTMLFormElement;
    const formData = new FormData(form);
    const entries = Array.from(formData.entries())
      .map(([key, value]) => `${key}: ${value}`)
      .join('\n');
    alert(`Form submitted with:\n${entries}`);
  }
}

Render Props

Angular has no React-style render props. Bind [(selected)] and interpolate state in the template instead.

Disabled
import { Component, signal } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

@Component({
  selector: 'app-switch-render-props-demo',
  imports: [AvSwitchImports],
  template: `<div av-switch [(selected)]="isSelected">
      <span av-switch-control>
        <span av-switch-thumb></span>
      </span>
      <span av-switch-content>{{ isSelected() ? 'Enabled' : 'Disabled' }}</span>
    </div>`,
})
export class SwitchRenderPropsDemo {
  readonly isSelected = signal(false);
}

Custom Styles

import { Component, signal } from '@angular/core';
import { AvSwitchImports } from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-switch-custom-styles-demo',
  imports: [
    AvSwitchImports,
    AppIconComponent,
  ],
  template: `<div av-switch aria-label="Power" class="group" [(selected)]="isSelected">
      <span av-switch-content>
        <span
          av-switch-control
          class="h-[31px] w-[51px] bg-blue-500 group-data-[selected=true]:bg-cyan-500 group-data-[selected=true]:shadow-[0_0_12px_rgba(6,182,212,0.5)]"
        >
          <span
            av-switch-thumb
            class="size-[27px] bg-white shadow-sm group-data-[selected=true]:ms-[22px] group-data-[selected=true]:shadow-lg"
          >
            <span av-switch-icon>
              @if (isSelected()) {
                <app-icon icon="solar:check-circle-bold" size="16" class="text-cyan-600" />
              } @else {
                <app-icon icon="solar:power-linear" size="16" class="text-blue-600" />
              }
            </span>
          </span>
        </span>
      </span>
    </div>`,
})
export class SwitchCustomStylesDemo {
  readonly isSelected = signal(false);
}

Styling

Passing Tailwind CSS classes

Pass utility classes on av-switch, av-switch-control, av-switch-thumb, and av-switch-group. Use group / group-data-[selected=true]: for selected-state utilities (see Custom Styles above), or customize the group layout:

<av-switch-group class="gap-8" orientation="horizontal">
  <div av-switch>
    <span av-switch-control>
      <span av-switch-thumb></span>
    </span>
    <span av-switch-content>Option 1</span>
  </div>
  <div av-switch>
    <span av-switch-control>
      <span av-switch-thumb></span>
    </span>
    <span av-switch-content>Option 2</span>
  </div>
</av-switch-group>

Customizing the component classes

To customize the Switch classes, use the @layer components directive. Learn more.

@layer components {
  .av-switch {
    @apply inline-flex gap-3 items-center;
  }

  .av-switch__control {
    @apply h-5 w-8 bg-gray-400 data-[selected=true]:bg-blue-500;
  }

  .av-switch__thumb {
    @apply bg-white shadow-sm;
  }

  .av-switch__content {
    @apply items-center gap-3;
  }

  .av-switch__icon {
    @apply h-3 w-3 text-current;
  }
}

Avesra follows the BEM methodology so component variants and states stay reusable and easy to customize.

CSS Classes

Switch Classes

  • .av-switch — Base switch container
  • .av-switch__content — Label and description container
  • .av-switch__control — Switch control track
  • .av-switch__thumb — Thumb that moves
  • .av-switch__icon — Optional icon inside the thumb
  • .av-switch__input — Visually hidden native input
  • .av-switch--sm — Small size variant
  • .av-switch--md — Medium size variant (default)
  • .av-switch--lg — Large size variant

SwitchGroup Classes

  • .av-switch-group — Switch group container
  • .av-switch-group--horizontal — Horizontal layout
  • .av-switch-group--vertical — Vertical layout (default)

Interactive States

The switch supports both CSS pseudo-classes and data attributes for flexibility:

  • Selected: [data-selected="true"] or [aria-checked="true"] (thumb position and background color change)
  • Hover: :hover or [data-hovered="true"]
  • Focus: :focus-visible or [data-focus-visible="true"]
  • Disabled: [data-disabled="true"] or [aria-disabled="true"]
  • Pressed: :active or [data-pressed="true"]

API

Switch

Props for div[av-switch]. Implements ControlValueAccessor for Angular forms. span[av-switch-control], span[av-switch-thumb], span[av-switch-icon], and span[av-switch-content] project content and have no inputs.

PropTypeDefaultDescription
size'sm' | 'md' | 'lg''md'Switch size.
disabledbooleanfalseDisables interaction. Also set by reactive forms.
default-selectedbooleanfalseInitial selected state for uncontrolled usage.
selectedbooleanfalseSelected state. Supports two-way binding with `[(selected)]`.
selectedChangeEventEmitter<boolean>—Emits when the selected state changes.
aria-labelstring—Accessible label when no visible label is provided.
namestring—Form field name for native form submission.
valuestring'on'Form field value when selected in native form submission.

SwitchGroup

Props for av-switch-group.

PropTypeDefaultDescription
orientation'horizontal' | 'vertical''vertical'Layout direction of grouped switches.

Made with ❤ by SyntaxHertz. Open source and available on GitHub.