diff --git a/src/core/live-announcer/README.md b/src/core/live-announcer/README.md
new file mode 100644
index 000000000000..9c2e99a6e441
--- /dev/null
+++ b/src/core/live-announcer/README.md
@@ -0,0 +1,30 @@
+# MdLiveAnnouncer
+`MdLiveAnnouncer` is a service, which announces messages to several screenreaders.
+
+### Methods
+
+| Name | Description |
+| --- | --- |
+| `announce(message, politeness)` | This announces a text message to the supported screenreaders.
The politeness parameter sets the `aria-live` attribute on the announcer element |
+
+### Examples
+The service can be injected in a component.
+```ts
+@Component({
+ selector: 'my-component'
+ providers: [MdLiveAnnouncer]
+})
+export class MyComponent {
+
+ constructor(live: MdLiveAnnouncer) {
+ live.announce("Hey Google");
+ }
+
+}
+```
+
+### Supported Screenreaders
+- JAWS (Windows)
+- NVDA (Windows)
+- VoiceOver (OSX and iOS)
+- TalkBack (Android)
diff --git a/src/core/live-announcer/live-announcer.scss b/src/core/live-announcer/live-announcer.scss
new file mode 100644
index 000000000000..c4eeae7d2682
--- /dev/null
+++ b/src/core/live-announcer/live-announcer.scss
@@ -0,0 +1,5 @@
+@import 'mixins';
+
+.md-live-announcer {
+ @include md-visually-hidden();
+}
\ No newline at end of file
diff --git a/src/core/live-announcer/live-announcer.spec.ts b/src/core/live-announcer/live-announcer.spec.ts
new file mode 100644
index 000000000000..242a3ef4edc4
--- /dev/null
+++ b/src/core/live-announcer/live-announcer.spec.ts
@@ -0,0 +1,121 @@
+import {
+ inject,
+ TestComponentBuilder,
+ ComponentFixture,
+ fakeAsync,
+ flushMicrotasks,
+ tick,
+ beforeEachProviders
+} from 'angular2/testing';
+import {
+ it,
+ describe,
+ expect,
+ beforeEach,
+} from '../../core/facade/testing';
+import {Component} from 'angular2/core';
+import {By} from 'angular2/platform/browser';
+import {MdLiveAnnouncer} from './live-announcer';
+
+export function main() {
+ describe('MdLiveAnnouncer', () => {
+ let live: MdLiveAnnouncer;
+ let builder: TestComponentBuilder;
+ let liveEl: Element;
+
+ beforeEachProviders(() => [MdLiveAnnouncer]);
+
+ beforeEach(inject([TestComponentBuilder, MdLiveAnnouncer],
+ (tcb: TestComponentBuilder, _live: MdLiveAnnouncer) => {
+ builder = tcb;
+ live = _live;
+ liveEl = getLiveElement();
+ }));
+
+ afterEach(() => {
+ // In our tests we always remove the current live element, because otherwise we would have
+ // multiple live elements due multiple service instantiations.
+ liveEl.parentNode.removeChild(liveEl);
+ });
+
+ it('should correctly update the announce text', fakeAsyncTest(() => {
+ let appFixture: ComponentFixture = null;
+
+ builder.createAsync(TestApp).then(fixture => {
+ appFixture = fixture;
+ });
+
+ flushMicrotasks();
+
+ let buttonElement = appFixture.debugElement
+ .query(By.css('button')).nativeElement;
+
+ buttonElement.click();
+
+ // This flushes our 100ms timeout for the screenreaders.
+ tick(100);
+
+ expect(liveEl.textContent).toBe('Test');
+ }));
+
+ it('should correctly update the politeness attribute', fakeAsyncTest(() => {
+ let appFixture: ComponentFixture = null;
+
+ builder.createAsync(TestApp).then(fixture => {
+ appFixture = fixture;
+ });
+
+ flushMicrotasks();
+
+ live.announce('Hey Google', 'assertive');
+
+ // This flushes our 100ms timeout for the screenreaders.
+ tick(100);
+
+ expect(liveEl.textContent).toBe('Hey Google');
+ expect(liveEl.getAttribute('aria-live')).toBe('assertive');
+ }));
+
+ it('should apply the aria-live value polite by default', fakeAsyncTest(() => {
+ let appFixture: ComponentFixture = null;
+
+ builder.createAsync(TestApp).then(fixture => {
+ appFixture = fixture;
+ });
+
+ flushMicrotasks();
+
+ live.announce('Hey Google');
+
+ // This flushes our 100ms timeout for the screenreaders.
+ tick(100);
+
+ expect(liveEl.textContent).toBe('Hey Google');
+ expect(liveEl.getAttribute('aria-live')).toBe('polite');
+ }));
+
+ });
+}
+
+function fakeAsyncTest(fn: () => void) {
+ return inject([], fakeAsync(fn));
+}
+
+function getLiveElement(): Element {
+ return document.body.querySelector('.md-live-announcer');
+}
+
+@Component({
+ selector: 'test-app',
+ template: ``,
+})
+class TestApp {
+
+ constructor(private live: MdLiveAnnouncer) {};
+
+ announceText(message: string) {
+ this.live.announce(message);
+ }
+
+}
+
diff --git a/src/core/live-announcer/live-announcer.ts b/src/core/live-announcer/live-announcer.ts
new file mode 100644
index 000000000000..560145ca69c8
--- /dev/null
+++ b/src/core/live-announcer/live-announcer.ts
@@ -0,0 +1,44 @@
+import {Injectable} from 'angular2/core';
+
+export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
+
+@Injectable()
+export class MdLiveAnnouncer {
+
+ private _liveElement: Element;
+
+ constructor() {
+ this._liveElement = this._createLiveElement();
+ }
+
+ /**
+ * @param message Message to be announced to the screenreader
+ * @param politeness The politeness of the announcer element.
+ */
+ announce(message: string, politeness: AriaLivePoliteness = 'polite'): void {
+ this._liveElement.textContent = '';
+
+ // TODO: ensure changing the politeness works on all environments we support.
+ this._liveElement.setAttribute('aria-live', politeness);
+
+ // This 100ms timeout is necessary for some browser + screen-reader combinations:
+ // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
+ // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
+ // second time without clearing and then using a non-zero delay.
+ // (using JAWS 17 at time of this writing).
+ setTimeout(() => this._liveElement.textContent = message, 100);
+ }
+
+ private _createLiveElement(): Element {
+ let liveEl = document.createElement('div');
+
+ liveEl.classList.add('md-live-announcer');
+ liveEl.setAttribute('aria-atomic', 'true');
+ liveEl.setAttribute('aria-live', 'polite');
+
+ document.body.appendChild(liveEl);
+
+ return liveEl;
+ }
+
+}
diff --git a/src/core/style/_mixins.scss b/src/core/style/_mixins.scss
index e809739c037f..b9445b392c7a 100644
--- a/src/core/style/_mixins.scss
+++ b/src/core/style/_mixins.scss
@@ -9,3 +9,19 @@
// Use a transform to create a new stacking context.
transform: translate3D(0, 0, 0);
}
+
+/**
+ * This mixin hides an element visually.
+ * That means it's still accessible for screen-readers but not visible in view.
+ */
+@mixin md-visually-hidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ text-transform: none;
+ width: 1px;
+}
\ No newline at end of file
diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html
index 9ce3d2de0ea1..1b2a4e037789 100644
--- a/src/demo-app/demo-app.html
+++ b/src/demo-app/demo-app.html
@@ -12,6 +12,7 @@