Skip to main content

Responsive and adaptive development

Looking for design guidelines? See Responsive and adaptive design.

Terms

Jutro components can be:

  • responsive: The component adjusts to the screen size automatically using media queries.
  • adaptive: The component does not adjust to the screen size, and instead you should configure them using the breakpoint props. The component renders differently depending on the device.
  • fixed: The component does not adjust to the screen size.

In Storybook, each component is labelled as responsive, adaptive, or fixed in the top bar.

While most components are responsive across all types of devices, some might not be as convenient to use the same way on small devices as they are on large ones. We recommend you configure these components using breakpoint props.

How to work with designers

A designer can provide you with app designs for different breakpoints. Ask them about expected behaviors and so on, to make sure you understand how the user can get the most out of the application.

The following are general tips for working with designers:

  • Look for designs of the same screen on different screens.
  • Designers may use the term "form factor" to mean something similar to breakpoint, for example, desktop, tablet, and so on.
  • Use "inspect" tools in the design app (for example Figma) to find out the requested spacing and sizes. Make sure to use Jutro spacing and sizing variables (see Theming).
  • Show your app to a designer as soon as you can to get feedback and implement more features iteratively.
  • Get clarification on items you do not understand or are not sure how they should work.
  • Use developer tools in your browser to test layout on different breakpoints.
  • If the application is required to work on phones, develop for phone first and move to larger sizes later.

Feasibility

Review the designs with the designer and work on feasibility. This process is also known as "UI QA" (quality assurance):

  • Are all components available? For example, you are asked to provide an image carousel, but that is not a standard Jutro component. That means you will have to create your own.
  • Do all components work as intended? You may need to create a POC app at this point.
  • If there are complex features or interactions happening, check if they are allowed in Jutro and cost-effective to implement? For example, is the carousel the best way ot meet the use case? It will require an option to stop image switching to help with motions sickness. Is it feasible to build this functionality within the scope of your project?

Breakpoint props

The breakpoint props are:

  • phone
  • phoneWide (mobile phone in horizontal mode)
  • tablet
  • desktop

Use these props to override the behavior of your component on different devices.

Override children

For example, you want your action title bar to not include an action when the user is on a mobile device. In that case, you can use the phone prop to override the children of the component.

<ActionTitleBar
phone={{
children: (
<TitleElement>
{/* On a phone, render only a heading */}
<h4>Just a title</h4>
</TitleElement>
),
}}>
{/* On large screens, render an icon with a tooltip and a button */}
<TitleElement>
<h4>Title</h4>
<TooltipIcon {...tooltipIconProps} />
<Button {...buttonProps}>Edit</Button>
</TitleElement>
</ActionTitleBar>

You can make most components render differently on different devices using breakpoint props.

Override props

These props override component props when rendering on a particular device. For example:

<RadioButtonField
availableValues={[
{
displayName: 'Perform payment on checkout',
},
{
displayName: 'Defer payment to a later date',
},
]}
phone={{
availableValues: [
{
displayName: 'Pay now',
},
{
displayName: 'Pay later',
},
],
}}
/>

This is a very simplistic example of displaying different labels for checkboxes on small screens. Normally, you would use intl messages to render text for a component, but this example illustrates how you would pass a copy of the availableValues prop and override the items it contains.

Override styles

Another example is overriding styles for different devices:

<Card
className={styles.largeBanner}
phone={{
className: styles.smallBanner,
}}
tablet={{
className: styles.mediumBanner,
}}
/>

In this example, we have a card that is styled differently depending on the device size.

The useBreakpoint hook

info

For new components introduced in Jutro 10 as well as a general recommendation for that purpose we recommend to use useBreakpoint hook. Breakpoint props like phone, tablet and phoneWide are not available on new components out of the box.

