diff --git a/src/Transition.js b/src/Transition.js
new file mode 100644
index 0000000000..ce8eb200cf
--- /dev/null
+++ b/src/Transition.js
@@ -0,0 +1,286 @@
+'use strict';
+import React from 'react';
+import TransitionEvents from './utils/TransitionEvents';
+import classnames from 'classnames';
+
+function omit(obj, keys) {
+ let included = Object.keys(obj).filter( k => keys.indexOf(k) === -1);
+ let newObj = {};
+
+ included.forEach( key => newObj[key] = obj[key] );
+ return newObj;
+}
+
+function ensureTransitionEnd(node, handler, duration){
+ let fired = false;
+ let done = e => {
+ if (!fired) {
+ fired = true;
+ handler(e);
+ }
+ };
+
+ if ( node ) {
+ TransitionEvents.addEndEventListener(node, done);
+ setTimeout(done, duration);
+ } else {
+ setTimeout(done, 0);
+ }
+}
+
+// reading a dimension prop will cause the browser to recalculate,
+// which will let our animations work
+let triggerBrowserReflow = node => node.offsetHeight; //eslint-disable-line no-unused-expressions
+
+class Transition extends React.Component {
+
+ constructor(props, context){
+ super(props, context);
+
+ this.state = {
+ in: !props.in,
+ transitioning: false
+ };
+
+ this.needsTransition = true;
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.in !== this.props.in) {
+ this.needsTransition = true;
+ }
+ }
+
+ componentDidUpdate() {
+ this.processChild();
+ }
+
+ componentWillMount() {
+ this._mounted = true;
+
+ if (!this.props.transitionAppear) {
+ this.needsTransition = false;
+ this.setState({ in: this.props.in });
+ }
+ }
+
+ componentWillUnmount(){
+ this._mounted = false;
+ }
+
+ componentDidMount() {
+ if (this.props.transitionAppear) {
+ this.processChild();
+ }
+ }
+
+ processChild(){
+ let needsTransition = this.needsTransition;
+ let enter = this.props.in;
+
+ if (needsTransition) {
+ this.needsTransition = false;
+ this[enter ? 'performEnter' : 'performLeave']();
+ }
+ }
+
+ performEnter() {
+ let maybeNode = React.findDOMNode(this);
+
+ let enter = node => {
+ node = this.props.transitioningNode(node) || node;
+
+ this.props.onEnter(node);
+
+ this.safeSetState({ in: true, transitioning: true, needInitialRender: false }, ()=> {
+
+ this.props.onEntering(node);
+
+ ensureTransitionEnd(node, () => {
+ if ( this.state.in ){
+ this.safeSetState({
+ transitioning: false
+ }, () => this.props.onEntered(node));
+ }
+
+ }, this.props.duration);
+ });
+ };
+
+ if (maybeNode) {
+ enter(maybeNode);
+ }
+ else if (this.props.unmountOnExit) {
+ this._ensureNode(enter);
+ }
+ }
+
+ performLeave() {
+ let node = React.findDOMNode(this);
+
+ node = this.props.transitioningNode(node) || node;
+
+ this.props.onExit(node);
+
+ this.setState({ in: false, transitioning: true }, () => {
+ this.props.onExiting(node);
+
+ ensureTransitionEnd(node, () => {
+ if ( !this.state.in ){
+ this.safeSetState({ transitioning: false }, ()=> this.props.onExited(node));
+ }
+ }, this.props.duration);
+ });
+ }
+
+ _ensureNode(callback) {
+
+ this.setState({ needInitialRender: true }, ()=> {
+ let node = React.findDOMNode(this);
+
+ triggerBrowserReflow(node);
+
+ callback(node);
+ });
+ }
+
+ safeSetState(newState, cb){
+ if (this._mounted) {
+ this.setState(newState, cb);
+ }
+ }
+
+ render() {
+ let childProps = omit(this.props, Object.keys(Transition.propTypes).concat('children'));
+
+ let child = this.props.children;
+ let starting = this.state.needInitialRender;
+ let out = !this.state.in && !this.state.transitioning;
+
+ if ( !child || (this.props.unmountOnExit && out && !starting) ){
+ return null;
+ }
+
+ let classes = '';
+
+ // for whatever reason classnames() doesn't actually work here,
+ // maybe because they aren't always single classes?
+ if (this.state.in && !this.state.transitioning) {
+ classes = this.props.enteredClassName;
+ }
+
+ else if (this.state.in && this.state.transitioning) {
+ classes = this.props.enteringClassName;
+ }
+
+ else if (!this.state.in && !this.state.transitioning) {
+ classes = this.props.exitedClassName;
+ }
+
+ else if (!this.state.in && this.state.transitioning) {
+ classes = this.props.exitingClassName;
+ }
+
+ return React.cloneElement(child, {
+ ...childProps,
+ className: classnames(
+ child.props.className
+ , this.props.className
+ , classes)
+ });
+ }
+}
+
+Transition.propTypes = {
+ /**
+ * Triggers the Enter or Exit animation
+ */
+ in: React.PropTypes.bool,
+
+ /**
+ * Specify whether the transitioning component should be unmounted (removed from the DOM) once the exit animation finishes.
+ */
+ unmountOnExit: React.PropTypes.bool,
+
+ /**
+ * Specify whether transitions should run when the Transition component mounts.
+ */
+ transitionAppear: React.PropTypes.bool,
+
+ /**
+ * Provide the durration of the animation in milliseconds, used to ensure that finishing callbacks are fired even if the
+ * original browser transition end events are canceled.
+ */
+ duration: React.PropTypes.number,
+
+ /**
+ * A css class or classes applied once the Component has exited.
+ */
+ exitedClassName: React.PropTypes.string,
+ /**
+ * A css class or classes applied while the Component is exiting.
+ */
+ exitingClassName: React.PropTypes.string,
+ /**
+ * A css class or classes applied once the Component has entered.
+ */
+ enteredClassName: React.PropTypes.string,
+ /**
+ * A css class or classes applied while the Component is entering.
+ */
+ enteringClassName: React.PropTypes.string,
+
+ /**
+ * A function that returns the DOM node to animate. This Node will have the transition classes applied to it.
+ * When left out, the Component will use its immediate child.
+ *
+ * @private
+ */
+ transitioningNode: React.PropTypes.func,
+
+ /**
+ * A callback fired just before the "entering" classes are applied
+ */
+ onEnter: React.PropTypes.func,
+ /**
+ * A callback fired just after the "entering" classes are applied
+ */
+ onEntering: React.PropTypes.func,
+ /**
+ * A callback fired after "enter" classes are applied
+ */
+ onEntered: React.PropTypes.func,
+ /**
+ * A callback fired after "exiting" classes are applied
+ */
+ onExit: React.PropTypes.func,
+ /**
+ * A callback fired after "exiting" classes are applied
+ */
+ onExiting: React.PropTypes.func,
+ /**
+ * A callback fired after "exit" classes are applied
+ */
+ onExited: React.PropTypes.func
+};
+
+// name the function so it is clearer in the documentation
+const noop = ()=>{};
+
+Transition.defaultProps = {
+ in: false,
+ duration: 300,
+ unmountOnExit: false,
+ transitionAppear: false,
+ transitioningNode: noop,
+
+ onEnter: noop,
+ onEntering: noop,
+ onEntered: noop,
+
+ onExit: noop,
+ onExiting: noop,
+ onExited: noop
+};
+
+export default Transition;
diff --git a/test/TransitionSpec.js b/test/TransitionSpec.js
new file mode 100644
index 0000000000..7945ba8ceb
--- /dev/null
+++ b/test/TransitionSpec.js
@@ -0,0 +1,231 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import { render } from './helpers';
+import Transition from '../src/Transition';
+//import classNames from 'classnames';
+
+describe('Transition', function () {
+
+
+ it('should not transition on mount', function(){
+ let instance = render(
+ { throw new Error('should not Enter'); }}>
+
+
+ );
+
+ instance.state.in.should.equal(true);
+ assert.ok(!instance.state.transitioning);
+ });
+
+ it('should transition on mount with transitionAppear', done =>{
+ let instance = ReactTestUtils.renderIntoDocument(
+ done()}
+ >
+
+
+ );
+
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(true);
+ });
+
+ describe('entering', ()=> {
+ let instance;
+
+ beforeEach(function(){
+ instance = render(
+
+
+
+ );
+ });
+
+ it('should fire callbacks', done => {
+ let onEnter = sinon.spy();
+ let onEntering = sinon.spy();
+
+ instance.state.in.should.equal(false);
+
+ instance = instance.renderWithProps({
+
+ in: true,
+
+ onEnter,
+
+ onEntering,
+
+ onEntered(){
+ assert.ok(onEnter.calledOnce);
+ assert.ok(onEntering.calledOnce);
+ assert.ok(onEnter.calledBefore(onEntering));
+ done();
+ }
+ });
+ });
+
+ it('should move to each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(false);
+
+ instance = instance.renderWithProps({
+
+ in: true,
+
+ onEnter(){
+ count++;
+ instance.state.in.should.equal(false);
+ instance.state.transitioning.should.equal(false);
+ },
+
+ onEntering(){
+ count++;
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(true);
+ },
+
+ onEntered(){
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(false);
+ assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ it('should apply classes at each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(false);
+
+ instance = instance.renderWithProps({
+
+ in: true,
+
+ onEnter(node){
+ count++;
+ assert.equal(node.className, '');
+ },
+
+ onEntering(node){
+ count++;
+ assert.equal(node.className, 'test-entering');
+ },
+
+ onEntered(node){
+ assert.equal(node.className, 'test-enter');
+ assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ });
+
+
+ describe('exiting', ()=> {
+ let instance;
+
+ beforeEach(function(){
+ instance = render(
+
+
+
+ );
+ });
+
+ it('should fire callbacks', done => {
+ let onExit = sinon.spy();
+ let onExiting = sinon.spy();
+
+ instance.state.in.should.equal(true);
+
+ instance = instance.renderWithProps({
+
+ in: false,
+
+ onExit,
+
+ onExiting,
+
+ onExited(){
+ assert.ok(onExit.calledOnce);
+ assert.ok(onExiting.calledOnce);
+ assert.ok(onExit.calledBefore(onExiting));
+ done();
+ }
+ });
+ });
+
+ it('should move to each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(true);
+
+ instance = instance.renderWithProps({
+
+ in: false,
+
+ onExit(){
+ count++;
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(false);
+ },
+
+ onExiting(){
+ count++;
+ instance.state.in.should.equal(false);
+ instance.state.transitioning.should.equal(true);
+ },
+
+ onExited(){
+ instance.state.in.should.equal(false);
+ instance.state.transitioning.should.equal(false);
+ //assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ it('should apply classes at each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(true);
+
+ instance = instance.renderWithProps({
+
+ in: false,
+
+ onExit(node){
+ count++;
+ assert.equal(node.className, '');
+ },
+
+ onExiting(node){
+ count++;
+ assert.equal(node.className, 'test-exiting');
+ },
+
+ onExited(node){
+ assert.equal(node.className, 'test-exit');
+ assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ });
+
+});