diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts
index 4d20d1ee3047..9c7b7ac2daba 100644
--- a/src/demo-app/demo-app-module.ts
+++ b/src/demo-app/demo-app-module.ts
@@ -29,6 +29,7 @@ import {MdCheckboxDemoNestedChecklist, CheckboxDemo} from './checkbox/checkbox-d
import {SelectDemo} from './select/select-demo';
import {SliderDemo} from './slider/slider-demo';
import {SidenavDemo} from './sidenav/sidenav-demo';
+import {SnackBarDemo} from './snack-bar/snack-bar-demo';
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
import {MenuDemo} from './menu/menu-demo';
import {TabsDemo} from './tabs/tab-group-demo';
@@ -61,6 +62,7 @@ import {TabsDemo} from './tabs/tab-group-demo';
LiveAnnouncerDemo,
MdCheckboxDemoNestedChecklist,
MenuDemo,
+ SnackBarDemo,
OverlayDemo,
PortalDemo,
ProgressBarDemo,
diff --git a/src/demo-app/demo-app/demo-app.html b/src/demo-app/demo-app/demo-app.html
index ff618689e032..15084fbcdc9f 100644
--- a/src/demo-app/demo-app/demo-app.html
+++ b/src/demo-app/demo-app/demo-app.html
@@ -23,6 +23,7 @@
Sidenav
Slider
Slide Toggle
+ Snack Bar
Tabs
Toolbar
Tooltip
diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts
index 03dbfd0ec3ac..b7c3fa47f3a7 100644
--- a/src/demo-app/demo-app/routes.ts
+++ b/src/demo-app/demo-app/routes.ts
@@ -26,6 +26,7 @@ import {MenuDemo} from '../menu/menu-demo';
import {RippleDemo} from '../ripple/ripple-demo';
import {DialogDemo} from '../dialog/dialog-demo';
import {TooltipDemo} from '../tooltip/tooltip-demo';
+import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
export const DEMO_APP_ROUTES: Routes = [
@@ -56,4 +57,5 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'ripple', component: RippleDemo},
{path: 'dialog', component: DialogDemo},
{path: 'tooltip', component: TooltipDemo},
+ {path: 'snack-bar', component: SnackBarDemo},
];
diff --git a/src/demo-app/snack-bar/snack-bar-demo.html b/src/demo-app/snack-bar/snack-bar-demo.html
new file mode 100644
index 000000000000..232a83f6a643
--- /dev/null
+++ b/src/demo-app/snack-bar/snack-bar-demo.html
@@ -0,0 +1,15 @@
+
SnackBar demo
+
+
Message:
+
+
+ Show button on snack bar
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/demo-app/snack-bar/snack-bar-demo.scss b/src/demo-app/snack-bar/snack-bar-demo.scss
new file mode 100644
index 000000000000..8d737ab226f6
--- /dev/null
+++ b/src/demo-app/snack-bar/snack-bar-demo.scss
@@ -0,0 +1,3 @@
+.demo-button-label-input {
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/src/demo-app/snack-bar/snack-bar-demo.ts b/src/demo-app/snack-bar/snack-bar-demo.ts
new file mode 100644
index 000000000000..a684a2e39bd4
--- /dev/null
+++ b/src/demo-app/snack-bar/snack-bar-demo.ts
@@ -0,0 +1,31 @@
+import {Component, ViewContainerRef} from '@angular/core';
+import {MdSnackBar, MdSnackBarConfig} from '@angular/material';
+
+@Component({
+ moduleId: module.id,
+ selector: 'snack-bar-demo',
+ templateUrl: 'snack-bar-demo.html',
+})
+export class SnackBarDemo {
+ message: string = 'Snack Bar opened.';
+ actionButtonLabel: string = 'Retry';
+ action: boolean = false;
+
+ constructor(
+ public snackBar: MdSnackBar,
+ public viewContainerRef: ViewContainerRef) { }
+
+ open() {
+ let config = new MdSnackBarConfig(this.viewContainerRef);
+ this.snackBar.open(this.message, this.action && this.actionButtonLabel, config);
+ }
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: 'demo-snack',
+ templateUrl: 'snack-bar-demo.html',
+ styleUrls: ['./snack-bar-demo.css'],
+})
+export class DemoSnack {}
diff --git a/src/lib/index.ts b/src/lib/index.ts
index ead3f4846688..027498aea622 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -18,6 +18,7 @@ export * from './select/index';
export * from './sidenav/index';
export * from './slider/index';
export * from './slide-toggle/index';
+export * from './snack-bar/index';
export * from './tabs/index';
export * from './toolbar/index';
export * from './tooltip/index';
diff --git a/src/lib/module.ts b/src/lib/module.ts
index 4a13daae74a9..ade62b770cec 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -23,6 +23,7 @@ import {MdIconModule} from './icon/index';
import {MdProgressCircleModule} from './progress-circle/index';
import {MdProgressBarModule} from './progress-bar/index';
import {MdInputModule} from './input/index';
+import {MdSnackBarModule} from './snack-bar/snack-bar';
import {MdTabsModule} from './tabs/index';
import {MdToolbarModule} from './toolbar/index';
import {MdTooltipModule} from './tooltip/index';
@@ -49,6 +50,7 @@ const MATERIAL_MODULES = [
MdSidenavModule,
MdSliderModule,
MdSlideToggleModule,
+ MdSnackBarModule,
MdTabsModule,
MdToolbarModule,
MdTooltipModule,
@@ -83,6 +85,7 @@ const MATERIAL_MODULES = [
MdRadioModule.forRoot(),
MdSliderModule.forRoot(),
MdSlideToggleModule.forRoot(),
+ MdSnackBarModule.forRoot(),
MdTooltipModule.forRoot(),
OverlayModule.forRoot(),
],
diff --git a/src/lib/snack-bar/README.md b/src/lib/snack-bar/README.md
new file mode 100644
index 000000000000..33b2a4b0bee3
--- /dev/null
+++ b/src/lib/snack-bar/README.md
@@ -0,0 +1,38 @@
+# MdSnackBar
+`MdSnackBar` is a service, which opens snack bar notifications in the view.
+
+### Methods
+
+| Name | Description |
+| --- | --- |
+| `open(message: string,
actionLabel: string, config: MdSnackBarConfig): MdSnackBarRef` | Creates and opens a simple snack bar noticiation matching material spec. |
+| `openFromComponent(component: ComponentType, config: MdSnackBarConfig): MdSnackBarRef` | Creates and opens a snack bar noticiation with a custom component as content. |
+
+### Config
+
+| Key | Description |
+| --- | --- |
+| `viewContainerRef: ViewContainerRef` | The view container ref to attach the snack bar to. |
+| `role: AriaLivePoliteness = 'assertive'` | The politeness level to announce the snack bar with. |
+| `announcementMessage: string` | The message to announce with the snack bar. |
+
+
+### Example
+The service can be injected in a component.
+```ts
+@Component({
+ selector: 'my-component'
+ providers: [MdSnackBar]
+})
+export class MyComponent {
+
+ constructor(snackBar: MdSnackBar
+ viewContainerRef: ViewContainerRef) {}
+
+ failedAttempt() {
+ config = new MdSnackBarConfig(this.viewContainerRef);
+ this.snackBar.open('It didn\'t quite work!', 'Try Again', config);
+ }
+
+}
+```
\ No newline at end of file
diff --git a/src/lib/snack-bar/index.ts b/src/lib/snack-bar/index.ts
new file mode 100644
index 000000000000..259852618083
--- /dev/null
+++ b/src/lib/snack-bar/index.ts
@@ -0,0 +1 @@
+export * from './snack-bar';
diff --git a/src/lib/snack-bar/simple-snack-bar.html b/src/lib/snack-bar/simple-snack-bar.html
new file mode 100644
index 000000000000..9e1fa7da019c
--- /dev/null
+++ b/src/lib/snack-bar/simple-snack-bar.html
@@ -0,0 +1,3 @@
+{{message}}
+
\ No newline at end of file
diff --git a/src/lib/snack-bar/simple-snack-bar.scss b/src/lib/snack-bar/simple-snack-bar.scss
new file mode 100644
index 000000000000..9935cd92950e
--- /dev/null
+++ b/src/lib/snack-bar/simple-snack-bar.scss
@@ -0,0 +1,28 @@
+md-simple-snackbar {
+ display: flex;
+ justify-content: space-between;
+}
+
+.md-simple-snackbar-message {
+ box-sizing: border-box;
+ border: none;
+ color: white;
+ font-family: Roboto, 'Helvetica Neue', sans-serif;
+ font-size: 14px;
+ line-height: 20px;
+ outline: none;
+ text-decoration: none;
+ word-break: break-all;
+}
+
+.md-simple-snackbar-action {
+ box-sizing: border-box;
+ color: white;
+ float: right;
+ font-weight: 600;
+ line-height: 20px;
+ margin: -5px 0 0 48px;
+ min-width: initial;
+ padding: 5px;
+ text-transform: uppercase;
+}
\ No newline at end of file
diff --git a/src/lib/snack-bar/simple-snack-bar.ts b/src/lib/snack-bar/simple-snack-bar.ts
new file mode 100644
index 000000000000..d74d6736c3df
--- /dev/null
+++ b/src/lib/snack-bar/simple-snack-bar.ts
@@ -0,0 +1,32 @@
+import {Component} from '@angular/core';
+import {MdSnackBarRef} from './snack-bar-ref';
+
+
+/**
+ * A component used to open as the default snack bar, matching material spec.
+ * This should only be used internally by the snack bar service.
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'simple-snack-bar',
+ templateUrl: 'simple-snack-bar.html',
+ styleUrls: ['simple-snack-bar.css'],
+})
+export class SimpleSnackBar {
+ /** The message to be shown in the snack bar. */
+ message: string;
+
+ /** The label for the button in the snack bar. */
+ action: string;
+
+ /** The instance of the component making up the content of the snack bar. */
+ snackBarRef: MdSnackBarRef;
+
+ /** Dismisses the snack bar. */
+ dismiss(): void {
+ this.snackBarRef.dismiss();
+ }
+
+ /** If the action button should be shown. */
+ get hasAction(): boolean { return !!this.action; }
+}
diff --git a/src/lib/snack-bar/snack-bar-config.ts b/src/lib/snack-bar/snack-bar-config.ts
new file mode 100644
index 000000000000..8eaebb8202cc
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-config.ts
@@ -0,0 +1,18 @@
+import {ViewContainerRef} from '@angular/core';
+import {AriaLivePoliteness} from '../core';
+
+
+export class MdSnackBarConfig {
+ /** The politeness level for the MdAriaLiveAnnouncer announcement. */
+ politeness: AriaLivePoliteness = 'assertive';
+
+ /** Message to be announced by the MdAriaLiveAnnouncer */
+ announcementMessage: string;
+
+ /** The view container to place the overlay for the snack bar into. */
+ viewContainerRef: ViewContainerRef;
+
+ constructor(viewContainerRef: ViewContainerRef) {
+ this.viewContainerRef = viewContainerRef;
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar-container.html b/src/lib/snack-bar/snack-bar-container.html
new file mode 100644
index 000000000000..23e1d44627f8
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-container.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/snack-bar/snack-bar-container.scss b/src/lib/snack-bar/snack-bar-container.scss
new file mode 100644
index 000000000000..dd5c4b48dd06
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-container.scss
@@ -0,0 +1,19 @@
+@import '../core/style/elevation';
+
+$md-snack-bar-padding: 14px 24px !default;
+$md-snack-bar-height: 20px !default;
+$md-snack-bar-min-width: 288px !default;
+$md-snack-bar-max-width: 568px !default;
+
+
+:host {
+ @include md-elevation(24);
+ background: #323232;
+ border-radius: 2px;
+ display: block;
+ height: $md-snack-bar-height;
+ max-width: $md-snack-bar-max-width;
+ min-width: $md-snack-bar-min-width;
+ overflow: hidden;
+ padding: $md-snack-bar-padding;
+}
\ No newline at end of file
diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts
new file mode 100644
index 000000000000..3ef2d71fc592
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-container.ts
@@ -0,0 +1,47 @@
+import {
+ Component,
+ ComponentRef,
+ ViewChild
+} from '@angular/core';
+import {
+ BasePortalHost,
+ ComponentPortal,
+ TemplatePortal,
+ PortalHostDirective
+} from '../core';
+import {MdSnackBarConfig} from './snack-bar-config';
+import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors';
+
+
+/**
+ * Internal component that wraps user-provided snack bar content.
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'snack-bar-container',
+ templateUrl: 'snack-bar-container.html',
+ styleUrls: ['snack-bar-container.css'],
+ host: {
+ 'role': 'alert'
+ }
+})
+export class MdSnackBarContainer extends BasePortalHost {
+ /** The portal host inside of this container into which the snack bar content will be loaded. */
+ @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
+
+ /** The snack bar configuration. */
+ snackBarConfig: MdSnackBarConfig;
+
+ /** Attach a portal as content to this snack bar container. */
+ attachComponentPortal(portal: ComponentPortal): ComponentRef {
+ if (this._portalHost.hasAttached()) {
+ throw new MdSnackBarContentAlreadyAttached();
+ }
+
+ return this._portalHost.attachComponentPortal(portal);
+ }
+
+ attachTemplatePortal(portal: TemplatePortal): Map {
+ throw Error('Not yet implemented');
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar-errors.ts b/src/lib/snack-bar/snack-bar-errors.ts
new file mode 100644
index 000000000000..abd3fb3892ff
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-errors.ts
@@ -0,0 +1,8 @@
+import {MdError} from '../core';
+
+
+export class MdSnackBarContentAlreadyAttached extends MdError {
+ constructor() {
+ super('Attempting to attach snack bar content after content is already attached');
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar-ref.ts b/src/lib/snack-bar/snack-bar-ref.ts
new file mode 100644
index 000000000000..10453bdf772d
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-ref.ts
@@ -0,0 +1,35 @@
+import {OverlayRef} from '../core';
+import {Observable} from 'rxjs/Observable';
+import {Subject} from 'rxjs/Subject';
+
+// TODO(josephperrott): Implement onAction observable.
+
+
+/**
+ * Reference to a snack bar dispatched from the snack bar service.
+ */
+export class MdSnackBarRef {
+ /** The instance of the component making up the content of the snack bar. */
+ readonly instance: T;
+
+ /** Subject for notifying the user that the snack bar has closed. */
+ private _afterClosed: Subject = new Subject();
+
+ constructor(instance: T, private _overlayRef: OverlayRef) {
+ // Sets the readonly instance of the snack bar content component.
+ this.instance = instance;
+ }
+
+ /** Dismisses the snack bar. */
+ dismiss(): void {
+ if (!this._afterClosed.closed) {
+ this._overlayRef.dispose();
+ this._afterClosed.complete();
+ }
+ }
+
+ /** Gets an observable that is notified when the snack bar is finished closing. */
+ afterDismissed(): Observable {
+ return this._afterClosed.asObservable();
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar.spec.ts b/src/lib/snack-bar/snack-bar.spec.ts
new file mode 100644
index 000000000000..574346af4470
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar.spec.ts
@@ -0,0 +1,178 @@
+import {
+ inject,
+ async,
+ ComponentFixture,
+ TestBed
+} from '@angular/core/testing';
+import {
+ NgModule,
+ Component,
+ Directive,
+ ViewChild,
+ ViewContainerRef
+} from '@angular/core';
+import {MdSnackBar, MdSnackBarModule} from './snack-bar';
+import {OverlayContainer} from '../core';
+import {MdSnackBarConfig} from './snack-bar-config';
+import {SimpleSnackBar} from './simple-snack-bar';
+
+
+describe('MdSnackBar', () => {
+ let snackBar: MdSnackBar;
+ let overlayContainerElement: HTMLElement;
+
+ let testViewContainerRef: ViewContainerRef;
+ let viewContainerFixture: ComponentFixture;
+
+ let simpleMessage = 'Burritos are here!';
+ let simpleActionLabel = 'pickup';
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdSnackBarModule.forRoot(), SnackBarTestModule],
+ providers: [
+ {provide: OverlayContainer, useFactory: () => {
+ overlayContainerElement = document.createElement('div');
+ return {getContainerElement: () => overlayContainerElement};
+ }}
+ ],
+ });
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(inject([MdSnackBar], (sb: MdSnackBar) => {
+ snackBar = sb;
+ }));
+
+ beforeEach(() => {
+ viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
+
+ viewContainerFixture.detectChanges();
+ testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
+ });
+
+ it('should have the role of alert', () => {
+ let config = new MdSnackBarConfig(testViewContainerRef);
+ snackBar.open(simpleMessage, simpleActionLabel, config);
+
+ let containerElement = overlayContainerElement.querySelector('snack-bar-container');
+
+ expect(containerElement.getAttribute('role'))
+ .toBe('alert', 'Expected snack bar container to have role="alert"');
+ });
+
+ it('should open a simple message with a button', () => {
+ let config = new MdSnackBarConfig(testViewContainerRef);
+ let snackBarRef = snackBar.open(simpleMessage, simpleActionLabel, config);
+
+ viewContainerFixture.detectChanges();
+
+ expect(snackBarRef.instance)
+ .toEqual(jasmine.any(SimpleSnackBar),
+ 'Expected the snack bar content component to be SimpleSnackBar');
+ expect(snackBarRef.instance.snackBarRef)
+ .toBe(snackBarRef, 'Expected the snack bar reference to be placed in the component instance');
+
+ let messageElement = overlayContainerElement.querySelector('span.md-simple-snackbar-message');
+ expect(messageElement.tagName).toBe('SPAN', 'Expected snack bar message element to be ');
+ expect(messageElement.textContent)
+ .toBe(simpleMessage, `Expected the snack bar message to be '${simpleMessage}''`);
+
+ let buttonElement = overlayContainerElement.querySelector('button.md-simple-snackbar-action');
+ expect(buttonElement.tagName)
+ .toBe('BUTTON', 'Expected snack bar action label to be a