DEV Community

Graham Trott
Graham Trott

Posted on • Edited on

Web Apps: Code Once, Run Anywhere

This article is about how to build a Responsive website using JavaScript.

If you Google "responsive design" you'll find a lot about Media Queries, which seem to be the preferred way to create a product that will run on any screen from a smartphone to a 4k monitor. However, I feel in many cases this is a clunky, over-elaborate way of avoiding the real issue, which is one of mathematics. Let me explain.

A Web App is a single web page that looks like a regular application. The code delivered to the browser is generated either on the server or in the browser itself, but either way it's going to contain a lot of JavaScript to provide the user experience. The additional code needed to handle responsive design is quite modest, so let's see how it works.

Take a look at the web app below, which always fills the browser client area vertically without any scrollbars being needed (scrolling is done inside the content panel). It tries to maintain a specific aspect ratio and has an optional set of background images to provide a frame for the content. The live app can be seen here. Please note: The live app may not look exactly like this image as it's still being developed. The current version is always held in the Storyteller repository; the main site source script is ecs/storyteller.txt.

Storyteller

We start by creating a theme descriptor; a JSON file called theme.json, that defines a few things:

{
    "aspect-w":4,
    "aspect-h":3,
    "border":10,
    "font-scale":35
}
Enter fullscreen mode Exit fullscreen mode

aspect-w and aspect-h are the width and height components of the desired aspect ratio, as described earlier. You might use 4:3 (as above), 16:9 or something different.

border is the width of the left (or right) border in the frame graphic as a percentage of the image width. Trial and error may be needed to find the optimum value.

font-scale is a value that defines the number of lines of text that will fit into the height available for content. Its value is usually between 30 and 40, where the former gives larger text than the latter, and it can really only be chosen by experimentation as it causes different effects on different browsers.

The graphic frame you see is also part of the theme pack. It comprises images for the middle, top and bottom of the frame, which use absolute positioning so they sit on top of each other correctly. When the screen resizes the top and bottom stay anchored while the middle adjusts itself to fill the height of the window.

In your JavaScript code, set up a handler for the window.resize event:

window.addEventListener('resize', function() {      
    // Respond to the event by calling SetStyles below
});
Enter fullscreen mode Exit fullscreen mode

Now for the calculations. I'll present this in what looks like pseudo-code but is actually a subroutine that's part of the production code for the app shown above. The script for the entire website is less than 500 lines long, including white space and comments but excluding the user content (which is taken from Markdown files), and the language used is EasyCoder, a browser-based Javascript engine that compiles scripts on the fly then runs them.

Variables, some of whose purpose is obvious, such as to handle buttons on the screen, all have initial Capital Letters; everything else is part of the syntax of the language. I've added extra comments to explain the purpose of each variable and the logic of the calculations. Note in particular the way the language directly sets the CSS styles of DOM elements; there's no HTML and no virtual DOM. cat, short for catenate, appends one string to another.

!  Responsive design: Compute the size and position of all the screen elements

!  AspectW is the 'aspect-w' value from the theme descriptor
!  AspectH is the 'aspect-h' value from the theme descriptor
!  Border is the 'border' value from the theme descriptor
!  FontScale is the 'font-scale' value from the theme descriptor
!  Mobile is a flag that's true when the system has detected
!    we're using a smartphone in portrait mode

SetStyles:
!  Get the browser client dimensions
   put the width of window into WindowWidth
   put the height of window into WindowHeight

!  Choose an optimum width based on the window height and the requested aspect ratio
   put WindowHeight into Height
   multiply Height by AspectW giving Width
   divide Width by AspectH

!  Make sure the window is wide enough
   take Width from WindowWidth giving Margin
   divide Margin by 2
   if Margin is less than 0
   begin
      put 0 into Margin
      put WindowWidth into Width
   end

!  Style the Container (a DIV)
   set style `left` of Container to Margin cat `px`
   set style `top` of Container to 0
   set style `width` of Container to Width cat `px`
   set style `height` of Container to Height cat `px`

!  Style the background images (IMG elements).
!  There's a small fiddle in here to maintain a border at the bottom of the window
   set the style of MidImage to
      `position:absolute;left:0;top:0;width:` cat Width
      cat `px;height:` cat `calc(` cat Height cat `px - 2vh)`
   set the style of TopImage to
      `position:absolute;left:0;top:0;width:` cat Width cat `px`
   set the style of BottomImage to 
      `position:absolute;left0;bottom:2vh;width:` cat Width cat `px`

!  Get the width of the Content panel (a child of Container). Border is a percentage.
   multiply Width by Border giving Left
   divide Left by 100
   take Left from Width
   take Left from Width

!  Style the buttons at the top of the screen
   divide Width by 20 giving ButtonSize
!  If we are on a mobile device we need to make the buttons bigger
   if Mobile multiply ButtonSize by 2
   set style `position` of HomeButton to `absolute`
   set style `left` of HomeButton to 0
   set style `top` of HomeButton to 0
   set style `width` of HomeButton to ButtonSize cat `px`
   set style `height` of HomeButton to ButtonSize cat `px`

   multiply ButtonSize by 2 giving N
   set style `position` of BackButton to `absolute`
   set style `left` of BackButton to N
   set style `top` of BackButton to 0
   set style `width` of BackButton to ButtonSize cat `px`
   set style `height` of BackButton to ButtonSize cat `px`

   set style `position` of InfoButton to `absolute`
   set style `right` of InfoButton to 0
   set style `top` of InfoButton to 0
   set style `width` of InfoButton to ButtonSize cat `px`
   set style `height` of InfoButton to ButtonSize cat `px`

!  Compute the top and height of the content panel
   multiply Border by AspectW giving Top
   divide Top by AspectH
   take Top from Height giving Height
   take Top from Height

!  Style the content panel
   set the style of Content to
      `position:absolute;left:` cat Left cat `px;top:` cat Top cat `px;`
      cat `width:` cat Width cat `px;height:` cat Height cat `px`

!  Style the fonts at the top level (so the settings cascade)   
   divide Height by FontScale giving FontSize
   set style `font-size` of Container to FontSize cat `px`
   return
Enter fullscreen mode Exit fullscreen mode

The algorithm starts with the viewport height, from which it calculates the width corresponding to the desired aspect ratio. This is then constrained to be no greater than the viewport width, ensuring it will fit on a smartphone screen.

The 3 frame images are now sized to fit the computed width. The 'middle' image is also sized to fit the viewport height. Note the need for a small fiddle, without which the bottom image is slightly cropped. No, I don't yet know why.

Next, the Home, Back and Info buttons are sized.

Next, the content panel is sized, using the border and aspect values given in the theme descriptor. Before calling this subroutine the script has already given the panel a margin:0 auto style to make it sit centrally in the window.

Finally, the font size is calculated, as a proportion of the viewport height.

When the window is resized the content adjusts itself instantly and smoothly to fit the new space available. This is in contrast to the way media queries usually cause the page to 'jump' at each transition from one size to the next.

The code here can easily be converted into a JavaScript function and integrated into any web page; you just need to ensure all the elements that need sizing are handled appropriately. I've used absolute positioning because of the need to keep to the content height of the browser panel, and also because it's easy to position components from an edge. Web designs that freely extend below the fold can probably be allowed to flow naturally.

Top comments (0)