DEV Community

MartinJ
MartinJ

Posted on • Edited on

6.2 Polishing your Firebase webapp - Responsive / Adaptive Design - a React "cards" layout example.

Last reviewed: Mar 2023

This post is part of a series designed to give IT students a basic introduction to commercial software development practices. It might be a bit wordy for some tastes, and isn't tremendously helpful about matters of detail. But if you're just trying to get your head round some of the broad issues that are described here and want to home in on key coding patterns to handle them, you might find it useful. If not, please feel free to tell me and I'll try to do better!

For a full index to the series see the Waypoints index at ngatesystems.com.

1. Introduction

Everybody wants to be popular - but you have to work at it. Your webapp is no different. Obviously you'll have done your best to make your webapp smart, but now you'll want it to get around a bit and get some publicity.

The "getting around a bit" part is tricky and requires a lot of effort. The problem is that your users will be trying to use your software on many different types of device. As a developer it's likely that your first ideas when designing your layout will be something that looks elegant on a large screen. Unfortunately, this is likely to be completely unusable on, say, an iphone. Bang - you've just lost half of your potential customers. Conversely, if you design purely for a mobile phone with its neat touch-screen scrolling, you'll discourage potential laptop users who may struggle to scroll with a touch-pad.

Several techniques have been proposed as solutions to these and similarly-related problems

Responsive, Adaptive and Adaptive Design - think "RAP"

In Responsive design you are encouraged to take advantage of the browser's ability to capture dimensions and then apply proportional html styling (eg font sizes and table widths) to display-elements.

But the problem here is that, while the resultant layout will always work after a fashion, elements are prone to becoming either ridiculously small or absurdly large. The figure below shows an unreadable multi-column design overflowing the screen of an iphone and a bloated single column display overwhelming the screen of a laptop.

Too large for iphone and too small for laptop

Adaptive design takes a more radical approach and accepts that, while you might still take advantage of responsive design ideas, the layout is just going to have to change between devices.

All (ha ha) that remains is to find a way of making this happen without writing a completely different app for each potential device size.

Progressive design is for a future post. This introduces techniques intended to allow the webapp to tap into features unique to the hardware platform they're running on - see Wikipedia -Progressive web applications.

Back in the mainstream, some good tools are available to provide systematic solutions for Responsive and Adaptive designs - but they come at a price. For example, you might look at Google's Material Design software.

Much of this is actually completely free, but you're going to have to invest quite a lot of your own time ploughing through all those codelabs and documentation. You should certainly not regard your own time as free! More practically at this stage, you might look at something like the (completely free) Bootstrap library. This is a much easier bite to chew, but some people find that it produces some curiously obscure code.

So, as a newcomer I have two initial suggestions for you (there's actually also a third, but we'll come to this in a moment).

My first suggestion is that, at this stage, you forget about using tools like Bootstrap and concentrate your efforts on achieving your responsive/adaptive design through standard Javascript and HTML. This way you'll have full control of what's going on and you will be developing mainstream skills. I'll give you examples of how you might do this in just a moment.

My second suggestion that you don't try to be too adventurous at first - concentrate on using "adaptive" techniques to target just a strictly limited range of device sizes - say an iphone and a medium-range laptop - and then trust that "responsive" proportional styling will ensure that other devices will at least work reasonably well.

Now onward to my third suggestion. When starting a new project, begin by designing for the small screen and touch-screen interface of a mobile phone. Leave that gorgeous full-width laptop design for later. Here's the reason.

So far, we've only tackled half of the "popularity" problem. Maybe your new webapp will work beautifully on every conceivable device, but nobody is going to use it unless they know about it.

Marketing your product is a topic well outside the scope of this post, but one important element of this will certainly be ensuring that references to your webapp pop up in Internet searches. Ensuring that this happens is referred to as SEO - Search Engine Optimisation. The techniques that search engines employ to build their indexes are closely guarded secrets but Google, for example, has at least published an SEO Starter Guide to assist designers. The most important feature of this, given your present circumstances, is that Google has clearly stated that they now give special prominence in their search results to links that perform well on mobile devices. By "special prominence" they mean that they will position references to mobile-friendly sites ahead of the rest in search returns.

