Material-ui: Tabs indicator custom width and position

Created on 27 Feb 2018  路  18Comments  路  Source: mui-org/material-ui

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior


I'd like the ability to transform the indicator style (namely width and left) within the Tabs component based on the default value. This would be in the form of an optional function prop that looks like this:

function transformIndicatorStyle({left, width}) {
    return {
        left: left + 6,
        width: width - 12,
    };
}

This function would be invoked either before setting the indicatorStyle state value, or before the style is passed to the TabIndicator child during render.

Current Behavior


The Tabs indicator is locked to the left and width of the currently selected tab's getBoundingClientRect.

Steps to Reproduce (for bugs)

This sandbox demonstrates how far I'm able to go for indicator customization. I've modified the height and margins of the indicator using class overrides, but I'm not able to access or modify the width because it's a function of the selected tab size. Notice the overhang on the right side of the selection.

https://codesandbox.io/s/6j663n24zw

Context


We are creating a component that wraps AppBar and Tabs. It has a segmented option which places the indicator underneath the tab (z-axis) at full height, similar to what was done for #10123, with some margin around the sides. Style overrides allow us to mostly accomplish what we want, but we're unable to find a way to modify the indicator width so that it plays nicely with the margin.

segmented_tabs

Your Environment

| Tech | Version |
|--------------|---------|
| Material-UI | 1.0.0-beta.34 |
| React | 16.2.0 |
| browser | Chrome 64 |

Tabs enhancement

Most helpful comment

@HiranmayaGundu Ok, can you add this example in the documentation? 馃槃 https://codesandbox.io/s/xl526z333q alongside this demo?
https://material-ui.com/demos/tabs/#customized-tabs

import React from "react";
import PropTypes from "prop-types";
import { withStyles } 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";

const styles = theme => ({
  root: {
    backgroundColor: "#2e1534"
  },
  typography: {
    padding: theme.spacing.unit * 3
  }
});

const StyledTabs = withStyles(theme => ({
  indicator: {
    display: "flex",
    justifyContent: "center",
    backgroundColor: "transparent",
    "& > div": {
      maxWidth: 40,
      width: "100%",
      backgroundColor: "#635ee7"
    }
  }
}))(props => <Tabs {...props} TabIndicatorProps={{ children: <div /> }} />);

const StyledTab = withStyles(theme => ({
  root: {
    textTransform: "initial",
    color: "#9e88a2",
    fontWeight: theme.typography.fontWeightLight,
    fontSize: theme.typography.pxToRem(15),
    marginRight: theme.spacing.unit * 1,
    "&:hover": {
      color: "#fff"
    },
    "&$selected": {
      color: "#fff"
    },
    "&:focus": {
      color: "#fff"
    }
  },
  selected: {}
}))(props => <Tab disableRipple {...props} />);

class CustomizedTabs extends React.Component {
  state = {
    value: 0
  };

  handleChange = (event, value) => {
    this.setState({ value });
  };

  render() {
    const { classes } = this.props;
    const { value } = this.state;

    return (
      <div className={classes.root}>
        <StyledTabs value={value} onChange={this.handleChange}>
          <StyledTab label="Workflows" />
          <StyledTab label="Datasets" />
          <StyledTab label="Connections" />
        </StyledTabs>
        <Typography className={classes.typography} />
      </div>
    );
  }
}

CustomizedTabs.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(styles)(CustomizedTabs);

feb-14-2019 09-56-08

Thanks!

All 18 comments

I'd like the ability to transform the indicator style (namely width and left) within the Tabs component based on the default value. This would be in the form of an optional function prop that looks like this:

@jody-zeitler The indicator is rendered two different ways. One that is during for the server-sider rendering and one that is absolute for transitioning the active tab. Fixing both requirement might be challenging.

I'll look into the server-side aspect of it and see what might work.

I've realized I can accomplish what I want by adding left/right margin to the tab itself, so the bounding box is calculated appropriately. Updated sandbox:

capture d ecran 2018-02-28 a 14 46 59

import classnames from "classnames";
import MuiAppBar from "material-ui/AppBar";
import withStyles from "material-ui/styles/withStyles";
import MuiTabs from "material-ui/Tabs/Tabs";
import PropTypes from "prop-types";
import React from "react";

const SEGMENT_MARGIN = 6;

/**
 * Tabs are a composite of a static AppBar and the base Tabs component.
 */
const Tabs = props => {
  const { children, classes, light, segmented, ...otherProps } = props;

  const conditionalClasses = classnames({
    [classes.light]: light,
    [classes.segmented]: segmented
  });
  const appBarClassName = classnames(classes.appBar, conditionalClasses);
  const indicatorClassName = classnames(classes.indicator, conditionalClasses);
  const tabClassName = classnames(classes.tab, conditionalClasses);

  return (
    <MuiAppBar className={appBarClassName} position="static">
      <MuiTabs indicatorClassName={indicatorClassName} {...otherProps}>
        {React.Children.map(children, child => {
          return React.cloneElement(child, {
            "aria-label": child.props["aria-label"] || child.props.label,
            className: classnames(tabClassName, child.props.className)
          });
        })}
      </MuiTabs>
    </MuiAppBar>
  );
};

