Repository: uber-web/baseui
Pull request #653 (2018-12-14 18:47:15)

feat(tabs): add tabs implementation
Add implementation of the tabs component

<img width="359" alt="screen shot 2018-12-15 at 12 15 42 am" src="https://user-images.githubusercontent.com/1387961/50021299-a3d19e80-fffe-11e8-819e-884f61e46e92.png">



CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/uber-web/baseui?pullRequest=653) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you all sign our [Contributor License Agreement](https://cla-assistant.io/uber-web/baseui?pullRequest=653) before we can accept your contribution.<br/>**4** out of **5** committers have signed the CLA.<br/><br/>:white_check_mark: nguyensomniac<br/>:white_check_mark: anantkamath<br/>:white_check_mark: chasestarr<br/>:white_check_mark: tajo<br/>:x: renovate[bot]<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/uber-web/baseui?pullRequest=653) it.</sub>

Patch:
diff --git a/src/select/dropdown.js b/src/select/dropdown.js
index 3404a76fc..a957fc87a 100644
--- a/src/select/dropdown.js
+++ b/src/select/dropdown.js
@@ -101,7 +101,7 @@ export default class SelectDropdown extends React.Component<DropdownPropsT> {
             highlightedIndex:
               Array.isArray(value) && value.length === 1
                 ? options.findIndex(opt => opt.id === value[0].id)
-                : -1,
+                : 0,
           }}
           overrides={mergeOverrides(
             {
diff --git a/src/tabs/README.md b/src/tabs/README.md
new file mode 100644
index 000000000..001b445b0
--- /dev/null
+++ b/src/tabs/README.md
@@ -0,0 +1,159 @@
+# Tabs Component
+
+## Usage
+
+### Basic usage
+
+```js
+import * as React from 'react';
+import {StatefulTabs, TabPanel} from 'baseui/tabs';
+
+export default () => {
+  return (
+    <div>
+      <StatefulTabs initialState={{activeKey: '1'}}>
+        <TabPanel title="Tab 1">Tab 1 Content</Tab>
+        <TabPanel title="Tab 2">Tab 2 Content</Tab>
+        <TabPanel title="Tab 3">Tab 3 Content</Tab>
+      </StatefulTabs>
+    </div>
+)};
+```
+
+### Advanced usage
+
+```js
+import * as React from 'react';
+import {Tabs, TabPanel, StyledTabBar, StyledTabContent, StyledTab, ORIENTATION} from 'baseui/tabs';
+import {styled} from 'baseui';
+
+const CustomTabBar = styled(StyledTabBar, props => ({
+  backgroundColor: 'yellow'
+}));
+
+const CustomTabContent = styled(StyledTabContent, props => ({
+  padding: '10px',
+  color: 'red',
+}));
+
+const overrides = {
+  TabBar: CustomTabBar,
+  TabContent: CustomTabContent
+};
+
+const CustomTab = styled(StyledTab, props => ({
+  borderBottom: props.$active ? `2px solid green` : 'none',
+}));
+
+const tabOverrides = {
+  Tab: CustomTab
+};
+
+export default () => {
+  return (
+    <div>
+      <StatefulTabs orientation={ORIENTATION.vertical} initialState={{activeKey: '1'}} overrides={overrides}>
+        <TabPanel key="1" title="Tab 1" overrides={tabOverrides}>Tab 1 Content</TabPanel>
+        <TabPanel key="2" title="Tab 2" overrides={tabOverrides}>Tab 2 Content</TabPanel>
+        <TabPanel key="3" title="Tab 3" overrides={tabOverrides}>Tab 3 Content</TabPanel>
+      </StatefulTabs>
+    </div>
+)};
+```
+
+## Exports
+
+* `Tabs`
+* `StatefulTabs`
+* `TabPanel`
+* `StyledTab`
+* `StyledTabBar`
+* `StyledTabContent`
+* `StyledRoot`
+* `ORIENTATION`
+* `STATE_CHANGE_TYPE`
+
+## `Tabs` API
+
+* `children: Array<TabPanel>` - Required.
+  An array of TabPanel items.
+* `activeKey: string` - Required.
+  Key of the the tab to be selected.
+* `disabled: boolean` - Optional. Default is `false`
+  If set to `true` all its tabs will be disabled
+* `orientation: $Values<typeof ORIENTATION>` - Optional. Defaults to ORIENTATION.horizontal
+* `overrides: {Root, TabContent, TabBar}` - Optional.
+  Overrides for presentational components. See "Presentational Components Props API" below
+  * `[ComponentName]: ReactComponent | {props: {}, style: {}, component: ReactComponent}` - Optional
+* `onChange: ({activeKey: string>})` - Optional.
+  onChange handler that is called every time a new tab is selected
+
+## `StatefulTabs` API
+
+* `children: Array<TabPanel>` - Required.
+  An array of TabPanel items.
+* `initialState: {activeKey: string}` - Optional.
+  Initial state of the component where `activeKey` is the key of the tab to be selected by default. Defaults to the key of the first `TabPanel`.
+* `disabled: boolean` - Optional. Default is `false`
+  If set to `true` all its tabs will be disabled
+* `orientation: $Values<typeof ORIENTATION>` - Optional. Defaults to ORIENTATION.horizontal
+* `overrides: {Root, TabContent, TabBar}` - Optional.
+  Overrides for presentational components. See "Presentational Components Props API" below
+  * `[ComponentName]: ReactComponent | {props: {}, style: {}, component: ReactComponent}` - Optional
+* `onChange: ({activeKey: string>})` - Optional.
+  onChange handler that is called every time a new tab is selected
+* `stateReducer: (type: string, nextState: {activeKey: string}, currentState: {activeKey: string}) => stateToSet: {activeKey: string}` - Optional.
+  A state change handler.
+  * `type` - a state change type
+  * `nextState` - a new state value to be set
+  * `currentState` - current state value
+  * `stateToSet` - a return value that the state will be updated with
+
+## `TabPanel` API
+
+* `title: React$Node` - Required.
+  Title of the Tab to be shown in the Tab bar.
+* `children: React$Node` - Optional.
+  Content of the tab to be shown when it is selected. (Can be omitted if using `Tabs` as an externally controlled component for example)
+* `key: string` - Optional.
+  Unique key for the tab. Defaults to the child index.
+* `disabled: boolean` Optional.
+  Defaults to the `disabled` value passed from the parent`Tabs` component
+* `onSelect: () => {}` - Optional.
+  onSelect handler for the `Tab` element
+* `onClick: (event) => {}` - Optional.
+  onClick handler for the `Tab` element
+* `onKeyDown: (event) => {}` - Optional.
+  onKeyDown handler for the `Tab` element
+* `overrides: {Tab}` - Optional
+  Overrides for Tab's presentational components. See "Presentational Components Props API" below
+  * [ComponentName]: ReactComponent | {props: {}, style: {}, component: ReactComponent}` - Optional
+
+## Presentational components props API
+
+These properties are passed to presentational (styled) components that are exported:
+
+* `$disabled: boolean` If the Tabs component (or an individual tab) is disabled
+* `$active: boolean` If a particular Tab is active
+* `$orientation: string` Orientation of the tab bar (either ORIENTATION.horizontal or ORIENTATION.vertical)
+
+## STATE_CHANGE_TYPE Constant
+
+* `change` - The active tab is changed (a new tab has been selected)
+
+## ORIENTATION Constant
+
+* `horizontal`
+* `vertical`
+
+## Accessibility
+
+`tab`, `shift + tab` - to move focus between tabs.
+
+`Space` or `Enter` - to display the selected tab's content
+
+`aria-selected`, `aria-disabled` are applied to the tabs.
+
+Roles: `role=tab`, `role=tablist`,  `role=tabpanel`
+
+Refer [https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html](https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html)
\ No newline at end of file
diff --git a/src/tabs/constants.js b/src/tabs/constants.js
new file mode 100644
index 000000000..2a01d723d
--- /dev/null
+++ b/src/tabs/constants.js
@@ -0,0 +1,16 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+export const STATE_CHANGE_TYPE = {
+  change: 'change',
+};
+
+export const ORIENTATION = {
+  horizontal: 'horizontal',
+  vertical: 'vertical',
+};
diff --git a/src/tabs/examples-list.js b/src/tabs/examples-list.js
new file mode 100644
index 000000000..7485d8b53
--- /dev/null
+++ b/src/tabs/examples-list.js
@@ -0,0 +1,16 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+/* eslint-env node */
+/* eslint-disable flowtype/require-valid-file-annotation */
+
+module.exports = {
+  TABS_EXAMPLE: 'Tabs',
+  STATEFUL_TABS_EXAMPLE: 'Stateful Tabs',
+  VERTICAL_TABS_EXAMPLE: 'Vertical Tabs',
+  STYLE_PROPS_OVERRIDES: 'Stateful tabs with overrides',
+  CONTROLLED_TABS_EXAMPLE: 'Externally controlled tabs',
+};
diff --git a/src/tabs/examples.js b/src/tabs/examples.js
new file mode 100644
index 000000000..411746f60
--- /dev/null
+++ b/src/tabs/examples.js
@@ -0,0 +1,140 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+import {styled} from '../styles/index.js';
+import {
+  Tabs,
+  StatefulTabs,
+  TabPanel,
+  StyledTabBar,
+  StyledTabContent,
+  StyledTab,
+  ORIENTATION,
+} from './index.js';
+import examples from './examples-list.js';
+
+export const suite = 'Component Test Suite';
+
+const Centered = styled('div', {
+  position: 'relative',
+  display: 'flex',
+  flexDirection: 'column',
+  justifyContent: 'start',
+  alignItems: 'center',
+  height: '90vh',
+  lineHeight: 1.5,
+  width: '500px',
+  margin: '0 auto',
+});
+
+const CustomTabBar = styled(StyledTabBar, props => ({
+  backgroundColor: 'yellow',
+}));
+
+const CustomTabContent = styled(StyledTabContent, props => ({
+  padding: '10px',
+  color: 'red',
+}));
+
+const overrides = {
+  TabBar: CustomTabBar,
+  TabContent: CustomTabContent,
+};
+
+const CustomTab = styled(StyledTab, props => ({
+  borderBottom: props.$active ? `2px solid green` : 'none',
+}));
+
+const tabOverrides = {
+  Tab: CustomTab,
+};
+
+class ControlledTabsStory extends React.Component<{}, {activeKey: React.Key}> {
+  state = {
+    activeKey: '0',
+  };
+
+  onChange = ({activeKey}) => {
+    this.setState({activeKey});
+  };
+
+  render() {
+    const content = ['Tab Content 1', 'Tab Content 2', 'Tab Content 3'];
+    return (
+      <Centered>
+        <Tabs activeKey={this.state.activeKey} onChange={this.onChange}>
+          <TabPanel title="Tab Link 1" />
+          <TabPanel title="Tab Link 2" />
+          <TabPanel title="Tab Link 3" />
+        </Tabs>
+        <div>Content:</div>
+        <div>{content[Number(this.state.activeKey)]}</div>
+      </Centered>
+    );
+  }
+}
+
+export default {
+  [examples.TABS_EXAMPLE]: function Story1() {
+    return (
+      <Centered>
+        <Tabs activeKey="1">
+          <TabPanel title="Tab Link 1">Tab 1 content</TabPanel>
+          <TabPanel title="Tab Link 2">Tab 2 content</TabPanel>
+          <TabPanel title="Tab Link 3">Tab 3 content</TabPanel>
+        </Tabs>
+      </Centered>
+    );
+  },
+  [examples.STATEFUL_TABS_EXAMPLE]: function Story2() {
+    return (
+      <Centered>
+        <StatefulTabs>
+          <TabPanel title="Tab Link 1">Tab 1 content</TabPanel>
+          <TabPanel title="Tab Link 2">Tab 2 content</TabPanel>
+          <TabPanel title="Tab Link 3">Tab 3 content</TabPanel>
+        </StatefulTabs>
+      </Centered>
+    );
+  },
+  [examples.VERTICAL_TABS_EXAMPLE]: function Story3() {
+    return (
+      <Centered>
+        <StatefulTabs orientation={ORIENTATION.vertical}>
+          <TabPanel title="Tab Link 1">Tab 1 content</TabPanel>
+          <TabPanel title="Tab Link 2">Tab 2 content</TabPanel>
+          <TabPanel title="Tab Link 3">Tab 3 content</TabPanel>
+        </StatefulTabs>
+      </Centered>
+    );
+  },
+  [examples.STYLE_PROPS_OVERRIDES]: function Story4() {
+    return (
+      <Centered>
+        <StatefulTabs
+          orientation={ORIENTATION.vertical}
+          initialState={{activeKey: '1'}}
+          overrides={overrides}
+        >
+          <TabPanel title="Tab 1" overrides={tabOverrides}>
+            Tab 1 Content
+          </TabPanel>
+          <TabPanel title="Tab 2" overrides={tabOverrides}>
+            Tab 2 Content
+          </TabPanel>
+          <TabPanel title="Tab 3" overrides={tabOverrides}>
+            Tab 3 Content
+          </TabPanel>
+        </StatefulTabs>
+      </Centered>
+    );
+  },
+  [examples.CONTROLLED_TABS_EXAMPLE]: function Story5() {
+    return <ControlledTabsStory />;
+  },
+};
diff --git a/src/tabs/index.js b/src/tabs/index.js
new file mode 100644
index 000000000..4f4a59df5
--- /dev/null
+++ b/src/tabs/index.js
@@ -0,0 +1,21 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+export {default as Tabs} from './tabs.js';
+export {default as StatefulTabs} from './stateful-tabs.js';
+export {default as TabPanel} from './tab-panel.js';
+// Constants
+export {ORIENTATION, STATE_CHANGE_TYPE} from './constants.js';
+// Styled elements
+export {
+  Root as StyledRoot,
+  Tab as StyledTab,
+  TabBar as StyledTabBar,
+  TabContent as StyledTabContent,
+} from './styled-components.js';
+// Flow
+export * from './types';
diff --git a/src/tabs/stateful-tabs.js b/src/tabs/stateful-tabs.js
new file mode 100644
index 000000000..f43728daa
--- /dev/null
+++ b/src/tabs/stateful-tabs.js
@@ -0,0 +1,65 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+import Tabs from './tabs.js';
+import {STATE_CHANGE_TYPE} from './constants.js';
+import type {
+  StatefulTabsPropsT,
+  StatefulTabsStateT,
+  StateChangeTypeT,
+} from './types.js';
+
+export default class StatefulTabs extends React.Component<
+  StatefulTabsPropsT,
+  StatefulTabsStateT,
+> {
+  static defaultProps: $Shape<StatefulTabsPropsT> = {
+    disabled: false,
+    onChange: () => {},
+    overrides: {},
+    stateReducer: (type, newState) => newState,
+  };
+
+  state = {
+    activeKey: this.getInitialKey(),
+  };
+
+  onTabChange = (newState: StatefulTabsStateT) => {
+    this.internalSetState(STATE_CHANGE_TYPE.change, newState);
+  };
+
+  internalSetState(type: StateChangeTypeT, changes: StatefulTabsStateT) {
+    const {stateReducer, onChange} = this.props;
+    const newState = stateReducer(type, changes, this.state);
+    this.setState(newState);
+    typeof onChange === 'function' && onChange(newState);
+  }
+
+  getInitialKey() {
+    const {initialState, children} = this.props;
+    if (initialState && initialState.activeKey) {
+      return initialState.activeKey;
+    } else {
+      return React.Children.map(
+        children,
+        (child, index) => child.key || String(index),
+      )[0];
+    }
+  }
+
+  render() {
+    const {initialState, stateReducer, ...restProps} = this.props;
+    return (
+      <Tabs
+        {...restProps}
+        activeKey={this.state.activeKey}
+        onChange={this.onTabChange}
+      />
+    );
+  }
+}
diff --git a/src/tabs/stories.js b/src/tabs/stories.js
new file mode 100644
index 000000000..0023c351c
--- /dev/null
+++ b/src/tabs/stories.js
@@ -0,0 +1,21 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* global module */
+import {storiesOf} from '@storybook/react';
+import {withReadme} from 'storybook-readme';
+import examples from './examples.js';
+
+//$FlowFixMe
+import TabsReadme from './README.md';
+
+Object.entries(examples).forEach(([description, example]) =>
+  storiesOf('Tabs', module)
+    .addDecorator(withReadme(TabsReadme))
+    // $FlowFixMe
+    .add(description, example),
+);
diff --git a/src/tabs/styled-components.js b/src/tabs/styled-components.js
new file mode 100644
index 000000000..0a7b7d467
--- /dev/null
+++ b/src/tabs/styled-components.js
@@ -0,0 +1,88 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import {styled} from '../styles/index.js';
+import type {SharedStylePropsT} from './types.js';
+import {ORIENTATION} from './constants.js';
+
+/**
+ * Main component container element
+ */
+export const Root = styled('div', (props: SharedStylePropsT) => {
+  const {$orientation} = props;
+  return {
+    display: 'flex',
+    flexDirection: $orientation === ORIENTATION.vertical ? 'row' : 'column',
+  };
+});
+
+export const Tab = styled('div', (props: SharedStylePropsT) => {
+  const {
+    $disabled,
+    $active,
+    $orientation,
+    $theme: {colors, sizing, typography},
+  } = props;
+  let style = {
+    ...typography.font300,
+    boxSizing: 'border-box',
+    color: $active ? colors.black : colors.mono800,
+    cursor: $disabled ? 'not-allowed' : 'pointer',
+    paddingTop: sizing.scale600,
+    paddingBottom: sizing.scale600,
+    paddingLeft: sizing.scale300,
+    paddingRight: sizing.scale300,
+    marginLeft: sizing.scale200,
+    marginRight: sizing.scale200,
+    borderBottom:
+      $orientation === ORIENTATION.horizontal && $active
+        ? `2px solid ${colors.primary400}`
+        : 'none',
+    display: 'inline-block',
+  };
+  if (!$disabled) {
+    style = {
+      ...style,
+      ':focus': {
+        color: colors.primary400,
+      },
+      ':hover': {
+        color: colors.primary400,
+      },
+    };
+  }
+  return style;
+});
+
+export const TabBar = styled('div', (props: SharedStylePropsT) => {
+  const {
+    $orientation,
+    $theme: {colors, sizing},
+  } = props;
+  return {
+    display: 'flex',
+    flexDirection: $orientation === ORIENTATION.vertical ? 'column' : 'row',
+    paddingLeft: sizing.scale400,
+    paddingRight: sizing.scale400,
+    backgroundColor: colors.mono200,
+  };
+});
+
+export const TabContent = styled('div', (props: SharedStylePropsT) => {
+  const {
+    $active,
+    $theme: {sizing, typography},
+  } = props;
+  return {
+    ...typography.font400,
+    display: $active ? 'block' : 'none',
+    paddingLeft: sizing.scale800,
+    paddingRight: sizing.scale800,
+    paddingTop: sizing.scale500,
+    paddingBottom: sizing.scale500,
+  };
+});
diff --git a/src/tabs/tab-panel.js b/src/tabs/tab-panel.js
new file mode 100644
index 000000000..b76c90590
--- /dev/null
+++ b/src/tabs/tab-panel.js
@@ -0,0 +1,26 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+
+import type {TabPropsT} from './types.js';
+
+class TabPanel extends React.Component<TabPropsT> {
+  static defaultProps: $Shape<TabPropsT> = {
+    disabled: false,
+    onSelect: () => {},
+    onClick: () => {},
+    onKeyDown: () => {},
+    title: '',
+  };
+
+  render() {
+    return null;
+  }
+}
+
+export default TabPanel;
diff --git a/src/tabs/tab.js b/src/tabs/tab.js
new file mode 100644
index 000000000..588180c32
--- /dev/null
+++ b/src/tabs/tab.js
@@ -0,0 +1,80 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+import {getOverrides} from '../helpers/overrides.js';
+import {Tab as StyledTab} from './styled-components.js';
+
+import type {TabPropsT, SharedStylePropsArgT} from './types.js';
+
+class TabComponent extends React.Component<TabPropsT> {
+  static defaultProps = {
+    disabled: false,
+    expanded: false,
+    onSelect: () => {},
+    onClick: () => {},
+    onKeyDown: () => {},
+    title: '',
+  };
+
+  onClick = (e: Event) => {
+    const {disabled, onSelect, onClick} = this.props;
+    if (disabled) {
+      return;
+    }
+    typeof onClick === 'function' && onClick(e);
+    typeof onSelect === 'function' && onSelect();
+    return;
+  };
+
+  onKeyDown = (e: KeyboardEvent) => {
+    const {disabled, onSelect, onKeyDown} = this.props;
+    if (disabled) {
+      return;
+    }
+    typeof onKeyDown === 'function' && onKeyDown(e);
+    // toggle on Enter or Space button pressed
+    if (e.key === 'Enter' || e.which === 32) {
+      typeof onSelect === 'function' && onSelect();
+    }
+    return;
+  };
+
+  getSharedProps(): SharedStylePropsArgT {
+    const {disabled, active, $orientation} = this.props;
+    return {
+      $disabled: disabled,
+      $active: active,
+      $orientation: $orientation,
+    };
+  }
+
+  render() {
+    const {active, disabled, id, overrides = {}, children} = this.props;
+    const sharedProps = this.getSharedProps();
+    const {Tab: TabOverride} = overrides;
+    const [Tab, tabProps] = getOverrides(TabOverride, StyledTab);
+
+    return (
+      <Tab
+        tabIndex={disabled ? -1 : 0}
+        role="tab"
+        id={id}
+        aria-selected={active}
+        aria-disabled={disabled || null}
+        {...sharedProps}
+        {...tabProps}
+        onClick={this.onClick}
+        onKeyDown={this.onKeyDown}
+      >
+        {children}
+      </Tab>
+    );
+  }
+}
+
+export default TabComponent;
diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js
new file mode 100644
index 000000000..f97c45f40
--- /dev/null
+++ b/src/tabs/tabs.js
@@ -0,0 +1,117 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+import {getOverrides} from '../helpers/overrides.js';
+import Tab from './tab.js';
+import {
+  Root as StyledRoot,
+  TabBar as StyledTabBar,
+  TabContent as StyledTabContent,
+} from './styled-components.js';
+import type {TabsPropsT, SharedStylePropsArgT} from './types.js';
+import {ORIENTATION} from './constants.js';
+
+export default class Tabs extends React.Component<TabsPropsT> {
+  static defaultProps: $Shape<TabsPropsT> = {
+    disabled: false,
+    onChange: () => {},
+    overrides: {},
+    orientation: ORIENTATION.horizontal,
+  };
+
+  onChange({activeKey}: {activeKey: string}) {
+    const {onChange} = this.props;
+    typeof onChange === 'function' && onChange({activeKey});
+  }
+
+  getTabs() {
+    const {activeKey, disabled, orientation, children} = this.props;
+    // eslint-disable-next-line flowtype/no-weak-types
+    const tabs = React.Children.map(children, (child: any, index) => {
+      if (!child) return;
+      const key = child.key || String(index);
+      const props = {
+        key,
+        id: key, // for aria-labelledby
+        active: key === activeKey,
+        disabled: disabled || child.props.disabled,
+        $orientation: orientation,
+        overrides: child.props.overrides,
+        onSelect: () => this.onChange({activeKey: key}),
+      };
+      return <Tab {...props}>{child.props.title}</Tab>;
+    });
+    return tabs;
+  }
+
+  getPanels() {
+    const {
+      activeKey,
+      disabled,
+      orientation,
+      children,
+      overrides = {},
+    } = this.props;
+    const {TabContent: TabContentOverride} = overrides;
+    const [TabContent, tabContentProps] = getOverrides(
+      TabContentOverride,
+      StyledTabContent,
+    );
+    // eslint-disable-next-line flowtype/no-weak-types
+    const tabs = React.Children.map(children, (child: any, index) => {
+      if (!child) return;
+      const key = child.key || String(index);
+      const props = {
+        key,
+        'aria-labelledby': key,
+      };
+      const sharedProps = {
+        $active: key === activeKey,
+        $disabled: disabled,
+        $orientation: orientation,
+      };
+
+      return (
+        <TabContent
+          role="tabpanel"
+          {...sharedProps}
+          {...tabContentProps}
+          {...props}
+        >
+          {child.props.children}
+        </TabContent>
+      );
+    });
+    return tabs;
+  }
+
+  getSharedProps(): SharedStylePropsArgT {
+    const {disabled, orientation} = this.props;
+    return {
+      $disabled: disabled,
+      $orientation: orientation,
+    };
+  }
+
+  render() {
+    const sharedProps = this.getSharedProps();
+    const {overrides = {}} = this.props;
+    const {Root: RootOverride, TabBar: TabBarOverride} = overrides;
+    const [Root, rootProps] = getOverrides(RootOverride, StyledRoot);
+    const [TabBar, tabBarProps] = getOverrides(TabBarOverride, StyledTabBar);
+
+    return (
+      <Root {...sharedProps} {...rootProps}>
+        <TabBar role="tablist" {...sharedProps} {...tabBarProps}>
+          {this.getTabs()}
+        </TabBar>
+        {this.getPanels()}
+      </Root>
+    );
+  }
+}
diff --git a/src/tabs/types.js b/src/tabs/types.js
new file mode 100644
index 000000000..d69831b33
--- /dev/null
+++ b/src/tabs/types.js
@@ -0,0 +1,84 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* eslint-disable flowtype/generic-spacing */
+import * as React from 'react';
+import {default as TabPanel} from './tab-panel.js';
+import type {ThemeT} from '../styles/types.js';
+import type {OverrideT} from '../helpers/overrides.js';
+import {ORIENTATION, STATE_CHANGE_TYPE} from './constants.js';
+
+export type StatefulTabsStateT = {
+  activeKey: React.Key,
+};
+
+export type StateChangeTypeT = $Keys<typeof STATE_CHANGE_TYPE>;
+
+export type StateReducerT = (
+  stateChangeType: StateChangeTypeT,
+  nextState: StatefulTabsStateT,
+  currentState: StatefulTabsStateT,
+) => StatefulTabsStateT;
+
+export type TabsOverridesT<T> = {
+  Root?: OverrideT<T>,
+  TabBar?: OverrideT<T>,
+  TabContent?: OverrideT<T>,
+};
+
+export type TabOverridesT<T> = {
+  Tab?: OverrideT<T>,
+};
+
+export type OnChangeHandlerT = ({activeKey: React.Key}) => void;
+
+export type TabsPropsT = {
+  children: React.ChildrenArray<React.Element<typeof TabPanel>>,
+  activeKey: React.Key,
+  disabled?: boolean,
+  onChange?: OnChangeHandlerT,
+  orientation?: $Values<typeof ORIENTATION>,
+  overrides?: TabsOverridesT<$Diff<SharedStylePropsArgT, {$active?: ?boolean}>>,
+};
+
+export type StatefulTabsPropsT = $Diff<TabsPropsT, {activeKey: React.Key}> & {
+  initialState?: StatefulTabsStateT,
+  stateReducer: StateReducerT,
+};
+
+export type TabPanelPropsT = {
+  children: React.ChildrenArray<React.Node>,
+  disabled?: boolean,
+  active?: boolean,
+  key?: React.Key,
+  onClick?: (e: Event) => void,
+  onKeyDown?: (e: KeyboardEvent) => void,
+  onSelect?: () => void,
+  overrides?: TabOverridesT<SharedStylePropsArgT>,
+  title?: React.Node,
+};
+
+export type TabPropsT = TabPanelPropsT & {
+  id?: string,
+  $orientation?: $Values<typeof ORIENTATION>,
+};
+
+export type SharedStylePropsArgT = {
+  $disabled: ?boolean,
+  $active?: ?boolean,
+  $orientation?: $Values<typeof ORIENTATION>,
+  // Styletron stuff
+  $as?: string,
+  // styled function wrapper related
+  $style?: ?{},
+  /* eslint-disable flowtype/no-weak-types */
+  $ref?: React.Ref<any>,
+};
+
+export type SharedStylePropsT = SharedStylePropsArgT & {
+  $theme: ThemeT,
+};


Test patch:
diff --git a/src/select/__tests__/select.test.js b/src/select/__tests__/select.test.js
index 533a70ac6..f432992d4 100644
--- a/src/select/__tests__/select.test.js
+++ b/src/select/__tests__/select.test.js
@@ -71,4 +71,16 @@ describe('Select component', function() {
       value: [],
     });
   });
+
+  test('highlights the found item by default', function() {
+    wrapper = mount(<Select {...props} />);
+    const e = {target: {value: 'label1'}};
+    wrapper.instance().handleInputChange(e);
+    expect(
+      wrapper
+        .render()
+        .find('ul')
+        .html(),
+    ).toMatch(/aria-activedescendant="true"/);
+  });
 });
diff --git a/src/tabs/__tests__/__snapshots__/stateful-tabs.test.js.snap b/src/tabs/__tests__/__snapshots__/stateful-tabs.test.js.snap
new file mode 100644
index 000000000..96302ea73
--- /dev/null
+++ b/src/tabs/__tests__/__snapshots__/stateful-tabs.test.js.snap
@@ -0,0 +1,416 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`StatefulTabs basic render with initial state: Stateful tabs render correctly with initial state 1`] = `
+<Component
+  initialState={
+    Object {
+      "activeKey": "1",
+    }
+  }
+>
+  <StatefulTabs
+    disabled={false}
+    initialState={
+      Object {
+        "activeKey": "1",
+      }
+    }
+    onChange={[Function]}
+    overrides={Object {}}
+    stateReducer={[Function]}
+  >
+    <Tabs
+      activeKey="1"
+      disabled={false}
+      onChange={[Function]}
+      orientation="horizontal"
+      overrides={Object {}}
+    >
+      <MockStyledComponent
+        $disabled={false}
+        $orientation="horizontal"
+      >
+        <div
+          styled-component="true"
+        >
+          <MockStyledComponent
+            $disabled={false}
+            $orientation="horizontal"
+            role="tablist"
+          >
+            <div
+              role="tablist"
+              styled-component="true"
+            >
+              <TabComponent
+                $orientation="horizontal"
+                active={false}
+                disabled={false}
+                expanded={false}
+                id="0"
+                key="0/.0"
+                onClick={[Function]}
+                onKeyDown={[Function]}
+                onSelect={[Function]}
+                title=""
+              >
+                <MockStyledComponent
+                  $active={false}
+                  $disabled={false}
+                  $orientation="horizontal"
+                  aria-disabled={null}
+                  aria-selected={false}
+                  id="0"
+                  onClick={[Function]}
+                  onKeyDown={[Function]}
+                  role="tab"
+                  tabIndex={0}
+                >
+                  <div
+                    aria-disabled={null}
+                    aria-selected={false}
+                    id="0"
+                    onClick={[Function]}
+                    onKeyDown={[Function]}
+                    role="tab"
+                    styled-component="true"
+                    tabIndex={0}
+                  >
+                    Tab Link 1
+                  </div>
+                </MockStyledComponent>
+              </TabComponent>
+              <TabComponent
+                $orientation="horizontal"
+                active={true}
+                disabled={false}
+                expanded={false}
+                id="1"
+                key="1/.1"
+                onClick={[Function]}
+                onKeyDown={[Function]}
+                onSelect={[Function]}
+                title=""
+              >
+                <MockStyledComponent
+                  $active={true}
+                  $disabled={false}
+                  $orientation="horizontal"
+                  aria-disabled={null}
+                  aria-selected={true}
+                  id="1"
+                  onClick={[Function]}
+                  onKeyDown={[Function]}
+                  role="tab"
+                  tabIndex={0}
+                >
+                  <div
+                    aria-disabled={null}
+                    aria-selected={true}
+                    id="1"
+                    onClick={[Function]}
+                    onKeyDown={[Function]}
+                    role="tab"
+                    styled-component="true"
+                    tabIndex={0}
+                  >
+                    Tab Link 2
+                  </div>
+                </MockStyledComponent>
+              </TabComponent>
+              <TabComponent
+                $orientation="horizontal"
+                active={false}
+                disabled={false}
+                expanded={false}
+                id="2"
+                key="2/.2"
+                onClick={[Function]}
+                onKeyDown={[Function]}
+                onSelect={[Function]}
+                title=""
+              >
+                <MockStyledComponent
+                  $active={false}
+                  $disabled={false}
+                  $orientation="horizontal"
+                  aria-disabled={null}
+                  aria-selected={false}
+                  id="2"
+                  onClick={[Function]}
+                  onKeyDown={[Function]}
+                  role="tab"
+                  tabIndex={0}
+                >
+                  <div
+                    aria-disabled={null}
+                    aria-selected={false}
+                    id="2"
+                    onClick={[Function]}
+                    onKeyDown={[Function]}
+                    role="tab"
+                    styled-component="true"
+                    tabIndex={0}
+                  >
+                    Tab Link 3
+                  </div>
+                </MockStyledComponent>
+              </TabComponent>
+            </div>
+          </MockStyledComponent>
+          <MockStyledComponent
+            $active={false}
+            $disabled={false}
+            $orientation="horizontal"
+            aria-labelledby="0"
+            key="0/.0"
+            role="tabpanel"
+          >
+            <div
+              aria-labelledby="0"
+              role="tabpanel"
+              styled-component="true"
+            >
+              Tab 1 content
+            </div>
+          </MockStyledComponent>
+          <MockStyledComponent
+            $active={true}
+            $disabled={false}
+            $orientation="horizontal"
+            aria-labelledby="1"
+            key="1/.1"
+            role="tabpanel"
+          >
+            <div
+              aria-labelledby="1"
+              role="tabpanel"
+              styled-component="true"
+            >
+              Tab 2 content
+            </div>
+          </MockStyledComponent>
+          <MockStyledComponent
+            $active={false}
+            $disabled={false}
+            $orientation="horizontal"
+            aria-labelledby="2"
+            key="2/.2"
+            role="tabpanel"
+          >
+            <div
+              aria-labelledby="2"
+              role="tabpanel"
+              styled-component="true"
+            >
+              Tab 3 content
+            </div>
+          </MockStyledComponent>
+        </div>
+      </MockStyledComponent>
+    </Tabs>
+  </StatefulTabs>
+</Component>
+`;
+
+exports[`StatefulTabs basic render: Stateful tabs render correctly 1`] = `
+<Component>
+  <StatefulTabs
+    disabled={false}
+    onChange={[Function]}
+    overrides={Object {}}
+    stateReducer={[Function]}
+  >
+    <Tabs
+      activeKey="0"
+      disabled={false}
+      onChange={[Function]}
+      orientation="horizontal"
+      overrides={Object {}}
+    >
+      <MockStyledComponent
+        $disabled={false}
+        $orientation="horizontal"
+      >
+        <div
+          styled-component="true"
+        >
+          <MockStyledComponent
+            $disabled={false}
+            $orientation="horizontal"
+            role="tablist"
+          >
+            <div
+              role="tablist"
+              styled-component="true"
+            >
+              <TabComponent
+                $orientation="horizontal"
+                active={true}
+                disabled={false}
+                expanded={false}
+                id="0"
+                key="0/.0"
+                onClick={[Function]}
+                onKeyDown={[Function]}
+                onSelect={[Function]}
+                title=""
+              >
+                <MockStyledComponent
+                  $active={true}
+                  $disabled={false}
+                  $orientation="horizontal"
+                  aria-disabled={null}
+                  aria-selected={true}
+                  id="0"
+                  onClick={[Function]}
+                  onKeyDown={[Function]}
+                  role="tab"
+                  tabIndex={0}
+                >
+                  <div
+                    aria-disabled={null}
+                    aria-selected={true}
+                    id="0"
+                    onClick={[Function]}
+                    onKeyDown={[Function]}
+                    role="tab"
+                    styled-component="true"
+                    tabIndex={0}
+                  >
+                    Tab Link 1
+                  </div>
+                </MockStyledComponent>
+              </TabComponent>
+              <TabComponent
+                $orientation="horizontal"
+                active={false}
+                disabled={false}
+                expanded={false}
+                id="1"
+                key="1/.1"
+                onClick={[Function]}
+                onKeyDown={[Function]}
+                onSelect={[Function]}
+                title=""
+              >
+                <MockStyledComponent
+                  $active={false}
+                  $disabled={false}
+                  $orientation="horizontal"
+                  aria-disabled={null}
+                  aria-selected={false}
+                  id="1"
+                  onClick={[Function]}
+                  onKeyDown={[Function]}
+                  role="tab"
+                  tabIndex={0}
+                >
+                  <div
+                    aria-disabled={null}
+                    aria-selected={false}
+                    id="1"
+                    onClick={[Function]}
+                    onKeyDown={[Function]}
+                    role="tab"
+                    styled-component="true"
+                    tabIndex={0}
+                  >
+                    Tab Link 2
+                  </div>
+                </MockStyledComponent>
+              </TabComponent>
+              <TabComponent
+                $orientation="horizontal"
+                active={false}
+                disabled={false}
+                expanded={false}
+                id="2"
+                key="2/.2"
+                onClick={[Function]}
+                onKeyDown={[Function]}
+                onSelect={[Function]}
+                title=""
+              >
+                <MockStyledComponent
+                  $active={false}
+                  $disabled={false}
+                  $orientation="horizontal"
+                  aria-disabled={null}
+                  aria-selected={false}
+                  id="2"
+                  onClick={[Function]}
+                  onKeyDown={[Function]}
+                  role="tab"
+                  tabIndex={0}
+                >
+                  <div
+                    aria-disabled={null}
+                    aria-selected={false}
+                    id="2"
+                    onClick={[Function]}
+                    onKeyDown={[Function]}
+                    role="tab"
+                    styled-component="true"
+                    tabIndex={0}
+                  >
+                    Tab Link 3
+                  </div>
+                </MockStyledComponent>
+              </TabComponent>
+            </div>
+          </MockStyledComponent>
+          <MockStyledComponent
+            $active={true}
+            $disabled={false}
+            $orientation="horizontal"
+            aria-labelledby="0"
+            key="0/.0"
+            role="tabpanel"
+          >
+            <div
+              aria-labelledby="0"
+              role="tabpanel"
+              styled-component="true"
+            >
+              Tab 1 content
+            </div>
+          </MockStyledComponent>
+          <MockStyledComponent
+            $active={false}
+            $disabled={false}
+            $orientation="horizontal"
+            aria-labelledby="1"
+            key="1/.1"
+            role="tabpanel"
+          >
+            <div
+              aria-labelledby="1"
+              role="tabpanel"
+              styled-component="true"
+            >
+              Tab 2 content
+            </div>
+          </MockStyledComponent>
+          <MockStyledComponent
+            $active={false}
+            $disabled={false}
+            $orientation="horizontal"
+            aria-labelledby="2"
+            key="2/.2"
+            role="tabpanel"
+          >
+            <div
+              aria-labelledby="2"
+              role="tabpanel"
+              styled-component="true"
+            >
+              Tab 3 content
+            </div>
+          </MockStyledComponent>
+        </div>
+      </MockStyledComponent>
+    </Tabs>
+  </StatefulTabs>
+</Component>
+`;
diff --git a/src/tabs/__tests__/__snapshots__/styled-components.test.js.snap b/src/tabs/__tests__/__snapshots__/styled-components.test.js.snap
new file mode 100644
index 000000000..26f1b0afb
--- /dev/null
+++ b/src/tabs/__tests__/__snapshots__/styled-components.test.js.snap
@@ -0,0 +1,297 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tabs styled components in horizontal orientation Basic render when active is set to true: StyledTab has correct styles when active is set to true in horizontal orientation 1`] = `
+Object {
+  ":focus": Object {
+    "color": "$theme.colors.primary400",
+  },
+  ":hover": Object {
+    "color": "$theme.colors.primary400",
+  },
+  "borderBottom": "2px solid $theme.colors.primary400",
+  "boxSizing": "border-box",
+  "color": "$theme.colors.black",
+  "cursor": "pointer",
+  "display": "inline-block",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "marginLeft": "$theme.sizing.scale200",
+  "marginRight": "$theme.sizing.scale200",
+  "paddingBottom": "$theme.sizing.scale600",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingRight": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render when active is set to true: StyledTabContent has correct styles when active is set to true in horizontal orientation 1`] = `
+Object {
+  "display": "block",
+  "fontFamily": "$theme.typography.font400.fontFamily",
+  "fontSize": "$theme.typography.font400.fontSize",
+  "fontWeight": "$theme.typography.font400.fontWeight",
+  "lineHeight": "$theme.typography.font400.lineHeight",
+  "paddingBottom": "$theme.sizing.scale500",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale500",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render when disabled: StyledRoot has correct styles when disabled is set to true in horizontal orientation 1`] = `
+Object {
+  "display": "flex",
+  "flexDirection": "column",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render when disabled: StyledTab has correct styles when disabled is set to true in horizontal orientation 1`] = `
+Object {
+  "borderBottom": "none",
+  "boxSizing": "border-box",
+  "color": "$theme.colors.mono800",
+  "cursor": "not-allowed",
+  "display": "inline-block",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "marginLeft": "$theme.sizing.scale200",
+  "marginRight": "$theme.sizing.scale200",
+  "paddingBottom": "$theme.sizing.scale600",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingRight": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render when disabled: StyledTabBar has correct styles when disabled is set to true in horizontal orientation 1`] = `
+Object {
+  "backgroundColor": "$theme.colors.mono200",
+  "display": "flex",
+  "flexDirection": "row",
+  "paddingLeft": "$theme.sizing.scale400",
+  "paddingRight": "$theme.sizing.scale400",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render when disabled: StyledTabContent has correct styles when disabled is set to true in horizontal orientation 1`] = `
+Object {
+  "display": "none",
+  "fontFamily": "$theme.typography.font400.fontFamily",
+  "fontSize": "$theme.typography.font400.fontSize",
+  "fontWeight": "$theme.typography.font400.fontWeight",
+  "lineHeight": "$theme.typography.font400.lineHeight",
+  "paddingBottom": "$theme.sizing.scale500",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale500",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render: StyledRoot has correct styles in horizontal orientation 1`] = `
+Object {
+  "display": "flex",
+  "flexDirection": "column",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render: StyledTab has correct styles in horizontal orientation 1`] = `
+Object {
+  ":focus": Object {
+    "color": "$theme.colors.primary400",
+  },
+  ":hover": Object {
+    "color": "$theme.colors.primary400",
+  },
+  "borderBottom": "none",
+  "boxSizing": "border-box",
+  "color": "$theme.colors.mono800",
+  "cursor": "pointer",
+  "display": "inline-block",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "marginLeft": "$theme.sizing.scale200",
+  "marginRight": "$theme.sizing.scale200",
+  "paddingBottom": "$theme.sizing.scale600",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingRight": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render: StyledTabBar has correct styles in horizontal orientation 1`] = `
+Object {
+  "backgroundColor": "$theme.colors.mono200",
+  "display": "flex",
+  "flexDirection": "row",
+  "paddingLeft": "$theme.sizing.scale400",
+  "paddingRight": "$theme.sizing.scale400",
+}
+`;
+
+exports[`Tabs styled components in horizontal orientation Basic render: StyledTabContent has correct styles in horizontal orientation 1`] = `
+Object {
+  "display": "none",
+  "fontFamily": "$theme.typography.font400.fontFamily",
+  "fontSize": "$theme.typography.font400.fontSize",
+  "fontWeight": "$theme.typography.font400.fontWeight",
+  "lineHeight": "$theme.typography.font400.lineHeight",
+  "paddingBottom": "$theme.sizing.scale500",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale500",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render when active is set to true: StyledTab has correct styles when active is set to true in vertical orientation 1`] = `
+Object {
+  ":focus": Object {
+    "color": "$theme.colors.primary400",
+  },
+  ":hover": Object {
+    "color": "$theme.colors.primary400",
+  },
+  "borderBottom": "none",
+  "boxSizing": "border-box",
+  "color": "$theme.colors.black",
+  "cursor": "pointer",
+  "display": "inline-block",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "marginLeft": "$theme.sizing.scale200",
+  "marginRight": "$theme.sizing.scale200",
+  "paddingBottom": "$theme.sizing.scale600",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingRight": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render when active is set to true: StyledTabContent has correct styles when active is set to true in vertical orientation 1`] = `
+Object {
+  "display": "block",
+  "fontFamily": "$theme.typography.font400.fontFamily",
+  "fontSize": "$theme.typography.font400.fontSize",
+  "fontWeight": "$theme.typography.font400.fontWeight",
+  "lineHeight": "$theme.typography.font400.lineHeight",
+  "paddingBottom": "$theme.sizing.scale500",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale500",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render when disabled: StyledRoot has correct styles when disabled is set to true in vertical orientation 1`] = `
+Object {
+  "display": "flex",
+  "flexDirection": "row",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render when disabled: StyledTab has correct styles when disabled is set to true in vertical orientation 1`] = `
+Object {
+  "borderBottom": "none",
+  "boxSizing": "border-box",
+  "color": "$theme.colors.mono800",
+  "cursor": "not-allowed",
+  "display": "inline-block",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "marginLeft": "$theme.sizing.scale200",
+  "marginRight": "$theme.sizing.scale200",
+  "paddingBottom": "$theme.sizing.scale600",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingRight": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render when disabled: StyledTabBar has correct styles when disabled is set to true in vertical orientation 1`] = `
+Object {
+  "backgroundColor": "$theme.colors.mono200",
+  "display": "flex",
+  "flexDirection": "column",
+  "paddingLeft": "$theme.sizing.scale400",
+  "paddingRight": "$theme.sizing.scale400",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render when disabled: StyledTabContent has correct styles when disabled is set to true in vertical orientation 1`] = `
+Object {
+  "display": "none",
+  "fontFamily": "$theme.typography.font400.fontFamily",
+  "fontSize": "$theme.typography.font400.fontSize",
+  "fontWeight": "$theme.typography.font400.fontWeight",
+  "lineHeight": "$theme.typography.font400.lineHeight",
+  "paddingBottom": "$theme.sizing.scale500",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale500",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render: StyledRoot has correct styles in vertical orientation 1`] = `
+Object {
+  "display": "flex",
+  "flexDirection": "row",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render: StyledTab has correct styles in vertical orientation 1`] = `
+Object {
+  ":focus": Object {
+    "color": "$theme.colors.primary400",
+  },
+  ":hover": Object {
+    "color": "$theme.colors.primary400",
+  },
+  "borderBottom": "none",
+  "boxSizing": "border-box",
+  "color": "$theme.colors.mono800",
+  "cursor": "pointer",
+  "display": "inline-block",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "marginLeft": "$theme.sizing.scale200",
+  "marginRight": "$theme.sizing.scale200",
+  "paddingBottom": "$theme.sizing.scale600",
+  "paddingLeft": "$theme.sizing.scale300",
+  "paddingRight": "$theme.sizing.scale300",
+  "paddingTop": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render: StyledTabBar has correct styles in vertical orientation 1`] = `
+Object {
+  "backgroundColor": "$theme.colors.mono200",
+  "display": "flex",
+  "flexDirection": "column",
+  "paddingLeft": "$theme.sizing.scale400",
+  "paddingRight": "$theme.sizing.scale400",
+}
+`;
+
+exports[`Tabs styled components in vertical orientation Basic render: StyledTabContent has correct styles in vertical orientation 1`] = `
+Object {
+  "display": "none",
+  "fontFamily": "$theme.typography.font400.fontFamily",
+  "fontSize": "$theme.typography.font400.fontSize",
+  "fontWeight": "$theme.typography.font400.fontWeight",
+  "lineHeight": "$theme.typography.font400.lineHeight",
+  "paddingBottom": "$theme.sizing.scale500",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale500",
+}
+`;
diff --git a/src/tabs/__tests__/__snapshots__/tab.test.js.snap b/src/tabs/__tests__/__snapshots__/tab.test.js.snap
new file mode 100644
index 000000000..e522efbe0
--- /dev/null
+++ b/src/tabs/__tests__/__snapshots__/tab.test.js.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tab basic rendering: styled tab has correct props 1`] = `
+Object {
+  "$active": true,
+  "$disabled": false,
+  "$orientation": undefined,
+  "aria-disabled": null,
+  "aria-selected": true,
+  "children": "title",
+  "id": undefined,
+  "onClick": [Function],
+  "onKeyDown": [Function],
+  "role": "tab",
+  "tabIndex": 0,
+}
+`;
diff --git a/src/tabs/__tests__/stateful-tabs.test.js b/src/tabs/__tests__/stateful-tabs.test.js
new file mode 100644
index 000000000..c35ae2e69
--- /dev/null
+++ b/src/tabs/__tests__/stateful-tabs.test.js
@@ -0,0 +1,97 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import React from 'react';
+import {mount} from 'enzyme';
+import {StatefulTabs, TabPanel, StyledTab, StyledTabContent} from '../index.js';
+import {STATE_CHANGE_TYPE} from '../constants.js';
+
+describe('StatefulTabs', () => {
+  const Component = props => (
+    <StatefulTabs {...props}>
+      <TabPanel title="Tab Link 1">Tab 1 content</TabPanel>
+      <TabPanel title="Tab Link 2">Tab 2 content</TabPanel>
+      <TabPanel title="Tab Link 3">Tab 3 content</TabPanel>
+    </StatefulTabs>
+  );
+
+  test('basic render', () => {
+    const component = mount(<Component />);
+    expect(component).toMatchSnapshot('Stateful tabs render correctly');
+  });
+
+  test('basic render with initial state', () => {
+    const props = {
+      initialState: {activeKey: '1'},
+    };
+    const component = mount(<Component {...props} />);
+    expect(component).toMatchSnapshot(
+      'Stateful tabs render correctly with initial state',
+    );
+
+    const tab = component.find(StyledTab).at(1);
+    expect(tab).toHaveProp('$active', true);
+
+    const panel = component.find(StyledTabContent).at(1);
+    expect(panel).toHaveProp('$active', true);
+  });
+
+  test('onChange functionality', () => {
+    const props = {
+      onChange: jest.fn(),
+    };
+    const component = mount(<Component {...props} />);
+
+    const tab = component.find(StyledTab).at(1);
+    tab.simulate('click');
+    const newState = {activeKey: '1'};
+    expect(props.onChange).toHaveBeenCalledTimes(1);
+    expect(props.onChange).toHaveBeenCalledWith(newState);
+  });
+
+  test('stateReducer functionality', () => {
+    const props = {
+      stateReducer: jest.fn((type, newState) => newState),
+    };
+    const component = mount(<Component {...props} />);
+
+    const tab = component.find(StyledTab).at(1);
+    tab.simulate('click');
+    const oldState = {activeKey: '0'};
+    const newState = {activeKey: '1'};
+    expect(props.stateReducer).toHaveBeenCalledTimes(1);
+    expect(props.stateReducer).toHaveBeenCalledWith(
+      STATE_CHANGE_TYPE.change,
+      newState,
+      oldState,
+    );
+  });
+
+  test('state changes', () => {
+    const component = mount(<Component />);
+    expect(component.find(StyledTab).at(0)).toHaveProp('$active', true);
+
+    component
+      .find(StyledTab)
+      .at(1)
+      .simulate('click');
+    expect(component.find(StyledTab).at(1)).toHaveProp('$active', true);
+
+    // Click the same tab again
+    component
+      .find(StyledTab)
+      .at(1)
+      .simulate('click');
+    expect(component.find(StyledTab).at(1)).toHaveProp('$active', true);
+
+    component
+      .find(StyledTab)
+      .at(0)
+      .simulate('click');
+    expect(component.find(StyledTab).at(0)).toHaveProp('$active', true);
+  });
+});
diff --git a/src/tabs/__tests__/styled-components.test.js b/src/tabs/__tests__/styled-components.test.js
new file mode 100644
index 000000000..931780649
--- /dev/null
+++ b/src/tabs/__tests__/styled-components.test.js
@@ -0,0 +1,59 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import React from 'react';
+import {shallow} from 'enzyme';
+import {
+  StyledRoot,
+  StyledTabBar,
+  StyledTabContent,
+  StyledTab,
+  ORIENTATION,
+} from '../index.js';
+
+const styledComponents = [
+  [StyledRoot, 'StyledRoot'],
+  [StyledTabBar, 'StyledTabBar'],
+  [StyledTabContent, 'StyledTabContent'],
+  [StyledTab, 'StyledTab'],
+];
+
+describe.each(Object.values(ORIENTATION))(
+  'Tabs styled components in %s orientation',
+  orientation => {
+    test.each(styledComponents)('Basic render', (Component, name) => {
+      const component = shallow(<Component $orientation={orientation} />);
+      expect(component.instance().getStyles()).toMatchSnapshot(
+        `${name} has correct styles in ${orientation} orientation`,
+      );
+    });
+
+    test.each(styledComponents)(
+      'Basic render when disabled',
+      (Component, name) => {
+        const component = shallow(
+          <Component $orientation={orientation} $disabled={true} />,
+        );
+        expect(component.instance().getStyles()).toMatchSnapshot(
+          `${name} has correct styles when disabled is set to true in ${orientation} orientation`,
+        );
+      },
+    );
+
+    test.each([
+      [StyledTab, 'StyledTab'],
+      [StyledTabContent, 'StyledTabContent'],
+    ])('Basic render when active is set to true', (Component, name) => {
+      const component = shallow(
+        <Component $orientation={orientation} $active={true} />,
+      );
+      expect(component.instance().getStyles()).toMatchSnapshot(
+        `${name} has correct styles when active is set to true in ${orientation} orientation`,
+      );
+    });
+  },
+);
diff --git a/src/tabs/__tests__/tab.test.js b/src/tabs/__tests__/tab.test.js
new file mode 100644
index 000000000..cba918e55
--- /dev/null
+++ b/src/tabs/__tests__/tab.test.js
@@ -0,0 +1,100 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+import {mount} from 'enzyme';
+import {StyledTab} from '../index.js';
+
+import {default as Tab} from '../tab.js';
+
+describe('Tab', () => {
+  test('basic rendering', () => {
+    const props = {
+      active: true,
+      children: 'title',
+    };
+    let renderedTab;
+    const wrapper = mount(<Tab {...props} />);
+    renderedTab = wrapper.find(StyledTab).first();
+    expect(renderedTab.props()).toMatchSnapshot('styled tab has correct props');
+  });
+
+  test('component overrides', () => {
+    const overrides = {
+      // eslint-disable-next-line react/display-name
+      Tab: ({children}) => <span>{children}</span>,
+    };
+    // $FlowFixMe
+    const wrapper = mount(<Tab overrides={overrides}>Title</Tab>);
+    const tab = wrapper.find(overrides.Tab);
+    expect(tab).toHaveLength(1);
+  });
+
+  test('onSelect and onClick are called on a click event', () => {
+    const props = {
+      onSelect: jest.fn(),
+      onClick: jest.fn(),
+    };
+    const wrapper = mount(<Tab {...props}>Title</Tab>);
+    const tab = wrapper.find(StyledTab);
+    tab.simulate('click');
+    expect(props.onSelect).toHaveBeenCalledTimes(1);
+    expect(props.onClick).toHaveBeenCalledTimes(1);
+  });
+
+  test('onSelect and onKeydwon are called on a Enter keydown event', () => {
+    const props = {
+      onSelect: jest.fn(),
+      onKeyDown: jest.fn(),
+    };
+    const event = {key: 'Enter'};
+    const wrapper = mount(<Tab {...props}>Title</Tab>);
+    const tab = wrapper.find(StyledTab);
+    tab.simulate('keydown', event);
+    expect(props.onSelect).toHaveBeenCalledTimes(1);
+    expect(props.onKeyDown).toHaveBeenCalledTimes(1);
+  });
+
+  test('onSelect and onKeydown are called on a Space keydown event', () => {
+    const props = {
+      onSelect: jest.fn(),
+      onKeyDown: jest.fn(),
+    };
+    const event = {which: 32};
+    const wrapper = mount(<Tab {...props}>Title</Tab>);
+    const tab = wrapper.find(StyledTab);
+    tab.simulate('keydown', event);
+    expect(props.onSelect).toHaveBeenCalledTimes(1);
+    expect(props.onKeyDown).toHaveBeenCalledTimes(1);
+  });
+
+  test('onClick is not called when the tab is disabled', () => {
+    const props = {
+      onClick: jest.fn(),
+      disabled: true,
+    };
+    const wrapper = mount(<Tab {...props}>Title</Tab>);
+    const tab = wrapper.find(StyledTab);
+    tab.simulate('click');
+    expect(props.onClick).not.toHaveBeenCalledTimes(1);
+  });
+
+  test('onSelect nor onKeydown is not called on key presses when the tab is disabled', () => {
+    const props = {
+      onSelect: jest.fn(),
+      onKeyDown: jest.fn(),
+      disabled: true,
+    };
+    const wrapper = mount(<Tab {...props}>Title</Tab>);
+    const tab = wrapper.find(StyledTab);
+    tab.simulate('keydown', {key: 'Enter'});
+    tab.simulate('keydown', {which: 32});
+    expect(props.onSelect).not.toHaveBeenCalledTimes(1);
+    expect(props.onKeyDown).not.toHaveBeenCalledTimes(1);
+  });
+});