WebDev4R: Creating a weather app

WebDev
We create what looks like a weather app with {htmltools}
Author

Albert Rapp

Published

February 18, 2024

In this blog post, I’m using the {htmltools} package to build what looks like a weather app. This will combine a lot of tools we will learn in the WebDev4R series. If you’re reading this, before the series has reached the lesson on {htmltools} and flexboxes, you can think of this as a teaser of what’s to come. So let’s take a look at what we’re going to build.

11:38
Milwaukee, WI
22° Rain 22°/14°
Today
22°/14°
Tomorrow
23°/15°
Monday
23°/15°

We’re going to build this from the ground up. And if you’re a fan of video content, then you can also watch the video version of this blog post on my YouTube channel:

The structure of the app

Let’s start out by dishing out a bunch of div containers that will hold our content. What we will need are designated areas for the

  • rectangular phone shape that contains everything,
  • top bar with the time and status icons,
  • location and location icon,
  • temperature and forecast,
  • forecast icon and
  • container of the next 3 days’ forecast.
div(
  div(
    id = 'top-bar'
  ),
  div(
    id = 'location-container'
  ),
  div(
    id = 'temp-container'
  ),
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = '#333333',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) 

This is the basic structure of our app. Notice that I have passed the phone container to another div() that adds a line break and sets all styles to initial. This is to ensure that the styles that I set in my Quarto blog interfere as little as possible with the overall look.

Also, the line break is a clumsy way to ensure that there is some space below the rectangle (otherwise the text looks weird). Due to the way Quarto nests the output into containers, I can’t just add a margin to the phone container.

Set up phone container

Now that we have that squared away, let us set up the phone container. Here, we can ensure that we

  • use a (subtle) gradient background,
  • add a border,
  • make the border rounded,
  • ensure that the content has some padding away from the borders and
  • set font family and color
div(
  div(
    id = 'top-bar'
  ),
  div(
    id = 'location-container'
  ),
  div(
    id = 'temp-container'
  ),
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) 

Set up top bar

Sweet. Now let’s move on to the top bar. Inside of there we want to display the time and some status icons. We can use the shiny::icon() function to add some icons. All this requires is a name of a suitable Font Awesome icon.

But the more important thing is the layout. The time is all the way to the left (minding the padding) and the icons are all the way to the right. This just screams flexbox with justify-content: space-between.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
      ),
      shiny::icon(
        'wifi',
      ),
      shiny::icon(
        'battery-three-quarters',
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between'
    ),
    id = 'top-bar'
  ),
  div(
    id = 'location-container'
  ),
  div(
    id = 'temp-container'
  ),
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38

Nice, this worked pretty smoothly. And due to the fact that icons are inline elements, they are automatically aligned horizontally so that we don’t have to apply any further styling to the container that holds them. But we can modify their spacing and font style a bit.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'  ### <- Changes here
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'  ### <- Changes here
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'  ### <- Changes here
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',          ### <- Changes here
      font_weight = 600,           ### <- Changes here
    ),
    id = 'top-bar'
  ),
  div(
    id = 'location-container',
  ),
  div(
    id = 'temp-container'
  ),
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38

Set up location container

Excellent. That’s a beautiful top bar right there! Now, let’s move on to the location container. This will hold the name of the location and a location icon. It’s a pretty straight forward div container with

  • a text,
  • an icon,
  • and a couple of text styles.
div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
    ),
    id = 'top-bar'
  ),
  div(                          ### <- Changes here
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600,
    ),
    id = 'location-container',
  ),
  
  
  div(
    id = 'temp-container'
  ),
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI

But that looks a bit squished. Let’s give the label some space by adding a bottom margin to the top bar.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'   ### <- Changes here
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  
  
  div(
    id = 'temp-container'
  ),
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI

Set up temp container

The temperature container is a bit more complicated. Here, we have to mind that we use seperate font sizes and weights on the same line. Hence, we’ll have to nest a couple of span elements inside of the div container and style them accordingly.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(                            ### <- Changes here
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px'
      )
    ),                             
    id = 'temp-container'
  ),
  
  
  
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°

Using different font weights and font sizes worked nicely. But the texts are pretty close together. Hence, let us also add margins to the spans to space them out a bit.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'   ### <- Changes here
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'   ### <- Changes here
      )
    ),
    id = 'temp-container'
  ),
  
  
  div(
    id = 'forecast-icon'
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°

Ahh much better. Now let’s do the forecast icon.

Set up forecast icon

So far our code contains a container for the forecast icon. But that’s not actually necessary. What we can do is to add the icon directly to the phone container. I’ve just put that div container in there for structuring out my design. Just throw that icon in there just like before and ramp up the font-size.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(               ### <- Changes here
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px'
    )
  ),
  
  
  
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°