The useBreakpoint hook is exported from @jutro/layout and can be imported like import { useBreakpoint } from '@jutro/layout'. It returns an object with the following properties:

  • breakpointProps - the props you passed into the component merged with the values you overrode in one of the following props: phone, phoneWide, tablet
  • applyBreakpoint - a function that takes props as an argument and returns breakpointProps. You can use it in any nested code within your component where hooks are not allowed.
  • breakpoint - the name of the current breakpoint. Any of phone, phoneWide, tablet, or desktop.

There are four ways you can use the hook to get the expected result.

1. Get props from the hook for a certain breakpoint

The first way to use the hook is to pass the props and apply the output to the component.

In the following example, the phone props object is passed to useBreakpoint. The props are then returned as textInputPhoneProps on the phone breakpoint. Then they are assigned to TextInput and the previously added props are overridden. The label prop is overridden, then placeholder and className are added when the hook detects that the breakpoint changes to phone.

const { breakpointProps: textInputPhoneProps } = useBreakpoint({
phone: {
placeholder: 'Phone placeholder',
label: 'Phone label',
className: 'somePhoneClass',
},
});
return (
<TextInput
label="Text input"
{...textInputPhoneProps}
/>
);

2. Only get breakpoint status from hook

The second way to use the hook is to only get the breakpoint status and use it to change that prop based on the returned breakpoint. In the following example, breakpoint is obtained from the hook and it changes based on the viewport. A different value for the label prop can be assigned if the breakpoint is set to phone.

const { breakpoint } = useBreakpoint({});

return (
<TextInput label={breakpoint === 'phone' ? 'Phone label' : 'Text input'} />
);

3. Get the callback that returns breakpoint props

The third way to use the hook is to use applyBreakpoint - it is very similar to the first option but can be better if there are different components on the same page that would require many useBreakPoints instances. In the following example, the callback that is returning the props is obtained and then applyBreakpoint is assigned to every TextInput and different props are passed to every component.

const { applyBreakpoint } = useBreakpoint({});

return (
<React.Fragment>
<TextInput
label="Text input"
{...applyBreakpoint({
phone: {
placeholder: 'Phone placeholder',
label: 'Phone label',
className: 'somePhoneClass',
},
})}
/>
<TextInput
label="Text input instance 2"
{...applyBreakpoint({
phone: {
placeholder: 'Another Phone placeholder',
label: 'Another Phone label',
className: 'someOtherPhoneClass',
},
})}
/>
</React.Fragment>
);

4. All at once

It is possible to get all three things that are returned from useBreakpoint and use them all at once. For example:

const {
breakpointProps: textInputPhoneProps,
breakpoint,
applyBreakpoint,
} = useBreakpoint({
phone: {
placeholder: 'Phone placeholder',
label: 'Phone label',
className: 'somePhoneClass',
},
});

return (
<React.Fragment>
<TextInput
label="Text input"
{...textInputPhoneProps}
/>
<TextInput
label={breakpoint === 'phone' ? 'Phone label 2' : 'Text input'}
/>
<TextInput
label="Text input"
{...applyBreakpoint({
phone: {
placeholder: 'Phone placeholder 3',
label: 'Phone label 3',
className: 'someOtherPhoneClass',
},
})}
/>
</React.Fragment>
);

Different props for a breakpoint

A typical use case for the hook is when you want to pass custom props to a component based on the breakpoint. When using the component, you do the following:

<BigBanner
phone={{
message: 'Call us!',
}}
message="Get in touch"
/>

That means you want your users to see the message "Get in touch" in most cases. But, when they are on the phone, you want them to see a message that says "Call us!"

Inside the functional component, you use the useBreakpoint hook, and you pass the props as the only argument. Then, you extract the value of the message property.

On desktop, it returns "Get in touch", and on phone, it returns "Call us!"

import { useBreakpoint } from '@jutro/layout';

function BigBanner(props) {
const { breakpointProps } = useBreakpoint(props);

return <div>{breakpointProps.message}</div>;
}

Different styles for a breakpoint

Another use case for the hook is to apply different styling to a component based on the current breakpoint.

