Skip to main content

Custom layouts

You can create custom layouts using Jutro components. Benefits include:

  • Separation of concerns - metadata can be focused on content only, not mixed with layout definitions
  • Reuse of layouts between different pages
  • Easier to make changes both to content and layout
  • Documenting and naming layouts used in the application

On this page, we show you how to use GridLayout for building custom layouts.

Creating a layout

There are many different techniques to achieve the desired result. You can use GridItems, GridLayout props, nest layouts, or create separate layouts for each breakpoint.

GridItems

A component position and behavior can be modified by wrapping it in a GridItem component. For example, to make one of the components two columns wide you can write:

<GridLayout
columns={[1, 1]}
id="grid-layout">
<SomeElement />
<SomeElement />
<GridItem colSpan={2}>
<SomeElement />
</GridItem>
</GridLayout>

GridLayout automatically wraps children in GridItems, so you can define all properties of GridItems in a single place by passing props to GridLayout.

<GridLayout
columns={[1, 1]}
gridItems={[null, null, { colSpan: 2 }]}
id="grid-layout">
<SomeElement />
<SomeElement />
<SomeElement /> // will be 2-column wide
</GridLayout>
warning

GridItem passes a style prop to a wrapped component. The component then has to pass it to the underlying HTML tag. If it doesn't do that (some Jutro components don't), try passing clone={false} to GridItem:

<GridItem
colSpan={2}
clone={false}>
<SomeElement />
</GridItem>

Then SomeElement will be wrapped in an extra <div> that is styled instead.

Check the documentation page for grid items for more information.

Nesting GridLayouts

You can put GridLayouts inside another GridLayout to implement more complex designs:

<GridLayout
columns={[1, 1]}
id="grid-layout">
<GridLayout id="column-1">
<SomeElement />
<SomeElement />
</GridLayout>
<GridLayout id="column-2">
<SomeElement />
<SomeElement />
</GridLayout>
</GridLayout>

Using breakpoints

There are special props that can be used to change layout on different screen sizes: tablet, phoneWide, and phone. Properties passed to these props are applied on specific breakpoints.

To change the number of columns on different screen sizes you can use:

<GridLayout
columns={[1, 1, 1, 1]}
tablet={{ columns: [1, 1, 1] }}
phoneWide={{ columns: [1, 1] }}
phone={{ columns: [1] }}
id="grid-layout">
<SomeElement />
<SomeElement />
<SomeElement />
</GridLayout>

You could also customize everything based on the current breakpoint. This technique, however, is not recommended. Do not use conditionals with a breakpoint value.

const SomeLayout = (props) => {
const { breakpoint, breakpointProps } = useBreakpoint(props);
// This is wrong, do not try this!
if (breakpoint === 'tablet') {
return <GridLayout>/*tablet-specific layout*/</GridLayout>;
} else if (breakpoint === 'phoneWide') {
return <GridLayout>/*wide-phone-specific layout*/</GridLayout>;
} else if (breakpoint === 'phone') {
return <GridLayout>/*phone-specific layout*/</GridLayout>;
} else {
return <ColumnLayout>/*default layout*/</ColumnLayout>;
}
};

On a side note: useBreakpoint also returns breakpointProps with overrides from tablet, phoneWide and phone applied to normal props. This is how some Jutro components are implemented.

Passing children to a layout

To make a layout component reusable, you need a way for passing child components.

Using order of children

A simple technique is to differentiate children based on their order, for example: "the first child is always two columns wide, the fourth is placed in the third column". It can be used when the layout is simple, and ideally the number of children is known.

const SomeLayout = (props) => {
const { children } = props;

return (
<GridLayout
gridItems={[{ colSpan: 2 }, null, null, { colStart: 3 }]}
id="grid-layout">
{children}
</GridLayout>
);
};

// usage:
<SomeLayout>
<SomeComponent /> // will be 2 columns wide
<SomeComponent />
<SomeComponent />
<SomeComponent /> // will be in the third column
</SomeLayout>;