Notice that I’ve also added a margin to the top of the icon to space it out a bit from the temperature and forecast text.

Center forecast icon

Now we only have to figure out how to solve the age old struggle of centering content. Since this is only an icon that I want to center, we could make sure that the icon is displayed as a block and center it with text-align: center;.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px',
      display = 'block',       ### <- Changes here
      text_align = 'center'    ### <- Changes here
    )
  ),
  
  
  
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°

But I want to show you a more general trick. You see, when you have a container that you want to center, you can use the margin-left: auto; and margin-right: auto; trick. But that works only if the surrounding container (in this case the phone-container) is a flexbox.

But if we were to set the phone-container to display: flex;, then all elements would be displayed in a row. And that’s where the flex-direction property comes in. Set it to column and the elements will be displayed in a column and everything will look as before with the added bonus that the icon’s surrounded container is a flexbox. See:

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px', 
                               ### <- Changes here (removed display and text-align)
    )
  ),
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
    display = 'flex',          ### <- Changes here
    flex_direction = 'column', ### <- Changes here
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°

And now we can use the auto-margins trick.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px', 
      margin_right = 'auto',    ### <- Changes here
      margin_left = 'auto'      ### <- Changes here
    )
  ),
  
  
  div(
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
    display = 'flex', 
    flex_direction = 'column'
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°

Set up next 3 days container

Finally, we have to set up the container for the next 3 days forecast. This container needs to contain three similar rows and a row of circles. Let’s start with styling the container a bit and then we add the row of circles. This will leave the three other rows for later.

Style the surrounding container

We start out by giving the surrounding a couple of styles. By now, all of these styles shall be fairly familiar to you. And to make the container actually visibile, I have filled it with some generic text. This part will be replaced later with the rows.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px', 
      margin_right = 'auto',   
      margin_left = 'auto'      
    )
  ),
  div(                          ### <- Changes here
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc tincidunt tincidunt',
    style = htmltools::css(      
      background = 'linear-gradient(45deg, #7dc8ff, #7dcafe)',
      border_radius = '10px',
      margin_top = '50px',
      font_family = 'Source Sans Pro',
      color = 'white',
      margin_left = '30px',
      margin_right = '30px',
    ),
    id = 'next-3-days-container'
  ),
  
  
  
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
    display = 'flex', 
    flex_direction = 'column'
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc tincidunt tincidunt

Add the row of circles

