DEV Community

loading...
Cover image for Building a UI from scratch, Responsive Content

Building a UI from scratch, Responsive Content

Germán Llorente
Proactive and passionate dev. For some time now, I'm working exclusively with JavaScript, specifically Express, React, GraphQL and Apollo.
・8 min read

This is the third article of the Building a UI from scratch series:

Live demo: https://llorentegerman.github.io/react-admin-dashboard/

Repository: https://github.com/llorentegerman/react-admin-dashboard

Responsive Content

As part of the 3rd article, we will see how to make the content that appears in the design (see Part #1).
We want to make it responsive, and we want it to look like this:

As I said in the previous article, we don't have a responsive design to follow, so we will keep it simple, a main breakpoint at 768px.
We can identify 3 main sections in the content:

1- Row of MiniCards
2- Today's trends (graph + stats)
3- Row with 2 cards:
    4- Unresolved tickets
    5- Tasks

MiniCardComponent.js

This is a simple Component, just a Column with a title and value. The content of the column has to be centered.

styles:

container: {
    backgroundColor: '#FFFFFF',
    border: '1px solid #DFE0EB',
    borderRadius: 4,
    cursor: 'pointer',
    height: 70,
    maxWidth: 350,
    marginRight: 30,
    padding: '24px 32px 24px 32px',
    ':hover': {
        borderColor: '#3751FF',
        ':nth-child(n) > span': {
            color: '#3751FF'
        }
    }
},
title: {
    color: '#9FA2B4',
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: 'bold',
    fontSize: 19,
    lineHeight: '24px',
    letterSpacing: '0.4px',
    marginBottom: 12,
    minWidth: 102,
    textAlign: 'center'
},
value: {
    color: '#252733',
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: 'bold',
    fontSize: 40,
    letterSpacing: '1px',
    lineHeight: '50px',
    textAlign: 'center'
}

Pay attention to the container styles, on :hover we want to change the borderColor and fontColor of title and value, but, by default aphrodite sets the styles as !important so, we cannot change the styles of the children (title, value) from their parent (container). To be able to do that we have to import aphrodite in a differnet way than we normally do.

import { StyleSheet, css } from 'aphrodite/no-important';

Now we can overwrite the styles of the children from their parents.

View full file: MiniCardComponent.js

TodayTrendsComponent.js

This component is a Row with the following structure:

1- Column
    3- Row ({ horizontal: space-between })
        5- Column with title and subtitle
        6- legend
    4- Chart
2- Column: list of stats

It can be defined as follows:

where renderLegend and renderStat are defined as follows:

for the chart I have used react-svg-line-chart, to install it, just type:

yarn add react-svg-line-chart

and these are the styles:

container: {
    backgroundColor: '#FFFFFF',
    border: '1px solid #DFE0EB',
    borderRadius: 4,
    cursor: 'pointer'
},
graphContainer: {
    marginTop: 24,
    marginLeft: 0,
    marginRight: 0,
    width: '100%'
},
graphSection: {
    padding: 24
},
graphSubtitle: {
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: 'normal',
    fontSize: 12,
    lineHeight: '16px',
    letterSpacing: '0.1px',
    color: '#9FA2B4',
    marginTop: 4,
    marginRight: 8
},
graphTitle: {
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: 'bold',
    fontSize: 19,
    lineHeight: '24px',
    letterSpacing: '0.4px',
    color: '#252733'
},
legendTitle: {
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: '600',
    fontSize: 12,
    lineHeight: '15px',
    letterSpacing: '0.1px',
    color: '#9FA2B4',
    marginLeft: 8
},
separator: {
    backgroundColor: '#DFE0EB',
    width: 1,
    minWidth: 1,
},
statContainer: {
    borderBottom: '1px solid #DFE0EB',
    padding: '24px 32px 24px 32px',
    height: 'calc(114px - 48px)',
    ':last-child': {
        border: 'none'
    }
},
stats: {
    borderTop: '1px solid #DFE0EB',
    width: '100%'
},
statTitle: {
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: '600',
    fontSize: 16,
    lineHeight: '22px',
    letterSpacing: '0.3px',
    textAlign: 'center',
    color: '#9FA2B4',
    whiteSpace: 'nowrap',
    marginBottom: 6
},
statValue: {
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: 'bold',
    fontSize: 24,
    lineHeight: '30px',
    letterSpacing: '0.3px',
    textAlign: 'center',
    color: '#252733'
}

Notice that container will become a column when window.innerWidth <= 1024, so the stats column will be stacked under the graph. At same size the separator will disappear, and graph and stats will fill the whole width.
Pay attention to statContainer style, where we are setting borders for every child except for the last.

View full file: TodayTrendsComponent.js

CardComponent.js

As I said before, the 3rd section of the content page is a Row with 2 component. These components have many similar characteristics, so we can abstract the design like this:

1- Container (column)
    2- Row: 
        3- Column: title and subtitle (received by props)
        4- Link (view details or view all)
    5- List of items (received by props)

Code:

renderItem:

styles:

container: {
        backgroundColor: '#FFFFFF',
        border: '1px solid #DFE0EB',
        borderRadius: 4,
        padding: '24px 32px 12px 32px'
    },
    containerMobile: {
        padding: '12px 16px 6px 16px !important'
    },
    itemContainer: {
        marginLeft: -32,
        marginRight: -32,
        paddingLeft: 32,
        paddingRight: 32,
        paddingBottom: 18,
        paddingTop: 18,
        maxHeight: 22,
        borderBottom: '1px solid #DFE0EB',
        ':last-child': {
            borderBottom: 'none'
        }
    },
    itemContainerMobile: {
        marginLeft: -16,
        marginRight: -16,
        paddingLeft: 16,
        paddingRight: 16
    },
    link: {
        fontFamily: 'Muli',
        fontStyle: 'normal',
        fontWeight: '600',
        fontSize: 14,
        lineHeight: '20px',
        letterSpacing: '0.2px',
        color: '#3751FF',
        textAlign: 'right',
        cursor: 'pointer'
    },
    subtitle: {
        fontFamily: 'Muli',
        fontStyle: 'normal',
        fontWeight: 'normal',
        fontSize: 12,
        lineHeight: '16px',
        letterSpacing: '0.1px',
        color: '#9FA2B4'
    },
    subtitle2: {
        color: '#252733',
        marginLeft: 2
    },
    title: {
        fontFamily: 'Muli',
        fontStyle: 'normal',
        fontWeight: 'bold',
        fontSize: 19,
        lineHeight: '24px',
        letterSpacing: '0.4px',
        color: '#252733'
    }

See in itemContainer that all items will have border except the last one.

View full file: CardComponent.js

UnresolvedTicketsComponent.js

This component will be done based on CardComponent, it will look like this:

where renderStat is:

We need styles only for the title and value, all the others are set in CardComponent:

itemTitle: {
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: '600',
    fontSize: 14,
    lineHeight: '20px',
    letterSpacing: '0.2px',
    color: '#252733'
},
itemValue: {
    color: '#9FA2B4'
}

View full file: UnresolvedTicketsComponent.js

TasksComponent.js

As UnresolvedTicketsComponent, this component will be done based on CardComponent, it will look like this:

As you can see, the first item looks different from the others, it has a gray title and the add button. The rest of the items are stored in the state:

state = { items: [
    {title: 'Finish ticket update', checked: false, tag: TAGS.URGENT },
    {title: 'Create new ticket example', checked: false, tag: TAGS.NEW },
    {title: 'Update ticket report', checked: true, tag: TAGS.DEFAULT }
]};

These are the possible TAGS:

const TAGS = {
    URGENT: { text: 'URGENT', backgroundColor: '#FEC400', color: '#FFFFFF' },
    NEW: { text: 'NEW', backgroundColor: '#29CC97', color: '#FFFFFF' },
    DEFAULT: { text: 'DEFAULT', backgroundColor: '#F0F1F7', color: '#9FA2B4' },
}

and this is the renderTask function that will use other 2 functions: renderTag and renderCheckbox:

For the checkbox we are using two new icons that you can copy from here: checkbox-on and checkbox-off,

These are the styles:

addButton: {
    backgroundColor: '#F0F1F7',
    color: '#9FA2B4',
    fontSize: 20,
    padding: 7
},
itemTitle: {
    color: '#252733',
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: '600',
    fontSize: 14,
    letterSpacing: '0.2px',
    lineHeight: '20px'
},
itemValue: {
    color: '#9FA2B4'
},
greyTitle: {
    color: '#C5C7CD'
},
tagStyles: {
    borderRadius: 5,
    cursor: 'pointer',
    fontFamily: 'Muli',
    fontStyle: 'normal',
    fontWeight: 'bold',
    fontSize: 11,
    letterSpacing: '0.5px',
    lineHeight: '14px',
    padding: '5px 12px 5px 12px'
},
checkboxWrapper: {
    cursor: 'pointer',
    marginRight: 16
}

You can see in the repository code that I added some events to do this component interactive.

View full file: TasksComponent.js

ContentComponent.js

Now we have to combine these components into one. As I said before is a Column with 3 sections:

1- Row of MiniCardComponent
2- TodayTrendsComponent
3- Row with 2 components:
    4- UnresolvedTicketsComponent
    5- TasksComponent

The first section is probably the most complex, because we have to combine some styles. We have 4 cards, and we always want the same number of cards in each row regardless of the width of the screen. That is:

  • 4 cards in a row, or
  • 2 cards in each row, in two different rows, or
  • 1 card in each row, in four different rows

but we don't want something like this:

I think it's a good idea if we group them into pairs in this way:

so, when the main row is wider than the container, it will be divided into two new rows, and so on.

For TodayTrendsComponent is easy, we just need to wrap it in a div to apply some margins.

and the last section is a row with UnresolvedTicketsComponent and TasksComponent that will become a column when window.innerWidth <= 1024,

here is the full code:

styles:

cardsContainer: {
    marginRight: -30,
    marginTop: -30
},
cardRow: {
    marginTop: 30,
    '@media (max-width: 768px)': {
        marginTop: 0
    }
},
miniCardContainer: {
    flexGrow: 1,
    marginRight: 30,
    '@media (max-width: 768px)': {
        marginTop: 30,
        maxWidth: 'none'
    }
},
todayTrends: {
    marginTop: 30
},
lastRow: {
    marginTop: 30
},
unresolvedTickets: {
    marginRight: 30,
    '@media (max-width: 1024px)': {
        marginRight: 0
    }
},
tasks: {
    marginTop: 0,
    '@media (max-width: 1024px)': {
        marginTop: 30,
    }
}

pay attention to the negative margins of cardsContainer as they will absorb the excess margins of the elements that are located on the edges, to avoid this kind of things:

View full file: ContentComponent.js

MainComponent (App.js)

To finish we have to include the ContentComponent in our MainComponent

View the changes: App.js

View full file: App.js

SidebarComponent.js (important fix)

We have to include a change to our Sidebar, because at the moment, a transparent layer is filling all the screen on mobile so we cannot click any element.
We are applying these styles to the mainContainerMobile:

 mainContainerMobile: {
    ...
    width: '100%',
    minWidth: '100vh',
}

but we want those styles only when the Sidebar is expanded, so we will apply these changes to our component:

styles:

...
mainContainerMobile: {
    position: 'absolute',
    top: 0,
    left: 0
},
mainContainerExpanded: {
    width: '100%',
    minWidth: '100vh',
}
...

View the changes: SidebarComponent.js

View full file: SidebarComponent.js

Conclusion

This is the last article in the series, the goal was to show how to build a UI from scratch based on a design and how to turn it into a responsive UI. We have mainly used aphrodite (^ 2.3.1) for the styles and simple-flexbox (^ 2.2.1) to make the layout.

I hope it was useful.

You can star the repo and follow me on GitHub to see other examples that I will add.

Thanks for reading.

Discussion (0)