diff --git a/aio/src/app/shared/select/select.component.html b/aio/src/app/shared/select/select.component.html
new file mode 100644
index 0000000000..54974b82c0
--- /dev/null
+++ b/aio/src/app/shared/select/select.component.html
@@ -0,0 +1,16 @@
+
diff --git a/aio/src/app/shared/select/select.component.spec.ts b/aio/src/app/shared/select/select.component.spec.ts
new file mode 100644
index 0000000000..5efaed1e50
--- /dev/null
+++ b/aio/src/app/shared/select/select.component.spec.ts
@@ -0,0 +1,165 @@
+import { Component, DebugElement } from '@angular/core';
+import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { SelectComponent, Option } from './select.component';
+
+const options = [
+ { title: 'Option A', value: 'option-a' },
+ { title: 'Option B', value: 'option-b' }
+];
+
+let component: SelectComponent;
+let host: HostComponent;
+let fixture: ComponentFixture;
+let element: DebugElement;
+
+describe('SelectComponent', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SelectComponent, HostComponent ],
+ });
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HostComponent);
+ host = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(SelectComponent));
+ component = element.componentInstance;
+ });
+
+ describe('(initially)', () => {
+ it('should show the button and no options', () => {
+ expect(getButton()).toBeDefined();
+ expect(getOptionContainer()).toEqual(null);
+ });
+ });
+
+ describe('button', () => {
+ it('should display the label if provided', () => {
+ expect(getButton().textContent.trim()).toEqual('');
+ host.label = 'Label:';
+ fixture.detectChanges();
+ expect(getButton().textContent.trim()).toEqual('Label:');
+ });
+
+ it('should contain a symbol `` if hasSymbol is true', () => {
+ expect(getButton().querySelector('span')).toEqual(null);
+ host.showSymbol = true;
+ fixture.detectChanges();
+ const span = getButton().querySelector('span');
+ expect(span).not.toEqual(null);
+ expect(span.className).toContain('symbol');
+ });
+
+ it('should display the selected option, if there is one', () => {
+ host.showSymbol = true;
+ host.selected = options[0];
+ fixture.detectChanges();
+ expect(getButton().textContent).toContain(options[0].title);
+ expect(getButton().querySelector('span').className).toContain(options[0].value);
+ });
+
+ it('should toggle the visibility of the options list when clicked', () => {
+ host.options = options;
+ getButton().click();
+ fixture.detectChanges();
+ expect(getOptionContainer()).not.toEqual(null);
+ getButton().click();
+ fixture.detectChanges();
+ expect(getOptionContainer()).toEqual(null);
+ });
+ });
+
+ describe('options list', () => {
+ beforeEach(() => {
+ host.options = options;
+ host.showSymbol = true;
+ getButton().click(); // ensure the the options are visible
+ fixture.detectChanges();
+ });
+
+ it('should show the corresponding title of each option', () => {
+ getOptions().forEach((li, index) => {
+ expect(li.textContent).toContain(options[index].title);
+ });
+ });
+
+ it('should select the option that is clicked', () => {
+ getOptions()[0].click();
+ fixture.detectChanges();
+ expect(host.onChange).toHaveBeenCalledWith({ option: options[0], index: 0 });
+ expect(getButton().textContent).toContain(options[0].title);
+ expect(getButton().querySelector('span').className).toContain(options[0].value);
+ });
+
+ it('should select the current option when enter is pressed', () => {
+ const e = new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key: 'Enter'});
+ getOptions()[0].dispatchEvent(e);
+ fixture.detectChanges();
+ expect(host.onChange).toHaveBeenCalledWith({ option: options[0], index: 0 });
+ expect(getButton().textContent).toContain(options[0].title);
+ expect(getButton().querySelector('span').className).toContain(options[0].value);
+ });
+
+ it('should select the current option when space is pressed', () => {
+ const e = new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key: ' '});
+ getOptions()[0].dispatchEvent(e);
+ fixture.detectChanges();
+ expect(host.onChange).toHaveBeenCalledWith({ option: options[0], index: 0 });
+ expect(getButton().textContent).toContain(options[0].title);
+ expect(getButton().querySelector('span').className).toContain(options[0].value);
+ });
+
+ it('should hide when an option is clicked', () => {
+ getOptions()[0].click();
+ fixture.detectChanges();
+ expect(getOptionContainer()).toEqual(null);
+ });
+
+ it('should hide when there is a click that is not on the option list', () => {
+ fixture.nativeElement.click();
+ fixture.detectChanges();
+ expect(getOptionContainer()).toEqual(null);
+ });
+
+ it('should hide if the escape button is pressed', () => {
+ const e = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Escape' });
+ document.dispatchEvent(e);
+ fixture.detectChanges();
+ expect(getOptionContainer()).toEqual(null);
+ });
+ });
+});
+
+
+
+@Component({
+ template: `
+
+ `
+})
+class HostComponent {
+ onChange = jasmine.createSpy('onChange');
+ options: Option[];
+ selected: Option;
+ label: string;
+ showSymbol: boolean;
+}
+
+function getButton(): HTMLButtonElement {
+ return element.query(By.css('button')).nativeElement;
+}
+
+function getOptionContainer(): HTMLUListElement {
+ const de = element.query(By.css('ul'));
+ return de && de.nativeElement;
+}
+
+function getOptions(): HTMLLIElement[] {
+ return element.queryAll(By.css('li')).map(de => de.nativeElement);
+}
diff --git a/aio/src/app/shared/select/select.component.ts b/aio/src/app/shared/select/select.component.ts
new file mode 100644
index 0000000000..11bc9913df
--- /dev/null
+++ b/aio/src/app/shared/select/select.component.ts
@@ -0,0 +1,62 @@
+import { Component, ElementRef, EventEmitter, HostListener, Input, Output, OnInit } from '@angular/core';
+
+export interface Option {
+ title: string;
+ value?: any;
+}
+
+@Component({
+ selector: 'aio-select',
+ templateUrl: 'select.component.html'
+})
+export class SelectComponent implements OnInit {
+ @Input()
+ selected: Option;
+
+ @Input()
+ options: Option[];
+
+ @Output()
+ change = new EventEmitter<{option: Option, index: number}>();
+
+ @Input()
+ showSymbol = false;
+
+ @Input()
+ label: string;
+
+ showOptions = false;
+
+ constructor(private hostElement: ElementRef) {}
+
+ ngOnInit() {
+ this.label = this.label || '';
+ }
+
+ toggleOptions() {
+ this.showOptions = !this.showOptions;
+ }
+
+ hideOptions() {
+ this.showOptions = false;
+ }
+
+ select(option: Option, index: number) {
+ this.selected = option;
+ this.change.emit({option, index});
+ this.hideOptions();
+ }
+
+ @HostListener('document:click', ['$event.target'])
+ onClick(eventTarget: HTMLElement) {
+ // Hide the options if we clicked outside the component
+ if (!this.hostElement.nativeElement.contains(eventTarget)) {
+ this.hideOptions();
+ }
+ }
+
+ @HostListener('document:keydown.escape')
+ onKeyDown() {
+ this.hideOptions();
+ }
+}
diff --git a/aio/src/app/shared/shared.module.ts b/aio/src/app/shared/shared.module.ts
new file mode 100644
index 0000000000..2f7eafc0d3
--- /dev/null
+++ b/aio/src/app/shared/shared.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SelectComponent } from './select/select.component';
+
+@NgModule({
+ imports: [
+ CommonModule
+ ],
+ exports: [
+ SelectComponent
+ ],
+ declarations: [
+ SelectComponent
+ ]
+})
+export class SharedModule {}
diff --git a/aio/src/styles/2-modules/_modules-dir.scss b/aio/src/styles/2-modules/_modules-dir.scss
index e75eeff395..c2ac7b096c 100644
--- a/aio/src/styles/2-modules/_modules-dir.scss
+++ b/aio/src/styles/2-modules/_modules-dir.scss
@@ -27,3 +27,4 @@
@import 'search-results';
@import 'subsection';
@import 'toc';
+ @import 'select-menu';
diff --git a/aio/src/styles/2-modules/_select-menu.scss b/aio/src/styles/2-modules/_select-menu.scss
new file mode 100644
index 0000000000..a93fa753e9
--- /dev/null
+++ b/aio/src/styles/2-modules/_select-menu.scss
@@ -0,0 +1,72 @@
+/* SELECT MENU */
+
+.form-select-menu {
+ position: relative;
+}
+
+.form-select-button {
+ background: $white;
+ box-shadow: 0 2px 2px rgba($black, 0.24), 0 0 2px rgba($black, 0.12);
+ box-sizing: border-box;
+ border: 1px solid $white;
+ color: $blue-grey-600;
+ font-size: 12px;
+ font-weight: 400;
+ height: 32px;
+ line-height: 32px;
+ outline: none;
+ padding: 0 16px;
+ text-align: left;
+ width: 100%;
+ cursor: pointer;
+
+ strong {
+ font-weight: 600;
+ margin-right: 8px;
+ text-transform: uppercase;
+ }
+
+ &:focus {
+ border: 1px solid $blue-400;
+ box-shadow: 0 2px 2px rgba($blue-400, 0.24), 0 0 2px rgba($blue-400, 0.12);
+ }
+}
+
+.form-select-dropdown {
+ background: $white;
+ box-shadow: 0 16px 16px rgba($black, 0.24), 0 0 16px rgba($black, 0.12);
+ border-radius: 4px;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ z-index: $layer-2;
+
+ li {
+ cursor: pointer;
+ font-size: 14px;
+ line-height: 32px;
+ margin: 0;
+ padding: 0 16px 0 40px;
+ position: relative;
+ transition: all .2s;
+
+ &:hover {
+ background: $blue-grey-50;
+ color: $blue-500;
+ }
+
+ &.selected {
+ background-color: $blue-grey-100;
+ }
+
+ .symbol {
+ left: 16px;
+ position: absolute;
+ top: 8px;
+ z-index: $layer-5;
+ }
+ }
+}