diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts
index cb45e12228..a6c9e54c57 100644
--- a/libs/components/src/index.ts
+++ b/libs/components/src/index.ts
@@ -1,6 +1,7 @@
export * from "./badge";
export * from "./banner";
export * from "./button";
+export * from "./toggle-group";
export * from "./callout";
export * from "./form-field";
export * from "./menu";
diff --git a/libs/components/src/toggle-group/index.ts b/libs/components/src/toggle-group/index.ts
new file mode 100644
index 0000000000..bb7dba253e
--- /dev/null
+++ b/libs/components/src/toggle-group/index.ts
@@ -0,0 +1,2 @@
+export * from "./toggle-group.component";
+export * from "./toggle-group.module";
diff --git a/libs/components/src/toggle-group/toggle-group.component.html b/libs/components/src/toggle-group/toggle-group.component.html
new file mode 100644
index 0000000000..6dbc743063
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle-group.component.html
@@ -0,0 +1 @@
+
diff --git a/libs/components/src/toggle-group/toggle-group.component.spec.ts b/libs/components/src/toggle-group/toggle-group.component.spec.ts
new file mode 100644
index 0000000000..3bce3472e8
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle-group.component.spec.ts
@@ -0,0 +1,69 @@
+import { Component } from "@angular/core";
+import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+
+import { ToggleGroupModule } from "./toggle-group.module";
+import { ToggleComponent } from "./toggle.component";
+
+describe("Button", () => {
+ let fixture: ComponentFixture;
+ let testAppComponent: TestApp;
+ let buttonElements: ToggleComponent[];
+ let radioButtons: HTMLInputElement[];
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [ToggleGroupModule],
+ declarations: [TestApp],
+ });
+
+ TestBed.compileComponents();
+ fixture = TestBed.createComponent(TestApp);
+ testAppComponent = fixture.debugElement.componentInstance;
+ buttonElements = fixture.debugElement
+ .queryAll(By.css("bit-toggle"))
+ .map((e) => e.componentInstance);
+ radioButtons = fixture.debugElement
+ .queryAll(By.css("input[type=radio]"))
+ .map((e) => e.nativeElement);
+
+ fixture.detectChanges();
+ }));
+
+ it("should select second element when setting selected to second", () => {
+ testAppComponent.selected = "second";
+ fixture.detectChanges();
+
+ expect(buttonElements[1].selected).toBe(true);
+ });
+
+ it("should not select second element when setting selected to third", () => {
+ testAppComponent.selected = "third";
+ fixture.detectChanges();
+
+ expect(buttonElements[1].selected).toBe(false);
+ });
+
+ it("should emit new value when changing selection by clicking on radio button", () => {
+ testAppComponent.selected = "first";
+ fixture.detectChanges();
+
+ radioButtons[1].click();
+
+ expect(testAppComponent.selected).toBe("second");
+ });
+});
+
+@Component({
+ selector: "test-app",
+ template: `
+
+ First
+ Second
+ Third
+
+ `,
+})
+class TestApp {
+ selected?: string;
+}
diff --git a/libs/components/src/toggle-group/toggle-group.component.ts b/libs/components/src/toggle-group/toggle-group.component.ts
new file mode 100644
index 0000000000..adaed4bdb0
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle-group.component.ts
@@ -0,0 +1,24 @@
+import { Component, EventEmitter, HostBinding, Input, Output } from "@angular/core";
+
+let nextId = 0;
+
+@Component({
+ selector: "bit-toggle-group",
+ templateUrl: "./toggle-group.component.html",
+ preserveWhitespaces: false,
+})
+export class ToggleGroupComponent {
+ private id = nextId++;
+ name = `bit-toggle-group-${this.id}`;
+
+ @Input() selected?: unknown;
+ @Output() selectedChange = new EventEmitter();
+
+ @HostBinding("attr.role") role = "radiogroup";
+ @HostBinding("class") classList = ["tw-flex"];
+
+ onInputInteraction(value: unknown) {
+ this.selected = value;
+ this.selectedChange.emit(value);
+ }
+}
diff --git a/libs/components/src/toggle-group/toggle-group.module.ts b/libs/components/src/toggle-group/toggle-group.module.ts
new file mode 100644
index 0000000000..fe1ce0ec52
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle-group.module.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+
+import { BadgeModule } from "../badge";
+
+import { ToggleGroupComponent } from "./toggle-group.component";
+import { ToggleComponent } from "./toggle.component";
+
+@NgModule({
+ imports: [CommonModule, BadgeModule],
+ exports: [ToggleGroupComponent, ToggleComponent],
+ declarations: [ToggleGroupComponent, ToggleComponent],
+})
+export class ToggleGroupModule {}
diff --git a/libs/components/src/toggle-group/toggle-group.stories.ts b/libs/components/src/toggle-group/toggle-group.stories.ts
new file mode 100644
index 0000000000..8cc06f8c32
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle-group.stories.ts
@@ -0,0 +1,54 @@
+import { Meta, moduleMetadata, Story } from "@storybook/angular";
+
+import { BadgeModule } from "../badge";
+
+import { ToggleGroupComponent } from "./toggle-group.component";
+import { ToggleComponent } from "./toggle.component";
+
+export default {
+ title: "Component Library/Toggle Group",
+ component: ToggleGroupComponent,
+ args: {
+ selected: "all",
+ },
+ decorators: [
+ moduleMetadata({
+ declarations: [ToggleGroupComponent, ToggleComponent],
+ imports: [BadgeModule],
+ }),
+ ],
+ parameters: {
+ design: {
+ type: "figma",
+ url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17157",
+ },
+ },
+} as Meta;
+
+const Template: Story = (args: ToggleGroupComponent) => ({
+ props: args,
+ template: `
+
+
+ All 3
+
+
+
+ Invited
+
+
+
+ Accepted 2
+
+
+
+ Deactivated
+
+
+ `,
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ selected: "all",
+};
diff --git a/libs/components/src/toggle-group/toggle.component.html b/libs/components/src/toggle-group/toggle.component.html
new file mode 100644
index 0000000000..471ed5f0c0
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle.component.html
@@ -0,0 +1,11 @@
+
+
diff --git a/libs/components/src/toggle-group/toggle.component.spec.ts b/libs/components/src/toggle-group/toggle.component.spec.ts
new file mode 100644
index 0000000000..37c2e2ac10
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle.component.spec.ts
@@ -0,0 +1,71 @@
+import { Component } from "@angular/core";
+import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+
+import { ToggleGroupComponent } from "./toggle-group.component";
+import { ToggleGroupModule } from "./toggle-group.module";
+
+describe("Button", () => {
+ let mockGroupComponent: MockedButtonGroupComponent;
+ let fixture: ComponentFixture;
+ let testAppComponent: TestApp;
+ let radioButton: HTMLInputElement;
+
+ beforeEach(waitForAsync(() => {
+ mockGroupComponent = new MockedButtonGroupComponent();
+
+ TestBed.configureTestingModule({
+ imports: [ToggleGroupModule],
+ declarations: [TestApp],
+ providers: [{ provide: ToggleGroupComponent, useValue: mockGroupComponent }],
+ });
+
+ TestBed.compileComponents();
+ fixture = TestBed.createComponent(TestApp);
+ testAppComponent = fixture.debugElement.componentInstance;
+ radioButton = fixture.debugElement.query(By.css("input[type=radio]")).nativeElement;
+ }));
+
+ it("should emit value when clicking on radio button", () => {
+ testAppComponent.value = "value";
+ fixture.detectChanges();
+
+ radioButton.click();
+ fixture.detectChanges();
+
+ expect(mockGroupComponent.onInputInteraction).toHaveBeenCalledWith("value");
+ });
+
+ it("should check radio button when selected matches value", () => {
+ testAppComponent.value = "value";
+ fixture.detectChanges();
+
+ mockGroupComponent.selected = "value";
+ fixture.detectChanges();
+
+ expect(radioButton.checked).toBe(true);
+ });
+
+ it("should not check radio button when selected does not match value", () => {
+ testAppComponent.value = "value";
+ fixture.detectChanges();
+
+ mockGroupComponent.selected = "nonMatchingValue";
+ fixture.detectChanges();
+
+ expect(radioButton.checked).toBe(false);
+ });
+});
+
+class MockedButtonGroupComponent implements Partial {
+ onInputInteraction = jest.fn();
+ selected = null;
+}
+
+@Component({
+ selector: "test-app",
+ template: ` Element`,
+})
+class TestApp {
+ value?: string;
+}
diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts
new file mode 100644
index 0000000000..557ab38b38
--- /dev/null
+++ b/libs/components/src/toggle-group/toggle.component.ts
@@ -0,0 +1,80 @@
+import { HostBinding, Component, Input } from "@angular/core";
+
+import { ToggleGroupComponent } from "./toggle-group.component";
+
+let nextId = 0;
+
+@Component({
+ selector: "bit-toggle",
+ templateUrl: "./toggle.component.html",
+ preserveWhitespaces: false,
+})
+export class ToggleComponent {
+ id = nextId++;
+
+ @Input() value?: string;
+
+ constructor(private groupComponent: ToggleGroupComponent) {}
+
+ @HostBinding("tabIndex") tabIndex = "-1";
+ @HostBinding("class") classList = ["tw-group"];
+
+ get name() {
+ return this.groupComponent.name;
+ }
+
+ get selected() {
+ return this.groupComponent.selected === this.value;
+ }
+
+ get inputClasses() {
+ return ["tw-peer", "tw-appearance-none", "tw-outline-none"];
+ }
+
+ get labelClasses() {
+ return [
+ "!tw-font-semibold",
+ "tw-transition",
+ "tw-text-center",
+ "tw-border-text-muted",
+ "!tw-text-muted",
+ "tw-border-solid",
+ "tw-border-y",
+ "tw-border-r",
+ "tw-border-l-0",
+ "tw-cursor-pointer",
+ "group-first-of-type:tw-border-l",
+ "group-first-of-type:tw-rounded-l",
+ "group-last-of-type:tw-rounded-r",
+
+ "peer-focus:tw-outline-none",
+ "peer-focus:tw-ring",
+ "peer-focus:tw-ring-offset-2",
+ "peer-focus:tw-ring-primary-500",
+ "peer-focus:tw-z-10",
+ "peer-focus:tw-bg-primary-500",
+ "peer-focus:tw-border-primary-500",
+ "peer-focus:!tw-text-contrast",
+
+ "hover:tw-no-underline",
+ "hover:tw-bg-text-muted",
+ "hover:tw-border-text-muted",
+ "hover:!tw-text-contrast",
+
+ "peer-checked:tw-bg-primary-500",
+ "peer-checked:tw-border-primary-500",
+ "peer-checked:!tw-text-contrast",
+ "tw-py-1.5",
+ "tw-px-3",
+
+ // Fix for badge being pushed slightly lower when inside a button.
+ // Insipired by bootstrap, which does the same.
+ "[&>[bitBadge]]:tw-relative",
+ "[&>[bitBadge]]:-tw-top-[1px]",
+ ];
+ }
+
+ onInputInteraction() {
+ this.groupComponent.onInputInteraction(this.value);
+ }
+}