4

When I am presented with a design to implement using Material UI there is invariably some vertical space between section headers, form labels, input fields, etc. There seem to be a few ways to achieve this:

  1. Wrap each <Typography />, <Checkbox />, etc. in a <Box paddingBottom={2} />.
  2. Create a class for each element with the spacing, e.g.
const useStyles = makeStyles(theme => ({ subHeader: { marginBottom: theme.spacing(2) } }));
...
const classes = useStyles();
...
<Typography className={classes.subHeader} />
  1. Use inline styles, e.g.
const theme = useTheme();
<Typography style={{ marginBottom: theme.spacing(2) }} />

Each of these approaches doesn't seem right to me.

The first adds a lot of extra divs to your HTML code and guarantees that adjacent conceptual elements are never adjacent; they are always nested.

<div class="MuiBox-root">
  <span class="MuiTypography-root" />
</div>
<div class="MuiBox-root">
  <span class="MuiTypography-root" />
</div>

With the second you end up creating lots of fairly meaningless classes to accommodate needing different spacing below each element for purely design/aesthetic reasons, not semantic reasons, such as a class with marginBottom: 2, and one with marginBottom: 3.

The third option seems to make sense as extracting out spacing logic into reusable code seems overkill, but inline styles are generally frowned upon, and having to call const theme = useTheme() in every component doesn't seem right.

TLDR; What is the recommended way for spacing components vertically in Material UI?

Ryan Cogswell
  • 75,046
  • 9
  • 218
  • 198
jjt
  • 173
  • 4
  • 9
  • You're dissatisfied with creating classes for purely aesthetic reasons, and dissatisfied with using inline styles, but these are literally the only two ways you can do it. There's no need to worry about creating classes for aesthetic reasons, you'll never be able to semantically justify every little design and spacing detail. – Andy Sep 20 '21 at 15:36
  • That said, it makes perfect sense to want to avoid creating a bunch of classes that all do `marginBottom: 2`. For that you can share a `makeStyles` hook between components – Andy Sep 20 '21 at 15:38

3 Answers3

2

I would recommend using the clone prop of Box. This causes it to add the styles to its child (via React.cloneElement) rather than wrapping it with an extra element.

The example below adds bottom margin to the first Typography and left margin to the second without introducing any additional wrapper elements in the html.

import React from "react";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";

export default function App() {
  return (
    <>
      <Box mb={3} clone>
        <Typography variant="h5" color="primary">
          Some Text
        </Typography>
      </Box>
      <Box ml={2} clone>
        <Typography color="primary">Later Text</Typography>
      </Box>
    </>
  );
}

Edit Box clone

Unfortunately, as discussed in the comments, using the clone prop of Box can be brittle when there is overlap between the styles being set by the Box and the styles being set by the wrapped component (e.g. Typography) since then the order of import impacts which one wins (and not just the order of import in the particular file you are focusing on, but rather the order of their first import in the app).

One solution for these cases is to create your own wrapper component to imitate the functionality in Box that you want to use frequently. For instance, below is a component that can be used in place of Typography to control margin in a Box-like fashion:

import * as React from "react";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import clsx from "clsx";

const useStyles = makeStyles(theme => ({
  margin: ({ mb, mt, ml, mr }) => ({
    marginBottom: mb === undefined ? undefined : theme.spacing(mb),
    marginTop: mt === undefined ? undefined : theme.spacing(mt),
    marginLeft: ml === undefined ? undefined : theme.spacing(ml),
    marginRight: mr === undefined ? undefined : theme.spacing(mr)
  })
}));

const TypographyWithMargin = React.forwardRef(function TypographyWithMargin(
  { className, mb, ml, mt, mr, ...other },
  ref
) {
  const classes = useStyles({ mb, ml, mt, mr });
  return (
    <Typography
      {...other}
      className={clsx(className, classes.margin)}
      ref={ref}
    />
  );
});
export default TypographyWithMargin;

and then this can be used as follows:

import React from "react";
import Typography from "./TypographyWithMargin";

export default function App() {
  return (
    <>
      <Typography mb={3} variant="h5" color="primary">
        Some Text
      </Typography>
      <Typography ml={2} color="primary">
        Later Text
      </Typography>
    </>
  );
}

Edit Box imitation

Ryan Cogswell
  • 75,046
  • 9
  • 218
  • 198
  • I didn't know about `clone`. Looks like a clean option. I'll give it some more time before accepting to see if more answers come in. – jjt Jun 25 '20 at 09:47
  • I'm having issues with implementing this answer due to .MuiTypography-root setting `margin-bottom: 0;` and taking precedence. I suspect this is due to the order of imports as explained here: https://github.com/mui-org/material-ui/issues/15993. The recommended solution of ordering the imports isn't working for me. I suspect Next.js is changing the order. – jjt Jun 25 '20 at 10:16
  • I doubt next.js is changing the order, but you have to pay attention to the order in your entire app, not just the one file. If the first time that Box is imported comes before the first time that Typography is imported, then Typography will win. – Ryan Cogswell Jun 25 '20 at 11:24
  • @jjt I've added an alternate solution for cases where `Box` with the `clone` property doesn't work nicely. – Ryan Cogswell Jun 25 '20 at 12:40
  • 1
    For v5 (still at least 6 months out) the features of Box will likely be baked into the core components and then you would have a more ideal option available. – Ryan Cogswell Jun 25 '20 at 12:55
  • 1
    Thanks @Ryan, I really appreciate the follow up solutions. Looking forward to the baked in solution. – jjt Jun 26 '20 at 10:41
0

I've applied this, by Heydon Pickering.

I created a component Vertical.js:

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Box } from '@material-ui/core';

const useStyles = makeStyles((theme) => ({
  vertical: {
    '& > *+*': {
      marginTop: '1.5rem',
    },
  },
}));

const Vertical = ({ children }) => {
  const classes = useStyles();
  return <Box className={classes.vertical}>{children}</Box>;
};

export default Vertical;

Then use it in any other components e.g. Example.js:

import React from 'react';
import Vertical from './Vertical';

const Example = () => {
  return (
    <Vertical>
      <Component/>
      <Component />
      <Another />
      <AnotherComponent />
    </Vertical>
  );
};

export default Example;
John
  • 23
  • 4
0

You could avoid creating a bunch of duplicate CSS classes by sharing a makeStyles hook with some margin utility classes among your components:

// useMargins.js

import { makeStyles } from "@material-ui/core/styles"

export const useMargins = makeStyles((theme) => ({
  mt2: { marginTop: theme.spacing(2) },
  ml2: { marginLeft: theme.spacing(2) },
  mr2: { marginRight: theme.spacing(2) },
  mb2: { marginBottom: theme.spacing(2) },
}))

// MyComponent.js

import * as React from "react"
import Typography from "@material-ui/core/Typography"
import { useMargins } from "./useMargins"

export default function MyComponent(props): React.Node {
  const { mb2 } = useMargins()
  return <Typography className={mb2}>Hello</Typography>
}
Andy
  • 7,885
  • 5
  • 55
  • 61