How do they know whether or not code works well on mobile devices? Well, if you open the Chrome inspector on your webapp and look closely in the menu bar you will see a tool called "Lighthouse". This is a code analyser that you can instruct to provide comment and advice on the quality of your code. One of the criteria this offers is performance on mobile devices!

The "bots" that crawl the web and build the indexes that drive search-pages obviously do rather more than just look for url references!

Incidentally, Lighthouse is a really powerful tool for examining performance in a wide range of other areas. "Accessibility", for example, will tell you how well your design works for someone with a sight disability. I earnestly recommend you give it a try.

2. A practical example

Many designers have found it useful to build their layouts around a "card" model.

A "card" in this context is just a container for a block of information - a typical example would be a brief description of a product on a shopping site. The example below uses four card to introduce its products.

Typical Shopping site card layout

If the layout of a card is defined in terms of proportions of its width, the browser can be left to work out exactly how card content is actually rendered. All that your webapp's code then needs to decide is how many cards can be comfortably accommodated across the target screen. Note that we're principally determined at all costs to avoid the need for any horizontal scrolling - users strongly dislike this.

Typically on a phone you might render your product display as a single column of product cards (vertical scrolling is rarely an issue). But because cards can so easily be presented as multi-columned arrays it is easy to generalise your design when using this approach to suit a broad range of different device sizes.

Lets start by coding the content of a card. Now that you've boosted your productivity (and your fun levels) by using React to wrap your Javascript, here's a component to render a Card:

function Card(props) {
  return (
    <div>
        <p>{props.card.name}</p>
        <img src={require('./thumbnails/' + props.card.thumbnail)}
          alt={props.card.alt}
          width="90%" />
        <p>{props.card.description}</p>
      </div>
      );
} 
Enter fullscreen mode Exit fullscreen mode

With a bit of polishing (see below), this is going to produce a layout like so:

Product Item card

Pretty much everything I've got to say now is going to revolve around html styling. If you're familiar with this, please read on. If not, I suggest that you skip down to the reference section below for a quick introduction to this hugely powerful technique.

Back in the mainstream, as far as small-scale mobile devices are concerned, the little packet of jsx shown above probably delivers all that you need. With the addition of styling on the container <div> to center its content and to give the whole card a border, together with some arrangement to position it centrally on the page (more on this in a moment) you're good to go here. Note that within the <div>, the <img> element is declared to have a width that's a percentage of the width of its container. Proportional sizing like this is the key to "responsive" design. Percentage sizing and references to units like vh, vw (percentages of screen height and width) and rem (the size of a character in the applications root or body element) will ensure that your design will always do its level best to adjust itself to the device on which it runs. The result will not necessarily be pretty, but it will at least be usable. And it will certainly be fine on a mobile phone.

So, that's the "mobile first" bit completed. Now we can think about how we might apply some "adaptable" design techniques to make things work better on larger screens. Specifically I'm looking for a way to replace the default single column with an appropriately-sized multi-column layout.

The first thing I'm going to need is some way of determining the screen size. Once I've got that I can start to think about using this to guide the layout.

Conventionally, these two steps would be delivered through a special styling mechanism called "media queries". Here's an example:

@media screen and (min-width: 40rem) {
    .card {
        width: 50%;
    }
}
Enter fullscreen mode Exit fullscreen mode

This particular media query looks at your device and, if it is at least 40rem wide, configures the card styling class to bestow on whatever element invokes the card class a width property of 50% of that element's container's width. I think you can see that this would be the first step towards getting a two-column layout.

If you now added a second query to your stylesheet as follows:

@media screen and (min-width: 60rem) {
    .card {
        width: 25%;
    }
}
Enter fullscreen mode Exit fullscreen mode

on devices with a width of at least 60rem, this would over-ride the previous media query rule and set your webapp to deliver a four-column layout. And so on. Incidentally - another piece of jargon here - the values aligning device characteristics with styling in these rules are referred to as breakpoints.

