- Start Date: 2019-12-11
- Summary
- Motivation
- Detailed design
- CSS Grid Layout
- Alternatives considered
- Comparison methodology
- Adoption strategy
- How we teach this
A responsive grid system is a layout component that provides guidance on how components should be positioned within a user interface, adapting to screen size and orientation. We, the Carbon team, would like to implement the grid system designed by Sage UI designers.
The grid system is intended to solve a number of problems that are often experienced when developing complex web applications:
- CSS floats are not well suited to application layout.
- Layouts that use a combination of tables, JavaScript and floated elements are often brittle and can behave unexpectedly.
- Fixed layouts that behave predictably cannot take advantage of the entire screen real estate.
A well designed grid system will be:
- Consistent, which helps users navigate and understand where information will be found.
- Responsive, which gives users a predictable experience across multiple devices with different screen sizes.
- Adaptable, enabling new design themes to be introduced more quickly as designs evolve over time.
The Grid component is designed to provide:
- A common language for designers and developers to use when discussing layout.
- Structure and responsive layout without a developer writing additional CSS.
- No fixed breakpoints; responsive adjustments can be customised at a
GridItem
level. - Position and width calculated dynamically.
To avoid introducing a breaking change, and to give developers the freedom to choose whether or not to use the component, a new GridContainer
component will be developed, which can be used as a child of the AppWrapper
. As the parent wrapper component for all GridItems
and the element on which the display: grid
property is set, the GridContainer
will define 12 equal-width columns, including external margins and internal gutters.
12 is divisible into 12, 6, 4, 3, 2 or 1 equal-width columns, giving maximum possible flexibility for a layout.
By dividing the available space into predictably-sized modules (both vertically and horizontally), complex layouts that don't have fixed dimensions can be controlled and components can be positioned allowing a clean separation of content and style.
The container for a GridContainer
could specify either 100% or a fixed pixel width. By default the GridContainer
will respect the boundaries of this outer element, so if an outer element has a max-width: 1280px
, the GridContainer
including it's margins and gutters will be based on that size.
Margins for the GridContainer
are set automatically in the CSS, following the values in this table.
Breakpoints | Device Type | Margins |
---|---|---|
Extra Small (320 to 599px) | Smart Phones | 16px |
Small (600 to 959px) | Portrait Tablets | 24px |
Medium (960 to 1259px) | Landscape Tablets & Low-Res Laptops | 32px |
Large (1260 to 1920px) | High-Res Laptops & Monitors | 40px |
Extra Large (1921px and above) | Ultra High-Res Monitors | 40px |
The child of a GridContainer
is a GridItem
, which can have a set of breakpoints applied that are used to control how each GridItem
responds at given widths.
- Can be defined at any width between 1 and 12, each unit corresponding to a column of the
GridContainer
. - Unlimited breakpoints provide dynamic adjustment at any viewport width.
- Gutters are calculated automatically based on a the number of
GridItems
. - Can be re-ordered on the page by viewport dimensions.
- Can be styled to look like a container.
- Can be passed a range of values for padding.
Gutters for the GridItem
are set automatically in the CSS, following the values in this table.
Breakpoints | Device Type | Gutters |
---|---|---|
Extra Small (320 to 599px) | Smart Phones | 16px |
Small (600 to 959px) | Portrait Tablets | 16px |
Medium (960 to 1259px) | Landscape Tablets & Low-Res Laptops | 24px |
Large (1260 to 1920px) | High-Res Laptops & Monitors | 24px |
Extra Large (1921px and above) | Ultra High-Res Monitors | 40px |
Below is an example of how a Grid could be included as a component and used to implement the proposed solution used in the CSS comparison
.
import React from "react";
import { ThemeProvider } from "styled-components";
import { sageTheme } from "carbon-react/lib/style/themes";
import "carbon-react/lib/utils/css";
import { startRouter } from "carbon-react/lib/utils/router";
import AppWrapper from "carbon-react/lib/components/app-wrapper";
import { GridContainer, GridItem } from "./components/grid";
const App = () => {
const item1_900 = {
maxWidth: 900,
unitType: 'px',
colStart: 1,
colEnd: 9,
rowStart: 2,
rowEnd: 2,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const item1_1300 = {
maxWidth: 1300,
unitType: 'px',
colStart: 1,
colEnd: 13,
rowStart: 1,
rowEnd: 1,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const item1_1500 = {
maxWidth: 1500,
unitType: 'px',
colStart: 1,
colEnd: 7,
rowStart: 1,
rowEnd: 1,
alignSelf: 'stretch',
justifySelf: 'stretch'
}
const responsiveItem1Settings = [
item1_1300,
item1_900,
item1_1500
];
const item2_900 = {
maxWidth: 900,
unitType: 'px',
colStart: 1,
colEnd: 9,
rowStart: 3,
rowEnd: 3,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const item2_1300 = {
maxWidth: 1300,
unitType: 'px',
colStart: 1,
colEnd: 13,
rowStart: 2,
rowEnd: 2,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const item2_1500 = {
maxWidth: 1500,
unitType: 'px',
colStart: 7,
colEnd: 13,
rowStart: 1,
rowEnd: 1,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const responsiveItem2Settings = [
item2_1300,
item2_900,
item2_1500
];
const item3_900 = {
maxWidth: 900,
unitType: 'px',
colStart: 1,
colEnd: 9,
rowStart: 1,
rowEnd: 1,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const item3_1300 = {
maxWidth: 1300,
unitType: 'px',
colStart: 1,
colEnd: 13,
rowStart: 3,
rowEnd: 3,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const item3_1500 = {
maxWidth: 1500,
unitType: 'px',
colStart: 1,
colEnd: 13,
rowStart: 2,
rowEnd: 2,
alignSelf: 'stretch',
justifySelf: 'stretch'
};
const responsiveItem3Settings = [
item3_1300,
item3_900,
item3_1500
];
return (
<ThemeProvider theme={sageTheme}>
<AppWrapper>
<GridContainer>
<GridItem
responsiveSettings={responsiveItem1Settings}
>
cell 1
</GridItem>
<GridItem
responsiveSettings={responsiveItem2Settings}
>
cell 2
</GridItem>
<GridItem
responsiveSettings={responsiveItem3Settings}
>
cell 3
</GridItem>
</GridContainer>
</AppWrapper>
</ThemeProvider>
);
};
The GridItem component could be constructed with default props.
GridItem.defaultProps = {
gridColumnStart: 1,
gridColumnEnd: 13,
gridRowStart: 'auto',
gridRowEnd: 'auto'
}
export default GridItem;
This is a code fragment showing how a Styled Component would use defaults or props overides
return css`
margin: 0;
grid-column-start: ${gridColumnStart};
grid-column-end: ${gridColumnEnd};
grid-row-start: ${gridRowStart};
grid-row-end: ${gridRowEnd};
@media screen and (max-width: ${maxWidth+maxWidthUnits}) {
align-self: ${alignSelf};
justify-self: ${justifySelf};
grid-column-start: ${colStart};
grid-column-end: ${colEnd};
grid-row-start: ${rowStart};
grid-row-end: ${rowEnd};
}`
CSS Grid
Layout is a truly two-dimensional grid, allowing us to specify dimensions of rows and columns, and control where we position components within the grid.
- Provides a simple way to ensure spacing is always correct.
- Padding can be adjusted by viewport width.
- Layout can be controlled in two dimensions on the defined grid.
- No need to have width calculated by CSS, as grid spaces handle width.
Considering that a grid layout is created to serve the same content on different-width viewports, it comes with a few disadvantages:
- If two
GridItems
occupy the same space, one is hidden. - All the data is served to all viewports, regardless of how much real estate is available and how much content is actually displayed, adversely affecting page load speeds.
- Start and end positions are derived from supplied units and offset properties.
- To change one
GridItem
would also require adjustment of all other relatedGridItems
to avoid overlapping and colliding components. - The grid position of a component may be different to the DOM order, causing flow and context discrepancies when using screen readers.
- Users who navigate with a keyboard may experience unexpected sequences when tabbing through components.
The recommendation is that grid ordering and placement is used only for visual (not logical) reordering of content. For an explanation of what this means in practice, see the Mozilla CSS Grid Layout.
CSS Flexbox
is designed for layout in one dimension (either a row or a column). Although it does support wrapping, the second-dimension positioning of components cannot be controlled (since the components are just being pushed along a single axis until they wrap).
- If two
GridItems
get the same order, you will still see both. - You can pass a single value for number of units width required.
- Order will work as long as numbers are before/after.
- Move content with just an adjustment to single GridItem (i.e changes from 6 to 12 units and will auto push next column down)
- Overlapping modules within a Flexbox layout requires creative use of negative margins, transforms, and/or absolute positioning.
- Also has the same accessibility issues around screen readers and keyboard navigation as CSS Grid.
A CSS framework is a library of predefined CSS styles that are ready to use out-of-the-box. The best known of these are:
- Comprehensive documentation.
- Community support.
- Potentially quicker development speed.
- Including a CSS framework in Carbon (a library built with Styled Components at its heart) would require generating a special
StyledComponent
at a global level to enable CSS resets and base stylesheets, which may cause conflicts with Carbon's Base Theme. There would be other disadvantages too: - Difficulty in overriding the framework code.
- Bloated codebase that attempts to solve compatibility and support issues we don't need.
- More complex markup.
In order to evaluate the competing CSS styling options (Grid v Flexbox) a simple layout concept was proposed and a solution developed in each approach under consideration.
<h1>CSS Grid</h1>
<div class="GridContainer">
<div class="GridItem item1" data-start="1" data-end="7" data-row="1">1</div>
<div class="GridItem item2" data-start="7" data-end="13" data-row="1">2</div>
<div class="GridItem item3" data-start="1" data-end="13" data-row="1">3</div>
</div>
<div class="GridContainer">
<div class="GridItem item1" data-start="1" data-end="13" data-row="1">1</div>
<div class="GridItem item2" data-start="1" data-end="13" data-row="2">2</div>
<div class="GridItem item3" data-start="1" data-end="13" data-row="3">3</div>
</div>
<div class="GridContainer">
<div class="GridItem item1" data-start="1" data-end="13" data-row="2">1</div>
<div class="GridItem item2" data-start="1" data-end="13" data-row="3">2</div>
<div class="GridItem item3" data-start="1" data-end="13" data-row="1">3</div>
</div>
.GridContainer {
display: grid;
grid-gap: 24px;
grid-template-columns: repeat(12, 1fr);
width: 100%;
}
.GridItem {
box-sizing: border-box;
font-size: 14px;
line-height: 21px;
padding: 20px;
&[data-start="1"] { grid-column-start: 1; }
&[data-start="2"] { grid-column-start: 2; }
&[data-start="3"] { grid-column-start: 3; }
...
&[data-start="10"] { grid-column-start: 10; }
&[data-start="11"] { grid-column-start: 11; }
&[data-end="2"] { grid-column-end: 2; }
&[data-end="3"] { grid-column-end: 3; }
&[data-end="4"] { grid-column-end: 4; }
...
&[data-end="12"] { grid-column-end: 12; }
&[data-end="13"] { grid-column-end: 13; }
&[data-row="1"] { grid-row: 1; }
&[data-row="2"] { grid-row: 2; }
&[data-row="3"] { grid-row: 3; }
}
<h1>Flexbox</h1>
<div class="GridContainer">
<div class="GridItem item1" data-units="6" data-order="1">1</div>
<div class="GridItem item2" data-units="6" data-order="2">2</div>
<div class="GridItem item3" data-units="12" data-order="3">3</div>
</div>
<hr/>
<div class="GridContainer">
<div class="GridItem item1" data-units="12" data-order="1">1</div>
<div class="GridItem item2" data-units="12" data-order="2">2</div>
<div class="GridItem item3" data-units="12" data-order="3">3</div>
</div>
<hr/>
<div class="GridContainer">
<div class="GridItem item1" data-units="12" data-order="1">1</div>
<div class="GridItem item2" data-units="12" data-order="1">2</div>
<div class="GridItem item3" data-units="12" data-order="1">3</div>
</div>
$grid: 8.3333333333%;
.GridContainer {
align-items: flex-start;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
column-gap: 24px;
}
.GridItem {
box-sizing: border-box;
font-size: 14px;
line-height: 21px;
margin-bottom: 24px;
padding: 20px;
&[data-units="2"] { width: calc(#{$grid * 2} - 12px); }
&[data-units="3"] { width: calc(#{$grid * 3} - 12px); }
&[data-units="4"] { width: calc(#{$grid * 4} - 12px); }
...
&[data-units="11"] { width: calc(#{$grid * 11} - 12px); }
&[data-units="12"] { width: calc(#{$grid * 12}); }
&[data-order="1"] { order: 1; }
&[data-order="2"] { order: 2; }
&[data-order="3"] { order: 3; }
&.container {
background: white;
border: 1px solid #CCD6DB;
}
}
CSS Grid was adjudged the clear winner due to it's enhanced flexibility and control.
This is a new component and as such would not change the existing API, so it is not considered to be a breaking change. The new GridContainer
can be used at a page level to wrap any group of components. See the detailed design above for a code example.
Storybook and Fixtures testbed examples should be developed alongside the new component, demonstrating ways to implement the new layout.