Using the slot prop in children

To make placement of children explicit and independent of their order, you can add a special prop (slot) to all children of the layout. Please note that the name of the prop is not treated in any special way by Jutro.

import { groupBy } from 'lodash';

const SomeLayout = (props) => {
const { children } = props;
const childArray = React.Children.toArray(children);
const childrenBySlot = groupBy(childArray, (child) => child.props.slot);
// alternatively:
// const leftColumn = childArray.filter((child) => child.props.slot === "left");
// const rightColumn = childArray.filter((child) => child.props.slot === "right");

return (
<GridLayout
columns={[1, 1]}
id="grid-layout">
<GridLayout id="left-column">{childrenBySlot['left']}</GridLayout>
<GridLayout id="right-column">{childrenBySlot['right']}</GridLayout>
{childrenBySlot[undefined]}
</GridLayout>
);
};

// usage:
<SomeLayout>
<SomeComponent slot="left" />
<SomeComponent slot="left" />
<SomeComponent slot="right" />
<SomeComponent slot="left" />
<SomeComponent />
<SomeComponent slot="invalidSlot" /> // won't be rendered at all
</SomeLayout>;

In the example above, children are split based on the slot property into a few arrays, which are then used in the layout. Children with unknown slot are not rendered in this example. The order of children with the same slot prop matters, but children with different slots can be mixed with each other.

You can decide to use semantic names for slots like "important", "navigation", "informational", so for example columns can be reordered later without changes in usage.

Using a wrapper component

If you cannot use the slot prop with your child components, you can use a wrapper component. Add a simple component that passes children through, and wrap each child with it.

For example, the slot prop might not work when you are using Typescript but you do not want to suppress the "Unknown property" errors, or when the child components are already using the slot prop for a different purpose.

Implementation of the SomeLayout component is the same as in the previous example.

const SomeLayoutItem = ({ children, slot }) => <>{children}</>;

// usage:
<SomeLayout>
<SomeLayoutItem slot="left">
<SomeComponent />
</SomeLayoutItem>
<SomeLayoutItem slot="right">
<SomeComponent />
</SomeLayoutItem>
</SomeLayout>;

Using render props

As an alternative to using the slot property, you can use render props to render children. This is not recommended, because it is harder to use with metadata.

const SomeLayout = (props) => {
const { children, renderLeftColumn, renderRightColumn } = props;

return (
<GridLayout
columns={[1, 1]}
id="grid-layout">
<GridLayout id="left-column">{renderLeftColumn()}</GridLayout>
<GridLayout id="right-column">{renderRightColumn()}</GridLayout>
</GridLayout>
);
};

// usage:
<SomeLayout
renderLeftColumn={() => (
<>
<SomeComponent />
<SomeComponent />
<SomeComponent />
</>
)}
renderRightColumn={() => <SomeComponent />}
/>;

Tips & tricks

Breakpoints in CSS

Breakpoints are accessible in CSS to further customize the layout. For example, you can remove a padding on phones:

@import '~@jutro/theme/assets/sass/helpers';

.someLayout {
padding: var(--GW-LAYOUT-6);

@include gw-breakpoint-down(phone) {
padding: 0;
}
}

Pass ID through props

If the layout component appears on a page many times, you have to assign different ids each time it's used. Don't forget to assign an ID to every GridLayout, Grid and ColumnLayout used. Other examples on this page are not passing the ID only for clarity.

const SomeLayout = (props) => {
const { id } = props;
return <GridLayout id={id} />;
};

// usage:
return (
<SomePage>
<SomeLayout id="form-1" />
<SomeLayout id="other-form" />
</SomePage>
);

Use simple layout components

If you don't use any of the techniques from this document, it is still acceptable and recommended to make small components like this one:

const SimpleLayout = (props) => {
const { id, children } = props;
return (
<GridLayout
id={id}
columns={['1fr', '70px']}>
{children}
</GridLayout>
);
};