Usually, the card style generated by the media query would then be applied to a <div> styled with a display property set to flex. Flex is a wonderful mechanism for distributing the space between elements within a container. So, for example, a container div styled as display: flex; and justify-content: space-around; would lay out four child elements, each possessing a width of, say, 20% of the screen width, spaced neatly across the screen as shown below (illustration courtesy of Flex Cheatsheet.

spaced flex layout

So far so good. If, for example, the application knows that the display is to laid out as a grid of "n" columns, it can group up a long list of cards into groups of "n" and render these one row at a time.

The snag with media queries - which are the place where "n" is effectively being defined in this context - is that you don't know which query is being applied and so you don't have direct access to "n".

This problem would normally be solved via a second feature of flex styling, namely its flexWrap: "wrap" property. This allows you to tell flex to accommodate a row of children that's too long to fit onto a single line by arranging for them to overflow onto trailer lines.

However, while I've happily used this technique in the past, I'm not going to recommend it here. My reasons are as follows:

  • Wrap works well until flex comes to the last row and has to work out what it will do if this is "incomplete". Consider what will happen if flex is trying to render a list of 11 cards in an an array that is 4 cards wide. What is it do with the 3 cards left in the last row? Centre them? Left adjust them? Clearly, directing flex to do exactly what you want in this situation is going to be a challenge Although I love flex and use it all the time for managing single row displays, I find the complex wrap syntax required to format an overflow array more than my over-burdened brain can cope with. For me at least, "wrap" doesn't produces the "readable" code I need when I'm dealing with maintenance and enhancement issues long after the initial intricate design was so carefully crafted.

     

  • I'm not happy with the media query approach either - partly again because the syntax looks so awkward, but partly also because it separates styling specifications from the point at which they are actually used. Now that we're using JSX and applying styling specs as objects, I think it is much better if all "adaptive" instructions appears explicitly inside the Components to which they apply. I'll show you what I mean in just a moment.

But if I'm not going to be using media queries, all this depends on me being able to find a way of determining screen size and specifying breakpoints under my own steam. Fortunately this is a lot easier than you might imagine.

Consider the following block of code. It is designed to deliver a typical adaptive design for a cards display - single column for phones, two-column for tablets and four-column for laptops:

import ReactDOM from "react-dom/client";

// Get the pixel widths of both an individual character and the display body
const rootCssObj = window.getComputedStyle(document.getElementById("root"), null);
const characterWidthPixels = parseInt(rootCssObj.getPropertyValue("font-size"), 10);// parseInt strips off the "px"
const displayWidthPixels = parseInt(rootCssObj.getPropertyValue("width"), 10);// ditto
// Use these two values to get the width of the display in characters
const displayWidthCharacters = displayWidthPixels / characterWidthPixels;

//Determine an appropriate number of columns for the card display

let optimalCardColumnCountForThisDisplay = 2; // tablet
if (displayWidthCharacters <= 25) optimalCardColumnCountForThisDisplay = 1; // phone
if (displayWidthCharacters >= 75) optimalCardColumnCountForThisDisplay = 4; //laptop

const columnity = optimalCardColumnCountForThisDisplay; // too much of a mouth-full for subsequent heavy use!!

function ProductsTable() {

  // Simulated database read to obtain array of product objects

  const PRODUCTS = [];

  for (let i = 0; i < 11; i++) {
    PRODUCTS.push({
      number: i,
      name: "Product " + i,
      thumbnail: "standard-product-graphic.jpg",
      alt: "Product " + i + " graphic",
      description: "Description for product " + i
    })
  };

  // Back in the real world now, pad out the end of the PRODUCTS array with
  // empty objects to ensure that each card display row will be full

  if ((PRODUCTS.length % columnity) > 0) {
    for (let i = 1; i <= (PRODUCTS.length % columnity); i++) {
      PRODUCTS.push({
        number: PRODUCTS.length,
        name: ""
      });
    }
  }

  // Create a new array of card rows. Each cardRow property in cardRows
  // will itself be an array containing columnity Card objects

  let cardRows = [];
  for (let i = 0; i < PRODUCTS.length; i += columnity) {
    cardRows.push({
      cardRowIndex: i,
      cardRowElements: PRODUCTS.slice(i, i + columnity)
    })
  }

  return (
    <div>
      {cardRows.map((cardRow) => (
        <CardRow key={cardRow.cardRowIndex} columnity={columnity} cardRow={cardRow.cardRowElements} />
      ))}
    </div >
  );
}

function CardRow(props) {

  let cardRow = props.cardRow;

  return (
    <div style={{ display: "flex", justifyContent: "space-around" }}>
      {cardRow.map((card) => (
        <Card key={card.number} columnity={columnity} card={card} />
      ))}

    </div>
  )
}

function Card(props) {

  const cardWidth = displayWidthPixels / columnity - (2 * characterWidthPixels) + "px";

  const emptyCardStyle = {
    width: cardWidth
  };

  const regularCardStyle = {
    width: cardWidth,
    textAlign: "center",
    border: "1px solid black",
    marginBottom: "2rem"
  };

  if (props.card.name === "") {
    return (<div style={emptyCardStyle}></div>)
  } else {
    return (
      <div style={regularCardStyle}>
        <p>{props.card.name}</p>
        <img src={require('./thumbnails/' + props.card.thumbnail)}
          alt={props.card.alt}
          width="90%" />
        <p>{props.card.description}</p>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(
  document.getElementById("root")
);

root.render(
  <ProductsTable />
);
Enter fullscreen mode Exit fullscreen mode

This code is designed to live in the index.js file of a React application. Its purpose is to render an appropriately configured display of Product cards. The Card Component that renders an individual product card is actually nested inside a CardRow component inside a ProductsTable component. To keep things compact I've coded all of these as functional components within the index.js file itself. Note also that there's no style sheet - all of the styling is applied inline.

The code starts by determining its working environment - the pixel width of a character and the pixel width of the device page. This then enables the code to work out the number of characters that will fit across the width of that device.

Breakpoints specified in terms of line capacity are then used to assign an appropriate value to a "columnity" variable - the number of cards in a card array row. This will be used to drive all the subsequent logic. [Note, with regard to my use of the non-word "columnity" as a variable name, I spent an unprofitable evening considering other appropriately meaningful shorthand references to the number of columns in a display before coming across a learned discussion on Stack Exchange. This proposes the word "columnity" as an example of the "Turkish" approach to naming. Apparently, the Turks at one point in their history decided that their national language had become too complicated and simply sat down sat down and redesigned it, creating new words as necessary. I like this idea a lot!]

At first sight, the Javascript for the columnity-calculation code may appear rather intimidating, but it's actually quite simple once you break it down.

  • document.getElementById is just a Javascript function to get a pointer into the DOM for the element in the project's index.html qualified by an id with the value "root".

     

  • window.getComputedStyle gets all the formatting for this as an object

     

  • getPropertyValue is a method for this object that enables us to dig out individual properties - in this case I use it to get the font size and the width of the root element

     

  • finally, since these value come back as strings with value like '16px', parseInt(string, 10) is just a neat way of converting these strings to integers (the '10' bit says 'please treat this string as number with radix 10). Don't you just love Javascript?

Note that the characterWidthPixels field is essentially the value of 1 rem (or "root em") for the display - the size of 1 em or character displayed in the root element.

Armed now with clear instructions about what it's to do, ie "lay these cards out as a grid with columnity columns" - the ProductsTable component now swings into action. In this demo instance, it start off by creating itself a block test data - in a real application you should imagine this being replaced by a Firestore getDocs call.

With an eye to future problems rendering the last row of the array, the next job is to tack on enough dummy cards to ensure the grid will be full.

Finally, the code marshalls the initial array of cards into a new array of card rows (each containing columnity entries) and proceeds to render these as CardRow components. [I realise that this approach isn't tremendously efficient, but it "reads" well and, given the circumstances, I'm happy enough to use it. If anyone has a more efficient suggestion I'd love to hear it].

The CardRow component does nothing more than render the columnity entries from a card row into a <div container styled as { display: "flex", justifyContent: "space-around" }. [Note the switch to JSX object-style formatting for the styling specification]

So the action now finally reaches the Card component and applies the detailed responsive and adaptive formatting of the Card itself.

First of all, and most importantly, it calculates an appropriate width for the card within the columnity-up display. A couple of rems are subtracted to ensure that cards will have some blank space around them, (leaving flex to actually do the spacing) and the result is formatted as a string (eg "240px").

Matters are slightly complicated by the need to treat dummy cards differently from regular cards (for example they mustn't have a border), but I feel that the code remains perfectly readable. The logic isn't squirreled away in media queries inside a stylesheet file.

And basically, that's it. This is all you need to popularise your webapp by making it device-agnostic. Google will be proud of you and bump up your SEO!

If you want to give this a try, just configure yourself a new React project and copy the Index.js code above over the index.js of the React demo.

Here's what the output should look like on a laptop (once you've added a suitable card graphic) to your project:

11-card-layout picture

Here are a few afterthoughts:

  • The "dynamic styling" ideas introduced above can be deployed in all sorts of imaginative ways. For example, it might be a good idea if the product description respond to the type of layout in which it appears. While a long description can just be allowed to scroll on a phone, where scrolling is such a natural action, you might prefer to see the display length constrained on other types of devices and overflow handled by means of a scrollbar.

     

    To set this up you would just introduce a new style object for the <p>{props.card.description}</p> block and define this at the top of the component.

     

const descriptionStyle = {};
if (columnity !== 1) {
descriptionStyle.overflow = "auto";
}
Enter fullscreen mode Exit fullscreen mode

The columnity variable can be used to guide any number of variations on this theme.

  • In more extreme situations, where the "cards" approach simply runs out of steam, you might just use the breakpoint calculations to set up a "deviceType" variable and use this to drive conditional rendering in components, viz:
function awkwardComponent (props) {
switch(deviceType) {
  case "mobile":
    return (..JSX for mobile...)
    break;
  case "tablet":
  return (...JSX for tablet..)
  break;
  ... and so on .
}
Enter fullscreen mode Exit fullscreen mode

Hopefully you won't have to consider something like this too often!

  • When you're working on a project like this where so much of the logic is concerned with styling, it's really important that you get familiar with the browser's system inspection tool (see Google devtools for an overview of the Chrome inspector). I hope you've already had plenty of practice using this to set breakpoints on your code and monitor the execution of its logic. But the inspection tool is equally capable when it comes to investigating problems with your screen layout.

     

    With the inspector open, positioned and sized appropriately on the screen (you can tell the tool whether you want it to display itself at the side or at the bottom of the display and also size its various sub-windows by clicking and dragging on their borders), you can click on any element in your design and get detailed information about how the browser has decided to render it.

     

    But additionally, and crucially in this context, where we're trying to sort out designs for different device sizes, the inspector also allows you to choose from pre-programmed device setting and observe a simulation of how your design would render on that specific device.

     

    So if you were wondering how you might test your new layout on a "Samsung Galaxy S20 ultra", for example, no, you don't actually need to buy one.

     

  • While all of the above references the appearance of the screen, responsive techniques are also commonly used to ensure the efficiency of a webapp. When a responsive screen is servicing <img> elements, media queries are often used to choose appropriately-sized graphic files. See Post 6.4 for further details.

3. Reference : html styling

The appearance of an html element such as <p> is determined by its properties. A simple example would be its color property. These properties can be set in a number of different ways, but one direct method is to use the style keyword in the element's definition. For example, the following html code::

<p style="color: red;">Hello</p>
Enter fullscreen mode Exit fullscreen mode

would display the word "Hello" in red. This type of styling is referred to as "inline styling".

Because it's obviously going to be highly inconvenient to specify all the styles that you might want to apply to a particular type of element like this every time that it's used, the html standard has introduced the idea of "cascading" styles, or CSS for short. There are two main themes to this.

The first is "inheritance". An html script defines natural hierarchies of elements - <p> elements, for example, will generally be found as children inside a <div> parent. Styles defined for a parent are automatically inherited by its children.

The second theme is delivered through "style sheets". These are free-standing files that define both styles and their inheritance arrangements.

If, for example, we wanted all <p> elements to be colored red, we could create a stylesheet containing the following entry:

p {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

If we wanted to be a bit more selective, however, we could define a classname

.myspecialclass {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

This could then be attached to selected elements in your html by referencing the classname in their markup as follows:

<p class="myspecialclass">Hello</p>
Enter fullscreen mode Exit fullscreen mode

In a React application you would apply your stylesheet file by importing it into each of your components.

There's obviously lots more to styling than this, but that's probably all you need to know to get you started. For more detailed advice and a "sandbox" in which to try things out, you might find it useful to look at W3Schools CSS

One final point I need to mention though is that, when you're working with React, inline styles are defined with a subtly-altered pattern using object notation. See W3Schools React Inline Styling for details.

Top comments (0)