Tabs.propTypes = {
  /** set of Tab elements */
  children: PropTypes.node,
  /** class overrides */
  classes: PropTypes.shape({
    appBar: PropTypes.string,
    indicator: PropTypes.string
  }),
  /** use light theme colors */
  light: PropTypes.bool,
  /** use segmented selection style instead of underline */
  segmented: PropTypes.bool
};

Tabs.defaultProps = {};

const styles = ({ palette }) => ({
  appBar: {
    backgroundColor: palette.primary.main,
    color: palette.primary.contrastText,
    "&$light": {
      backgroundColor: palette.grey[100],
      color: palette.getContrastText(palette.grey[100]),
      "&$segmented": {
        backgroundColor: palette.grey[200],
        color: palette.getContrastText(palette.grey[200])
      }
    }
  },
  indicator: {
    height: 4,
    backgroundColor: palette.primary.dark,
    "&$light": {
      height: 2,
      backgroundColor: palette.primary.main,
      "&$segmented": {
        backgroundColor: palette.background.paper
      }
    },
    "&$segmented": {
      height: `calc(100% - ${SEGMENT_MARGIN * 2}px)`,
      bottom: SEGMENT_MARGIN,
      borderRadius: 4,
      "&$light": {
        height: `calc(100% - ${SEGMENT_MARGIN * 2}px)`
      }
    }
  },
  light: {},
  segmented: {},
  tab: {
    margin: `0 ${SEGMENT_MARGIN}px`,
    "&$segmented": {
      zIndex: 1
    }
  }
});

export default withStyles(styles)(Tabs);

https://codesandbox.io/s/9z9z4rkjny

@jody-zeitler where is the document about your custom code? I want to custom the width and left value of the red underline line too. Thanks a lot.

@wufeng87 width and left are calculated based on the bounding box of the selected tab. I couldn't find a good way to alter them directly so I applied a margin to the tab to achieve the look that I was aiming for. I was able to apply bottom and height directly to the indicator as those properties are not overridden by inline styles.

@jody-zeitler thanks. I moved to v1.0.0-beta, hope I could find a way to custom the indicator width.

We now demonstrate a tab customization example in the documentation: https://material-ui-next.com/demos/tabs/#customized-tabs.

@wufeng87 Were you able to find a way to access the default left value of the selected tab? I'd like to shrink the TabIndicator and centre it using the left value, but I don't see any way to access the left and width values as suggested in the issue.

@HiranmayaGundu Do you have a visual example? Maybe I can help.

@oliviertassinari This is what I was aiming for
tabs

@HiranmayaGundu Ok, can you add this example in the documentation? 馃槃 https://codesandbox.io/s/xl526z333q alongside this demo?
https://material-ui.com/demos/tabs/#customized-tabs

import React from "react";
import PropTypes from "prop-types";
import { withStyles } 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";

const styles = theme => ({
  root: {
    backgroundColor: "#2e1534"
  },
  typography: {
    padding: theme.spacing.unit * 3
  }
});

const StyledTabs = withStyles(theme => ({
  indicator: {
    display: "flex",
    justifyContent: "center",
    backgroundColor: "transparent",
    "& > div": {
      maxWidth: 40,
      width: "100%",
      backgroundColor: "#635ee7"
    }
  }
}))(props => <Tabs {...props} TabIndicatorProps={{ children: <div /> }} />);

const StyledTab = withStyles(theme => ({
  root: {
    textTransform: "initial",
    color: "#9e88a2",
    fontWeight: theme.typography.fontWeightLight,
    fontSize: theme.typography.pxToRem(15),
    marginRight: theme.spacing.unit * 1,
    "&:hover": {
      color: "#fff"
    },
    "&$selected": {
      color: "#fff"
    },
    "&:focus": {
      color: "#fff"
    }
  },
  selected: {}
}))(props => <Tab disableRipple {...props} />);

class CustomizedTabs extends React.Component {
  state = {
    value: 0
  };

  handleChange = (event, value) => {
    this.setState({ value });
  };

  render() {
    const { classes } = this.props;
    const { value } = this.state;

    return (
      <div className={classes.root}>
        <StyledTabs value={value} onChange={this.handleChange}>
          <StyledTab label="Workflows" />
          <StyledTab label="Datasets" />
          <StyledTab label="Connections" />
        </StyledTabs>
        <Typography className={classes.typography} />
      </div>
    );
  }
}

CustomizedTabs.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(styles)(CustomizedTabs);

feb-14-2019 09-56-08

Thanks!

@oliviertassinari Thanks for the prompt reply! I'll add it to the docs.

Was this intended behaviour for TabIndicatorProps? My understanding was that it was another option to style the Indicator, other than using the override.

@HiranmayaGundu I don't understand your point regarding the TabIndicatorProps property.

@oliviertassinari I think they were asking whether *Props properties being used to override children is intended behaviour

Yes, the indicator doesn't have any children we provide one.

How exactly do we style the indicator? TypeScript tells me I am not allowed to pass a indicator property into the classes prop of Tab.

Oh, I see, indicator must be supplied to the Tabs component, not to each Tab. It may be confusing at first, but makes sense that the indicator is not actually inside the tabs, just one underneath all tabs.

image

I want to customize the tab bar something like this with curve borders. Please see the attached screenshot. Can anyone help me out please

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sys13 picture sys13  路  3Comments

TimoRuetten picture TimoRuetten  路  3Comments

ryanflorence picture ryanflorence  路  3Comments

finaiized picture finaiized  路  3Comments

revskill10 picture revskill10  路  3Comments