Ignoring the Lorem Ipsum for a sec, we can add the row of circles below it really easily. Just throw in another div container that has three circle icons inside of them. We will use shiny::icon() just like before but on one of the circles we change the class to make it solid.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px', 
      margin_right = 'auto',   
      margin_left = 'auto'      
    )
  ),
  div(                         
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc tincidunt tincidunt',
    div(                      ### <- Changes here
      shiny::icon('circle', class = 'fa-solid'),
      shiny::icon('circle'),
      shiny::icon('circle'),
      style = htmltools::css(
        margin_bottom = '5px',
        font_size = '10px'
      )
    ),
    
    
    
    style = htmltools::css(      
      background = 'linear-gradient(45deg, #7dc8ff, #7dcafe)',
      border_radius = '10px',
      margin_top = '50px',
      font_family = 'Source Sans Pro',
      color = 'white',
      margin_left = '30px',
      margin_right = '30px',
    ),
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
    display = 'flex', 
    flex_direction = 'column'
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc tincidunt tincidunt

And now we can apply our auto margins trick from before to center the circles. But remember, the surrounding container needs to be a flexbox.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px', 
      margin_right = 'auto',   
      margin_left = 'auto'      
    )
  ),
  div(                         
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc tincidunt tincidunt',
    div(                      
      shiny::icon('circle', class = 'fa-solid'),
      shiny::icon('circle'),
      shiny::icon('circle'),
      style = htmltools::css(
        margin_bottom = '5px',
        font_size = '10px',
        margin_right = 'auto',   ### <- Changes here
        margin_left = 'auto'     ### <- Changes here
      )
    ),
    style = htmltools::css(      
      background = 'linear-gradient(45deg, #7dc8ff, #7dcafe)',
      border_radius = '10px',
      margin_top = '50px',
      font_family = 'Source Sans Pro',
      color = 'white',
      margin_left = '30px',
      margin_right = '30px',
      display = 'flex',         ### <- Changes here
      flex_direction = 'column' ### <- Changes here
    ),
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
    display = 'flex', 
    flex_direction = 'column'
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc tincidunt tincidunt

Set up the rows

Finally, all that’s left to do is to get the three rows in there. Since we have to throw in three similar rows, let us actually write a function that returns a row.

one_day_row <- function(icon = 'cloud-rain', day = 'Today', temps = '22°/14°') {
  div(
      div(
        shiny::icon(
          icon,
          style = 'margin-right: 10px; vertical-align: middle'
        ),
        span(
          day,
          style = 'margin-right: 25px'
        ),
      ),
      div(
        temps,
      ),
      style = htmltools::css(
        font_size = '20px',
        font_weight = 600,
        margin_top = '5px',
        display = 'flex',
        justify_content = 'space-between'
      )
    )
}

This function returns a row with an icon, a day and the temperatures. And we have really used mostly the same techniques as before. The only thing that might be new is justify_content = 'space-between'. This one stretches out elements in a container so that they are evenly distributed.

And with this function set up, we only need to call it repeatedly inside of the next-3-days-container and we are done.

div(
  div(
    '11:38',
    div(
      shiny::icon(
        'signal',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'wifi',
        style = 'margin-left: 5px'
      ),
      shiny::icon(
        'battery-three-quarters',
        style = 'margin-left: 5px'
      )
    ),
    style = htmltools::css(
      display = 'flex',
      justify_content = 'space-between',
      font_size = '20px',
      font_weight = 600,
      margin_bottom = '30px'
    ),
    id = 'top-bar'
  ),
  div(
    'Milwaukee, WI',
    shiny::icon(
      'location-dot',
      style = 'margin-left: 10px; font-size:32px'
    ),
    style = htmltools::css(
      font_size = '42px',
      font_weight = 600
    ),
    id = 'location-container',
  ),
  div(
    span(
      '22°',
      style = htmltools::css(
        font_size = '100px',
        font_weight = 600,
      )
    ),
    span(
      'Rain',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    span(
      '22°/14°',
      style = htmltools::css(
        font_size = '25px',
        margin_left = '10px'
      )
    ),
    id = 'temp-container'
  ),
   shiny::icon(
    'cloud-rain',
    style = htmltools::css(
      font_size = '150px',
      margin_top = '35px', 
      margin_right = 'auto',   
      margin_left = 'auto'      
    )
  ),
  div(                         
    div(                      ### <- Changes here
      one_day_row(),
      one_day_row('sun', 'Tomorrow', '23°/15°'),
      one_day_row('sun', 'Monday', '23°/15°'),
      style = 'margin: 20px'
    ),
    div(                      
      shiny::icon('circle', class = 'fa-solid'),
      shiny::icon('circle'),
      shiny::icon('circle'),
      style = htmltools::css(
        margin_bottom = '5px',
        font_size = '10px',
        margin_right = 'auto',  
        margin_left = 'auto'  
      )
    ),
    style = htmltools::css(      
      background = 'linear-gradient(45deg, #7dc8ff, #7dcafe)',
      border_radius = '10px',
      margin_top = '50px',
      font_family = 'Source Sans Pro',
      color = 'white',
      margin_left = '30px',
      margin_right = '30px',
      display = 'flex',         ### <- Changes here
      flex_direction = 'column' ### <- Changes here
    ),
    id = 'next-3-days-container'
  ),
  style = htmltools::css(
    width = '375px',
    height = '650px',
    background = 'linear-gradient(45deg, #46afff, #47c8ff)',
    border = '3px solid #333333',
    border_radius = '20px',
    padding = '15px 25px 15px 25px',
    font_family = 'Source Sans Pro',
    color = 'white',
    display = 'flex', 
    flex_direction = 'column'
  ),
  id = 'phone-container'
) |> 
  div(
    br(),
    style = 'all: initial;'
  ) |> browsable()
11:38
Milwaukee, WI
22° Rain 22°/14°
Today
22°/14°
Tomorrow
23°/15°
Monday
23°/15°

Conclusion

Yaaaay, we made it. That were quite a lot of moving parts but it’s definitely a good practice to build something like this. If you found this helpful, here are some other ways I can help you:


Stay in touch

If you enjoyed this post, then you may also like my weekly 3-minute newsletter. Every week, I share insights on data visualization, statistics and Shiny web app development. Reading time: 3 minutes or less. Subscribe at

You can also support my work with a coffee