Repository: mui/material-ui (TypeScript)
Date: 2019-07-18 06:27:32+00:00
Task type: Feature

Issue:
TabIndicatorProps is useless
<!--- Provide a general summary of the issue in the Title above -->

I am trying to restyle Tabs and Tabs indicator component. What I wanted to achieve is to make tab indicator smaller (80%). Is surprised me this is not possible in your current implementation. Documentation (https://material-ui.com/api/tabs) says that Tabs accept TabIndicatorProps that will be passed to indicator. However, the interface for it looks like this:
```
export interface TabIndicatorProps
  extends StandardProps<React.HTMLAttributes<HTMLDivElement>, TabIndicatorClassKey> {
  color: 'secondary' | 'primary' | string;
  style: { left: number; width: number };
}
```
Accepted style property is `left` and `width` as numbers. This is then used for indicator as left margin and width both in pixels. I believe width should accept `string` values like `80%` or `100px`. Now it is inconsisted and missleading. It is also useless it will never be responsive.


<!-- Checked checkbox should look like this: [x] -->
- [ ] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
TabIndicatorProps should accept `style: { left: number; width: string }` and use width directly.

## Current Behavior 😯
TabIndicatorProps should accepts `style: { left: number; width: number }` and width can be customized in pixels only.

## Your Environment 🌎
```
{
    "compilerOptions": {
        "target": "es5",
        "downlevelIteration": true,
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "preserve"
    },
    "include": ["src"],
	"plugins": [
		{ "transform": "ts-optchain/transform" },
	]
}
```

| Tech         | Version |
|--------------|---------|
| Material-UI  | v4.2.0  |
| React        |    16.8.6     |
| Browser      |  brave       |
| TypeScript   |3.4.5      |
| etc.         |         |



Issue discussion:

Solution patch:
diff --git a/docs/pages/api/tabs.md b/docs/pages/api/tabs.md
--- a/docs/pages/api/tabs.md
+++ b/docs/pages/api/tabs.md
@@ -25,9 +25,10 @@ import Tabs from '@material-ui/core/Tabs';
 | <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'div'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
 | <span class="prop-name">indicatorColor</span> | <span class="prop-type">enum:&nbsp;'secondary'&nbsp;&#124;<br>&nbsp;'primary'<br></span> | <span class="prop-default">'secondary'</span> | Determines the color of the indicator. |
 | <span class="prop-name">onChange</span> | <span class="prop-type">func</span> |  | Callback fired when the value changes.<br><br>**Signature:**<br>`function(event: object, value: any) => void`<br>*event:* The event source of the callback<br>*value:* We default to the index of the child (number) |
+| <span class="prop-name">orientation</span> | <span class="prop-type">enum:&nbsp;'horizontal'&nbsp;&#124;<br>&nbsp;'vertical'<br></span> | <span class="prop-default">'horizontal'</span> | The tabs orientation (layout flow direction). |
 | <span class="prop-name">ScrollButtonComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">TabScrollButton</span> | The component used to render the scroll buttons. |
 | <span class="prop-name">scrollButtons</span> | <span class="prop-type">enum:&nbsp;'auto'&nbsp;&#124;<br>&nbsp;'desktop'&nbsp;&#124;<br>&nbsp;'on'&nbsp;&#124;<br>&nbsp;'off'<br></span> | <span class="prop-default">'auto'</span> | Determine behavior of scroll buttons when tabs are set to scroll:<br>- `auto` will only present them when not all the items are visible. - `desktop` will only present them on medium and larger viewports. - `on` will always present them. - `off` will never present them. |
-| <span class="prop-name">TabIndicatorProps</span> | <span class="prop-type">object</span> | <span class="prop-default">{}</span> | Properties applied to the `TabIndicator` element. |
+| <span class="prop-name">TabIndicatorProps</span> | <span class="prop-type">object</span> | <span class="prop-default">{}</span> | Properties applied to the tab indicator element. |
 | <span class="prop-name">textColor</span> | <span class="prop-type">enum:&nbsp;'secondary'&nbsp;&#124;<br>&nbsp;'primary'&nbsp;&#124;<br>&nbsp;'inherit'<br></span> | <span class="prop-default">'inherit'</span> | Determines the color of the `Tab`. |
 | <span class="prop-name">value</span> | <span class="prop-type">any</span> |  | The value of the currently selected `Tab`. If you don't want any selected `Tab`, you can set this property to `false`. |
 | <span class="prop-name">variant</span> | <span class="prop-type">enum:&nbsp;'standard'&nbsp;&#124;<br>&nbsp;'scrollable'&nbsp;&#124;<br>&nbsp;'fullWidth'<br></span> | <span class="prop-default">'standard'</span> | Determines additional display behavior of the tabs:<br> - `scrollable` will invoke scrolling properties and allow for horizontally  scrolling (or swiping) of the tab bar.  -`fullWidth` will make the tabs grow to use all the available space,  which should be used for small views, like on mobile.  - `standard` will render the default state. |
@@ -45,7 +46,9 @@ This prop accepts the following keys:
 | Name | Description |
 |:-----|:------------|
 | <span class="prop-name">root</span> | Styles applied to the root element.
+| <span class="prop-name">vertical</span> | Styles applied to the root element if `orientation="vertical"`.
 | <span class="prop-name">flexContainer</span> | Styles applied to the flex container element.
+| <span class="prop-name">flexContainerVertical</span> | Styles applied to the flex container element if `orientation="vertical"`.
 | <span class="prop-name">centered</span> | Styles applied to the flex container element if `centered={true}` & `!variant="scrollable"`.
 | <span class="prop-name">scroller</span> | Styles applied to the tablist element.
 | <span class="prop-name">fixed</span> | Styles applied to the tablist element if `!variant="scrollable"`.
diff --git a/docs/src/pages/components/tabs/VerticalTabs.js b/docs/src/pages/components/tabs/VerticalTabs.js
new file mode 100644
--- /dev/null
+++ b/docs/src/pages/components/tabs/VerticalTabs.js
@@ -0,0 +1,100 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { makeStyles } from '@material-ui/core/styles';
+import Tabs from '@material-ui/core/Tabs';
+import Tab from '@material-ui/core/Tab';
+import Typography from '@material-ui/core/Typography';
+import Box from '@material-ui/core/Box';
+
+function TabPanel(props) {
+  const { children, value, index, ...other } = props;
+
+  return (
+    <Typography
+      component="div"
+      role="tabpanel"
+      hidden={value !== index}
+      id={`vertical-tabpanel-${index}`}
+      aria-labelledby={`vertical-tab-${index}`}
+      {...other}
+    >
+      <Box p={3}>{children}</Box>
+    </Typography>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.any.isRequired,
+  value: PropTypes.any.isRequired,
+};
+
+function a11yProps(index) {
+  return {
+    id: `vertical-tab-${index}`,
+    'aria-controls': `vertical-tabpanel-${index}`,
+  };
+}
+
+const useStyles = makeStyles(theme => ({
+  root: {
+    flexGrow: 1,
+    backgroundColor: theme.palette.background.paper,
+    display: 'flex',
+    height: 224,
+  },
+  tabs: {
+    borderRight: `1px solid ${theme.palette.divider}`,
+  },
+}));
+
+export default function VerticalTabs() {
+  const classes = useStyles();
+  const [value, setValue] = React.useState(0);
+
+  function handleChange(event, newValue) {
+    setValue(newValue);
+  }
+
+  return (
+    <div className={classes.root}>
+      <Tabs
+        orientation="vertical"
+        variant="scrollable"
+        value={value}
+        onChange={handleChange}
+        aria-label="Vertical tabs example"
+        className={classes.tabs}
+      >
+        <Tab label="Item One" {...a11yProps(0)} />
+        <Tab label="Item Two" {...a11yProps(1)} />
+        <Tab label="Item Three" {...a11yProps(2)} />
+        <Tab label="Item Four" {...a11yProps(3)} />
+        <Tab label="Item Five" {...a11yProps(4)} />
+        <Tab label="Item Six" {...a11yProps(5)} />
+        <Tab label="Item Seven" {...a11yProps(6)} />
+      </Tabs>
+      <TabPanel value={value} index={0}>
+        Item One
+      </TabPanel>
+      <TabPanel value={value} index={1}>
+        Item Two
+      </TabPanel>
+      <TabPanel value={value} index={2}>
+        Item Three
+      </TabPanel>
+      <TabPanel value={value} index={3}>
+        Item Four
+      </TabPanel>
+      <TabPanel value={value} index={4}>
+        Item Five
+      </TabPanel>
+      <TabPanel value={value} index={5}>
+        Item Six
+      </TabPanel>
+      <TabPanel value={value} index={6}>
+        Item Seven
+      </TabPanel>
+    </div>
+  );
+}
diff --git a/docs/src/pages/components/tabs/VerticalTabs.tsx b/docs/src/pages/components/tabs/VerticalTabs.tsx
new file mode 100644
--- /dev/null
+++ b/docs/src/pages/components/tabs/VerticalTabs.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
+import Tabs from '@material-ui/core/Tabs';
+import Tab from '@material-ui/core/Tab';
+import Typography from '@material-ui/core/Typography';
+import Box from '@material-ui/core/Box';
+
+interface TabPanelProps {
+  children?: React.ReactNode;
+  index: any;
+  value: any;
+}
+
+function TabPanel(props: TabPanelProps) {
+  const { children, value, index, ...other } = props;
+
+  return (
+    <Typography
+      component="div"
+      role="tabpanel"
+      hidden={value !== index}
+      id={`vertical-tabpanel-${index}`}
+      aria-labelledby={`vertical-tab-${index}`}
+      {...other}
+    >
+      <Box p={3}>{children}</Box>
+    </Typography>
+  );
+}
+
+function a11yProps(index: any) {
+  return {
+    id: `vertical-tab-${index}`,
+    'aria-controls': `vertical-tabpanel-${index}`,
+  };
+}
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      flexGrow: 1,
+      backgroundColor: theme.palette.background.paper,
+      display: 'flex',
+      height: 224,
+    },
+    tabs: {
+      borderRight: `1px solid ${theme.palette.divider}`,
+    },
+  }),
+);
+
+export default function VerticalTabs() {
+  const classes = useStyles();
+  const [value, setValue] = React.useState(0);
+
+  function handleChange(event: React.ChangeEvent<{}>, newValue: number) {
+    setValue(newValue);
+  }
+
+  return (
+    <div className={classes.root}>
+      <Tabs
+        orientation="vertical"
+        variant="scrollable"
+        value={value}
+        onChange={handleChange}
+        aria-label="Vertical tabs example"
+        className={classes.tabs}
+      >
+        <Tab label="Item One" {...a11yProps(0)} />
+        <Tab label="Item Two" {...a11yProps(1)} />
+        <Tab label="Item Three" {...a11yProps(2)} />
+        <Tab label="Item Four" {...a11yProps(3)} />
+        <Tab label="Item Five" {...a11yProps(4)} />
+        <Tab label="Item Six" {...a11yProps(5)} />
+        <Tab label="Item Seven" {...a11yProps(6)} />
+      </Tabs>
+      <TabPanel value={value} index={0}>
+        Item One
+      </TabPanel>
+      <TabPanel value={value} index={1}>
+        Item Two
+      </TabPanel>
+      <TabPanel value={value} index={2}>
+        Item Three
+      </TabPanel>
+      <TabPanel value={value} index={3}>
+        Item Four
+      </TabPanel>
+      <TabPanel value={value} index={4}>
+        Item Five
+      </TabPanel>
+      <TabPanel value={value} index={5}>
+        Item Six
+      </TabPanel>
+      <TabPanel value={value} index={6}>
+        Item Seven
+      </TabPanel>
+    </div>
+  );
+}
diff --git a/docs/src/pages/components/tabs/tabs.md b/docs/src/pages/components/tabs/tabs.md
--- a/docs/src/pages/components/tabs/tabs.md
+++ b/docs/src/pages/components/tabs/tabs.md
@@ -73,6 +73,10 @@ Here is an example of customizing the component. You can learn more about this i
 
 👑 If you are looking for inspiration, you can check [MUI Treasury's customization examples](https://mui-treasury.com/components/tabs).
 
+## Vertical tabs
+
+{{"demo": "pages/components/tabs/VerticalTabs.js"}}
+
 ## Nav Tabs
 
 By default tabs use a `button` element, but you can provide your own custom tag or component. Here's an example of implementing tabbed navigation:
diff --git a/packages/material-ui/src/Tabs/TabIndicator.d.ts b/packages/material-ui/src/Tabs/TabIndicator.d.ts
deleted file mode 100644
--- a/packages/material-ui/src/Tabs/TabIndicator.d.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as React from 'react';
-import { StandardProps } from '..';
-
-export interface TabIndicatorProps
-  extends StandardProps<React.HTMLAttributes<HTMLDivElement>, TabIndicatorClassKey> {
-  color: 'secondary' | 'primary' | string;
-  style: { left: number; width: number };
-}
-
-export type TabIndicatorClassKey = 'root' | 'colorSecondary' | 'colorPrimary';
-
-declare const TabIndicator: React.ComponentType<TabIndicatorProps>;
-
-export default TabIndicator;
diff --git a/packages/material-ui/src/Tabs/TabIndicator.js b/packages/material-ui/src/Tabs/TabIndicator.js
--- a/packages/material-ui/src/Tabs/TabIndicator.js
+++ b/packages/material-ui/src/Tabs/TabIndicator.js
@@ -5,7 +5,6 @@ import withStyles from '../styles/withStyles';
 import { capitalize } from '../utils/helpers';
 
 export const styles = theme => ({
-  /* Styles applied to the root element. */
   root: {
     position: 'absolute',
     height: 2,
@@ -13,25 +12,35 @@ export const styles = theme => ({
     width: '100%',
     transition: theme.transitions.create(),
   },
-  /* Styles applied to the root element if `color="primary"`. */
   colorPrimary: {
     backgroundColor: theme.palette.primary.main,
   },
-  /* Styles applied to the root element if `color="secondary"`. */
   colorSecondary: {
     backgroundColor: theme.palette.secondary.main,
   },
+  vertical: {
+    height: '100%',
+    width: 2,
+    right: 0,
+  },
 });
 
 /**
  * @ignore - internal component.
  */
 const TabIndicator = React.forwardRef(function TabIndicator(props, ref) {
-  const { classes, className, color, ...other } = props;
+  const { classes, className, color, orientation, ...other } = props;
 
   return (
     <span
-      className={clsx(classes.root, classes[`color${capitalize(color)}`], className)}
+      className={clsx(
+        classes.root,
+        {
+          [classes.vertical]: orientation === 'vertical',
+        },
+        classes[`color${capitalize(color)}`],
+        className,
+      )}
       ref={ref}
       {...other}
     />
@@ -52,7 +61,11 @@ TabIndicator.propTypes = {
    * @ignore
    * The color of the tab indicator.
    */
-  color: PropTypes.oneOf(['primary', 'secondary']),
+  color: PropTypes.oneOf(['primary', 'secondary']).isRequired,
+  /**
+   * The tabs orientation (layout flow direction).
+   */
+  orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
 };
 
 export default withStyles(styles, { name: 'PrivateTabIndicator' })(TabIndicator);
diff --git a/packages/material-ui/src/Tabs/TabScrollButton.d.ts b/packages/material-ui/src/Tabs/TabScrollButton.d.ts
deleted file mode 100644
--- a/packages/material-ui/src/Tabs/TabScrollButton.d.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { ExtendButtonBase } from '../ButtonBase';
-import { SimplifiedPropsOf } from '../OverridableComponent';
-
-declare const TabScrollButton: ExtendButtonBase<{
-  props: {
-    direction?: 'left' | 'right';
-    visible?: boolean;
-  };
-  defaultComponent: 'div';
-  classKey: TabScrollButtonClassKey;
-}>;
-
-export type TabScrollButtonClassKey = 'root';
-
-export type TabScrollButtonProps = SimplifiedPropsOf<typeof TabScrollButton>;
-
-export default TabScrollButton;
diff --git a/packages/material-ui/src/Tabs/TabScrollButton.js b/packages/material-ui/src/Tabs/TabScrollButton.js
--- a/packages/material-ui/src/Tabs/TabScrollButton.js
+++ b/packages/material-ui/src/Tabs/TabScrollButton.js
@@ -9,21 +9,32 @@ import withStyles from '../styles/withStyles';
 import ButtonBase from '../ButtonBase';
 
 export const styles = {
-  /* Styles applied to the root element. */
   root: {
-    color: 'inherit',
     width: 40,
     flexShrink: 0,
   },
+  vertical: {
+    width: '100%',
+    height: 40,
+    '& svg': {
+      transform: 'rotate(90deg)',
+    },
+  },
 };
 
 /**
  * @ignore - internal component.
  */
 const TabScrollButton = React.forwardRef(function TabScrollButton(props, ref) {
-  const { classes, className: classNameProp, direction, onClick, visible = true, ...other } = props;
+  const { classes, className: classNameProp, direction, orientation, visible, ...other } = props;
 
-  const className = clsx(classes.root, classNameProp);
+  const className = clsx(
+    classes.root,
+    {
+      [classes.vertical]: orientation === 'vertical',
+    },
+    classNameProp,
+  );
 
   if (!visible) {
     return <div className={className} />;
@@ -33,7 +44,6 @@ const TabScrollButton = React.forwardRef(function TabScrollButton(props, ref) {
     <ButtonBase
       component="div"
       className={className}
-      onClick={onClick}
       ref={ref}
       role={null}
       tabIndex={null}
@@ -61,15 +71,15 @@ TabScrollButton.propTypes = {
   /**
    * Which direction should the button indicate?
    */
-  direction: PropTypes.oneOf(['left', 'right']),
+  direction: PropTypes.oneOf(['left', 'right']).isRequired,
   /**
-   * Callback to execute for button press.
+   * The tabs orientation (layout flow direction).
    */
-  onClick: PropTypes.func,
+  orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
   /**
    * Should the button be present or just consume space.
    */
-  visible: PropTypes.bool,
+  visible: PropTypes.bool.isRequired,
 };
 
 export default withStyles(styles, { name: 'PrivateTabScrollButton' })(TabScrollButton);
diff --git a/packages/material-ui/src/Tabs/Tabs.d.ts b/packages/material-ui/src/Tabs/Tabs.d.ts
--- a/packages/material-ui/src/Tabs/Tabs.d.ts
+++ b/packages/material-ui/src/Tabs/Tabs.d.ts
@@ -1,6 +1,5 @@
 import * as React from 'react';
 import ButtonBase from '../ButtonBase/ButtonBase';
-import { TabIndicatorProps } from './TabIndicator';
 import { OverridableComponent, SimplifiedPropsOf } from '../OverridableComponent';
 
 declare const Tabs: OverridableComponent<{
@@ -10,9 +9,10 @@ declare const Tabs: OverridableComponent<{
     children?: React.ReactNode;
     indicatorColor?: 'secondary' | 'primary' | string;
     onChange?: (event: React.ChangeEvent<{}>, value: any) => void;
+    orientation?: 'horizontal' | 'vertical';
     ScrollButtonComponent?: React.ElementType;
     scrollButtons?: 'auto' | 'desktop' | 'on' | 'off';
-    TabIndicatorProps?: Partial<TabIndicatorProps>;
+    TabIndicatorProps?: Partial<React.HTMLAttributes<HTMLDivElement>>;
     textColor?: 'secondary' | 'primary' | 'inherit' | string;
     value: any;
     variant?: 'standard' | 'scrollable' | 'fullWidth';
diff --git a/packages/material-ui/src/Tabs/Tabs.js b/packages/material-ui/src/Tabs/Tabs.js
--- a/packages/material-ui/src/Tabs/Tabs.js
+++ b/packages/material-ui/src/Tabs/Tabs.js
@@ -22,10 +22,18 @@ export const styles = theme => ({
     WebkitOverflowScrolling: 'touch', // Add iOS momentum scrolling.
     display: 'flex',
   },
+  /* Styles applied to the root element if `orientation="vertical"`. */
+  vertical: {
+    flexDirection: 'column',
+  },
   /* Styles applied to the flex container element. */
   flexContainer: {
     display: 'flex',
   },
+  /* Styles applied to the flex container element if `orientation="vertical"`. */
+  flexContainerVertical: {
+    flexDirection: 'column',
+  },
   /* Styles applied to the flex container element if `centered={true}` & `!variant="scrollable"`. */
   centered: {
     justifyContent: 'center',
@@ -73,6 +81,7 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
     component: Component = 'div',
     indicatorColor = 'secondary',
     onChange,
+    orientation = 'horizontal',
     ScrollButtonComponent = TabScrollButton,
     scrollButtons = 'auto',
     TabIndicatorProps = {},
@@ -84,6 +93,13 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
   } = props;
   const scrollable = variant === 'scrollable';
   const isRtl = theme.direction === 'rtl';
+  const vertical = orientation === 'vertical';
+
+  const scrollStart = vertical ? 'scrollTop' : 'scrollLeft';
+  const start = vertical ? 'top' : 'left';
+  const end = vertical ? 'bottom' : 'right';
+  const clientSize = vertical ? 'clientHeight' : 'clientWidth';
+  const size = vertical ? 'height' : 'width';
 
   warning(
     !centered || !scrollable,
@@ -94,8 +110,8 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
   const [mounted, setMounted] = React.useState(false);
   const [indicatorStyle, setIndicatorStyle] = React.useState({});
   const [displayScroll, setDisplayScroll] = React.useState({
-    left: false,
-    right: false,
+    start: false,
+    end: false,
   });
   const [scrollerStyle, setScrollerStyle] = React.useState({
     overflow: 'hidden',
@@ -114,8 +130,11 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
       tabsMeta = {
         clientWidth: tabsNode.clientWidth,
         scrollLeft: tabsNode.scrollLeft,
+        scrollTop: tabsNode.scrollTop,
         scrollLeftNormalized: getNormalizedScrollLeft(tabsNode, theme.direction),
         scrollWidth: tabsNode.scrollWidth,
+        top: rect.top,
+        bottom: rect.bottom,
         left: rect.left,
         right: rect.right,
       };
@@ -147,49 +166,59 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
 
   const updateIndicatorState = useEventCallback(() => {
     const { tabsMeta, tabMeta } = getTabsMeta();
-    let left = 0;
+    let startValue = 0;
 
     if (tabMeta && tabsMeta) {
-      const correction = isRtl
-        ? tabsMeta.scrollLeftNormalized + tabsMeta.clientWidth - tabsMeta.scrollWidth
-        : tabsMeta.scrollLeft;
-      left = Math.round(tabMeta.left - tabsMeta.left + correction);
+      if (vertical) {
+        startValue = Math.round(tabMeta.top - tabsMeta.top + tabsMeta.scrollTop);
+      } else {
+        const correction = isRtl
+          ? tabsMeta.scrollLeftNormalized + tabsMeta.clientWidth - tabsMeta.scrollWidth
+          : tabsMeta.scrollLeft;
+        startValue = Math.round(tabMeta.left - tabsMeta.left + correction);
+      }
     }
 
     const newIndicatorStyle = {
-      left,
+      [start]: startValue,
       // May be wrong until the font is loaded.
-      width: tabMeta ? Math.round(tabMeta.width) : 0,
+      [size]: tabMeta ? Math.round(tabMeta[size]) : 0,
     };
 
     if (
-      (newIndicatorStyle.left !== indicatorStyle.left ||
-        newIndicatorStyle.width !== indicatorStyle.width) &&
-      !isNaN(newIndicatorStyle.left) &&
-      !isNaN(newIndicatorStyle.width)
+      (newIndicatorStyle[start] !== indicatorStyle[start] ||
+        newIndicatorStyle[size] !== indicatorStyle[size]) &&
+      !isNaN(newIndicatorStyle[start]) &&
+      !isNaN(newIndicatorStyle[size])
     ) {
       setIndicatorStyle(newIndicatorStyle);
     }
   });
 
   const scroll = scrollValue => {
-    animate('scrollLeft', tabsRef.current, scrollValue);
+    animate(scrollStart, tabsRef.current, scrollValue);
   };
 
   const moveTabsScroll = delta => {
-    const multiplier = isRtl ? -1 : 1;
-    const nextScrollLeft = tabsRef.current.scrollLeft + delta * multiplier;
-    // Fix for Edge
-    const invert = isRtl && detectScrollType() === 'reverse' ? -1 : 1;
-    scroll(invert * nextScrollLeft);
+    let scrollValue = tabsRef.current[scrollStart];
+
+    if (vertical) {
+      scrollValue += delta;
+    } else {
+      scrollValue += delta * (isRtl ? -1 : 1);
+      // Fix for Edge
+      scrollValue *= isRtl && detectScrollType() === 'reverse' ? -1 : 1;
+    }
+
+    scroll(scrollValue);
   };
 
-  const handleLeftScrollClick = () => {
-    moveTabsScroll(-tabsRef.current.clientWidth);
+  const handleStartScrollClick = () => {
+    moveTabsScroll(-tabsRef.current[clientSize]);
   };
 
-  const handleRightScrollClick = () => {
-    moveTabsScroll(tabsRef.current.clientWidth);
+  const handleEndScrollClick = () => {
+    moveTabsScroll(tabsRef.current[clientSize]);
   };
 
   const handleScrollbarSizeChange = React.useCallback(scrollbarHeight => {
@@ -205,29 +234,31 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
       <ScrollbarSize className={classes.scrollable} onChange={handleScrollbarSizeChange} />
     ) : null;
 
-    const scrollButtonsActive = displayScroll.left || displayScroll.right;
+    const scrollButtonsActive = displayScroll.start || displayScroll.end;
     const showScrollButtons =
       scrollable &&
       ((scrollButtons === 'auto' && scrollButtonsActive) ||
         scrollButtons === 'desktop' ||
         scrollButtons === 'on');
 
-    conditionalElements.scrollButtonLeft = showScrollButtons ? (
+    conditionalElements.scrollButtonStart = showScrollButtons ? (
       <ScrollButtonComponent
+        orientation={orientation}
         direction={isRtl ? 'right' : 'left'}
-        onClick={handleLeftScrollClick}
-        visible={displayScroll.left}
+        onClick={handleStartScrollClick}
+        visible={displayScroll.start}
         className={clsx(classes.scrollButtons, {
           [classes.scrollButtonsDesktop]: scrollButtons !== 'on',
         })}
       />
     ) : null;
 
-    conditionalElements.scrollButtonRight = showScrollButtons ? (
+    conditionalElements.scrollButtonEnd = showScrollButtons ? (
       <ScrollButtonComponent
+        orientation={orientation}
         direction={isRtl ? 'left' : 'right'}
-        onClick={handleRightScrollClick}
-        visible={displayScroll.right}
+        onClick={handleEndScrollClick}
+        visible={displayScroll.end}
         className={clsx(classes.scrollButtons, {
           [classes.scrollButtonsDesktop]: scrollButtons !== 'on',
         })}
@@ -244,28 +275,35 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
       return;
     }
 
-    if (tabMeta.left < tabsMeta.left) {
+    if (tabMeta[start] < tabsMeta[start]) {
       // left side of button is out of view
-      const nextScrollLeft = tabsMeta.scrollLeft + (tabMeta.left - tabsMeta.left);
-      scroll(nextScrollLeft);
-    } else if (tabMeta.right > tabsMeta.right) {
+      const nextScrollStart = tabsMeta[scrollStart] + (tabMeta[start] - tabsMeta[start]);
+      scroll(nextScrollStart);
+    } else if (tabMeta[end] > tabsMeta[end]) {
       // right side of button is out of view
-      const nextScrollLeft = tabsMeta.scrollLeft + (tabMeta.right - tabsMeta.right);
-      scroll(nextScrollLeft);
+      const nextScrollStart = tabsMeta[scrollStart] + (tabMeta[end] - tabsMeta[end]);
+      scroll(nextScrollStart);
     }
   });
 
   const updateScrollButtonState = useEventCallback(() => {
     if (scrollable && scrollButtons !== 'off') {
-      const { scrollWidth, clientWidth } = tabsRef.current;
-      const scrollLeft = getNormalizedScrollLeft(tabsRef.current, theme.direction);
-
-      // use 1 for the potential rounding error with browser zooms.
-      const showLeftScroll = isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
-      const showRightScroll = !isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
+      const { scrollTop, scrollHeight, clientHeight, scrollWidth, clientWidth } = tabsRef.current;
+      let showStartScroll;
+      let showEndScroll;
+
+      if (vertical) {
+        showStartScroll = scrollTop > 1;
+        showEndScroll = scrollTop < scrollHeight - clientHeight - 1;
+      } else {
+        const scrollLeft = getNormalizedScrollLeft(tabsRef.current, theme.direction);
+        // use 1 for the potential rounding error with browser zooms.
+        showStartScroll = isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
+        showEndScroll = !isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
+      }
 
-      if (showLeftScroll !== displayScroll.left || showRightScroll !== displayScroll.right) {
-        setDisplayScroll({ left: showLeftScroll, right: showRightScroll });
+      if (showStartScroll !== displayScroll.start || showEndScroll !== displayScroll.end) {
+        setDisplayScroll({ start: showStartScroll, end: showEndScroll });
       }
     }
   });
@@ -320,6 +358,7 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
   const indicator = (
     <TabIndicator
       className={classes.indicator}
+      orientation={orientation}
       color={indicatorColor}
       {...TabIndicatorProps}
       style={{
@@ -361,8 +400,18 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
   const conditionalElements = getConditionalElements();
 
   return (
-    <Component className={clsx(classes.root, className)} ref={ref} {...other}>
-      {conditionalElements.scrollButtonLeft}
+    <Component
+      className={clsx(
+        classes.root,
+        {
+          [classes.vertical]: vertical,
+        },
+        className,
+      )}
+      ref={ref}
+      {...other}
+    >
+      {conditionalElements.scrollButtonStart}
       {conditionalElements.scrollbarSizeListener}
       <div
         className={clsx(classes.scroller, {
@@ -375,6 +424,7 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
       >
         <div
           className={clsx(classes.flexContainer, {
+            [classes.flexContainerVertical]: vertical,
             [classes.centered]: centered && !scrollable,
           })}
           ref={childrenWrapperRef}
@@ -384,7 +434,7 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
         </div>
         {mounted && indicator}
       </div>
-      {conditionalElements.scrollButtonRight}
+      {conditionalElements.scrollButtonEnd}
     </Component>
   );
 });
@@ -433,6 +483,10 @@ Tabs.propTypes = {
    * @param {any} value We default to the index of the child (number)
    */
   onChange: PropTypes.func,
+  /**
+   * The tabs orientation (layout flow direction).
+   */
+  orientation: PropTypes.oneOf(['horizontal', 'vertical']),
   /**
    * The component used to render the scroll buttons.
    */
@@ -447,7 +501,7 @@ Tabs.propTypes = {
    */
   scrollButtons: PropTypes.oneOf(['auto', 'desktop', 'on', 'off']),
   /**
-   * Properties applied to the `TabIndicator` element.
+   * Properties applied to the tab indicator element.
    */
   TabIndicatorProps: PropTypes.object,
   /**


Test patch:
diff --git a/packages/material-ui/src/Tabs/TabIndicator.test.js b/packages/material-ui/src/Tabs/TabIndicator.test.js
--- a/packages/material-ui/src/Tabs/TabIndicator.test.js
+++ b/packages/material-ui/src/Tabs/TabIndicator.test.js
@@ -6,29 +6,34 @@ import TabIndicator from './TabIndicator';
 describe('<TabIndicator />', () => {
   let shallow;
   let classes;
+  const props = {
+    direction: 'left',
+    orientation: 'horizontal',
+    color: 'secondary',
+  };
   const style = { left: 1, width: 2 };
 
   before(() => {
     shallow = createShallow({ dive: true });
-    classes = getClasses(<TabIndicator color="secondary" style={style} />);
+    classes = getClasses(<TabIndicator {...props} />);
   });
 
   it('should render with the root class', () => {
-    const wrapper = shallow(<TabIndicator color="secondary" style={style} />);
+    const wrapper = shallow(<TabIndicator {...props} />);
     assert.strictEqual(wrapper.name(), 'span');
     assert.strictEqual(wrapper.hasClass(classes.root), true);
   });
 
   describe('prop: style', () => {
     it('should be applied on the root element', () => {
-      const wrapper = shallow(<TabIndicator color="secondary" style={style} />);
+      const wrapper = shallow(<TabIndicator {...props} style={style} />);
       assert.strictEqual(wrapper.props().style, style, 'should apply directly the property');
     });
   });
 
   describe('prop: className', () => {
     it('should append the className on the root element', () => {
-      const wrapper = shallow(<TabIndicator color="secondary" style={style} className="foo" />);
+      const wrapper = shallow(<TabIndicator {...props} className="foo" />);
       assert.strictEqual(wrapper.name(), 'span');
       assert.strictEqual(wrapper.hasClass('foo'), true);
     });
diff --git a/packages/material-ui/src/Tabs/TabScrollButton.test.js b/packages/material-ui/src/Tabs/TabScrollButton.test.js
--- a/packages/material-ui/src/Tabs/TabScrollButton.test.js
+++ b/packages/material-ui/src/Tabs/TabScrollButton.test.js
@@ -7,6 +7,8 @@ import ButtonBase from '../ButtonBase';
 describe('<TabScrollButton />', () => {
   const props = {
     direction: 'left',
+    visible: false,
+    orientation: 'horizontal',
   };
   let shallow;
   let mount;
@@ -33,7 +35,7 @@ describe('<TabScrollButton />', () => {
 
   describe('prop: !visible', () => {
     it('should render as a div with root class', () => {
-      const wrapper = shallow(<TabScrollButton {...props} visible={false} />);
+      const wrapper = shallow(<TabScrollButton {...props} />);
 
       assert.strictEqual(wrapper.name(), 'div');
       assert.strictEqual(wrapper.hasClass(classes.root), true);
diff --git a/packages/material-ui/src/Tabs/Tabs.test.js b/packages/material-ui/src/Tabs/Tabs.test.js
--- a/packages/material-ui/src/Tabs/Tabs.test.js
+++ b/packages/material-ui/src/Tabs/Tabs.test.js
@@ -611,4 +611,43 @@ describe('<Tabs />', () => {
       expect(style.backgroundColor).to.equal('green');
     });
   });
+
+  describe('prop: orientation', () => {
+    it('should support orientation="vertical"', () => {
+      const { setProps, container, getByRole } = render(
+        <Tabs value={1} variant="scrollable" scrollButtons="on" orientation="vertical">
+          <Tab />
+          <Tab />
+        </Tabs>,
+      );
+      const tablistContainer = getByRole('tablist').parentNode;
+      const tab = getByRole('tablist').children[1];
+
+      Object.defineProperty(tablistContainer, 'clientHeight', { value: 100 });
+      Object.defineProperty(tablistContainer, 'scrollHeight', { value: 100 });
+      tablistContainer.getBoundingClientRect = () => ({
+        top: 0,
+        bottom: 100,
+      });
+      tab.getBoundingClientRect = () => ({
+        top: 50,
+        height: 50,
+        bottom: 100,
+      });
+      setProps();
+      let style;
+      style = container.querySelector(`.${classes.indicator}`).style;
+      expect(style.top).to.equal('50px');
+      expect(style.height).to.equal('50px');
+      tab.getBoundingClientRect = () => ({
+        top: 60,
+        height: 50,
+        bottom: 110,
+      });
+      setProps();
+      style = container.querySelector(`.${classes.indicator}`).style;
+      expect(style.top).to.equal('60px');
+      expect(style.height).to.equal('50px');
+    });
+  });
 });