You create two classes in your styles module:

BigBanner.module.scss
.defaultColor {
color: magenta;
}

.phoneColor {
color: blue;
}

To use the styles, you get the current breakpoint using the hook. Then, you select a class based on the value of the hook.

BigBanner.js
import React from 'react';
import { useBreakpoint } from '@jutro/layout';
import styles from './BigBanner.module.scss';

function BigBanner(props) {
const { breakpoint } = useBreakpoint(props);

const myClass =
breakpoint === 'phone' ? styles.phoneColor : styles.defaultColor;

return <div className={myClass}>Welcome to my banner!</div>;
}

Or:

BigBanner.js
import React from 'react';
import cx from 'classnames';
import styles from './BigBanner.module.scss';
import { useBreakpoint } from '@jutro/layout';

function BigBanner(props) {
const { breakpointProps } = useBreakpoint({
...props,
className: cx(props.className, styles.defaultColor),
phone: {
className: cx(props.className, styles.phoneColor),
},
});
const { className } = breakpointProps;

return <div className={className}>{breakpointProps.message}</div>;
}

The BreakpointTracker context

You can also use BreakpointTrackerContext and BreakpointTracker directly. You can use them within class components or other places where using hooks is impossible.

The entire app is wrapped in this context provider, so you can use it within a class component, for example inside the render block. You can also use it within functions that are not nested inside functional components.

BreakpointTracker.applyBreakpointOverrides returns only a breakpointProps object.

It takes two arguments: props and a breakpoint.

Breakpoint context in a functional component

import React, { useContext } from 'react';
import { BreakpointTracker, BreakpointTrackerContext } from '@jutro/layout';

function BigBanner(props) {
const breakpoint = useContext(BreakpointTrackerContext);
const someRenderFunction = () => {
const breakpointProps = BreakpointTracker.applyBreakpointOverrides(
props,
breakpoint
);
return (
<pre>
<code>{JSON.stringify(breakpointProps, null, 2)}</code>
</pre>
);
};
return <div>{someRenderFunction()}</div>;
}

Breakpoint context in a class component

import React from 'react';
import { BreakpointTracker, BreakpointTrackerContext } from '@jutro/layout';

class ClassBanner extends React.Component {
static contextType = BreakpointTrackerContext;
render() {
const breakpointProps = BreakpointTracker.applyBreakpointOverrides(
this.props,
this.context
);
return (
<pre>
<code>{JSON.stringify(breakpointProps, null, 2)}</code>
</pre>
);
}
}

export default ClassBanner;

Breakpoints in unit tests

To test breakpoints, you can use the following example as a guide. It tests the BigBanner component which we created in other sections of this page.

import React from 'react';
import { render } from '@testing-library/react';
import { BreakpointTrackerContext } from '@jutro/layout';

import { BigBanner } from '../BigBanner';

import styles from '../BigBanner.module.scss';

function renderBigBannerWithBreakpoints(props = {}, breakpoint = 'desktop') {
return render(
<BreakpointTrackerContext.Provider value={breakpoint}>
<BigBanner {...props} />
</BreakpointTrackerContext.Provider>
);
}

describe('BigBanner', () => {
it('renders with default class on desktop', () => {
const { container } = renderBigBannerWithBreakpoints();

expect(container.firstChild).toHaveClass(styles.defaultColor);
});

it('renders with mobile class on phone', () => {
const { container } = renderBigBannerWithBreakpoints({}, 'phone');

expect(container.firstChild).toHaveClass(styles.phoneColor);
});
});

SASS helpers (mixins)

There are two SCSS mixins in @jutro/theme which are helpful in responsive development:

  • gw-breakpoint-down
  • gw-breakpoint-up
@import "~@jutro/theme/assets/sass/helpers";

.myCustomComponentWithHelpers {
@include gw-breakpoint-up(tablet) {
/* your styles here */
}

@include gw-breakpoint-down(phone) {
/* your styles here */
}
}