Prototyping Responsive Web Layout using Framer

Jiesi Huang
Prototypr
Published in
11 min readJul 24, 2018

--

My requirements for a responsive web prototype

A prototype is used to find and fix problems, as well as a communication tool to show others the exact behaviour that you are designing. Having a sample that works the way you want it to is immensely more easily understood than trying to explain with words.

Nowadays, responsive design is very important for websites, to cater to the myriad of screen sizes out there. Many websites have different layouts for standard screens, widescreens, tablets and phones. With the many pages of a website, trying to explain responsive behaviour to stakeholders can quickly become tedious and confusing. Therefore, these are my requirements for a responsive web prototype:

  1. The ability to resize the window (like on a web browser)
  2. Allows the layout to be changed based on screen width/height
  3. Able to be shared with multiple people, and these people can access it any time they need to (and hopefully it’s free to access — this is especially useful for developers).
  4. Able to be built fast, with the need to tweak as little elements as possible.

Selecting the tool for prototyping responsive web

UX is a growing field, and UX design tools are getting increasingly sophisticated, evolving with the rigorous needs of the industry. Even so, many tools have their limitations.

Non-coding design tools

Many non-coding tools such as Sketch or Figma boast responsive capabilities, but in truth, these are limited in function. They allow design elements to be fixed in positions (e.g. pin to left, pin to right), but there is no way to specify changes in layout for certain widths of a screen, called breakpoints. Don’t get me wrong, these tools are awesome. I use Sketch on a daily basis, it is my go-to software to design static screens. However, these design tools are just not able to execute what I have in mind to prototype a responsive web design.

“Resizing” — allows constraint to be set to elements in Sketch

Detracting a little, here are two very good articles of the responsive capabilities of Sketch and Figma, which are very useful functions for another purpose (such as prototyping for different mobile screen sizes, where there is no drastic change in layout):

Responsive Design in Sketch by Emin Inanc Unlu

5 Essential Ways to Use Design Constraints (Figma blog)

Website builders

As mentioned previously, many non-coding design tools are not able to change layouts based on breakpoints. I’ve also looked a tool specifically for website building, i.e. WebFlow. While I’m sure WebFlow is able to achieve what I want, it is of itself a full website builder. This means that it would take too much time and effort to build the interactions that I am testing — it might as well be my final website! It doesn’t fulfill the “build fast” requirement for my prototyping tool.

Landing page of WebFlow

Advantages of Framer for responsive web prototyping

Framer is a tool that combines both design and coding. In the design part, it has basic functions that are sufficient to build up to several screens, but it lacks advanced design features such as grouping multiple screens into different pages (like in Sketch), or the ability to have a master element that can be repeatable, often called Symbols or Components.

However, the power of Framer lies in its Code tab. By making layers in the Design tab as Targets, you can apply code to these layers to specify behaviours based on user actions. This allows me to specify different behaviours based on different screen widths; exactly what I need for responsive web.

At the same time, because I can make artwork right inside Framer itself, or import from Sketch/Figma/Photoshop, I don’t need to build design elements from scratch, and that saves me a lot of time.

Walking through creating a responsive page in Framer

I tested the responsive layout for the home page of barterbay.ca using Framer. You can see my prototype here (intended to be viewed on a laptop/desktop computer), and resize the window to test its responsiveness.

Import design elements

First, I created each design element for the home page in Sketch. They are all wireframes at this point, because I wanted to test the behaviours, not the content. For repeating elements such as the category buttons and the items, I just created one of them, and duplicated them in the code in Framer. These are all my design elements imported into Framer:

Select “Canvas” for the prototype

Framer has different sizes for different types of devices. Always go for “Canvas” for responsive web.

Change default cursor and hide hints

Next, the default cursor for Framer is a transparent circle, to emulate mobile interactions. I needed to change that into a normal cursor for web interactions. I also hid interaction hints to make it a more seamless experience.

# Function change cursor
Cursor = (style) ->
document.body.style.cursor = style
# Set the cursor to normal
Cursor(“auto”)
Framer.Extras.Hints.disable()

Declare variables for screen sizes and other elements

At this point, you need to have determined how many breakpoints you want, and the screen widths for the breakpoints. I put these as variables, so that later on, it will be easy to change the breakpoints just by playing with these numbers. Also, I store measurements from the design elements into variables — these will be used for mathematical calculations later. Not all of these measurements need to be stored and prepared right from the start. If you miss something out, you can always come back here later and declare more variables.

#-------------------- set up variables -----------------------------
widthSmall = 992
widthMedium = 1200
widthMax = 2322
catMinPadding = 20
itemSmallOriginalWidth = item_small.width
itemMediumOriginalWidth = item_medium.width
itemLargeOriginalWidth = item_large.width
bannerOriginalWidth = banner_image.width
bannerOriginalHeight = banner_image.height
#1 = small, 2 = medium, 3 = large 4 = max
screenType = 0

Create a page component and add all elements

Next, I create a page as a parent to hold all other components. I’ll create a scrollComponent for the whole page later on.

#----------------put everything into a page-------------------------
browsePage = new Layer
backgroundColor: null
height: 3000

Next, remember that I mentioned that I’ll create duplicates of my design elements? This is where I do it. First, I create a layer for the element, for example, a layer to hold the category buttons. I specify the page as this layer’s parent. Next, I then specify the layer to be the parent of the category button that I imported. Finally, I duplicate the category. I do the same for the items. Essentially, I am building a hierarchy that looks something like this —

#create all the categories
categoryBar = new Layer
backgroundColor: null
parent: browsePage
category1.parent = categoryBar
for index in [2..12]
category = category1.copy()
category.parent = categoryBar

Create a scrollComponent

Even though the positioning of my elements is not done yet (They’re just kind of floating around right now), I want to make my page scrollable, but only vertically. This is why everything was parented to my page — so that I just need to specify that the page is scrollable.

#---------------create Scroll Component for page--------------------
scrollBrowse = new ScrollComponent
scrollHorizontal: false
mouseWheelEnabled: true
browsePage.parent = scrollBrowse.content

Recalculating the screen size

I need to tell Framer to do things. In this instance, I want things to happen when I resize the screen, i.e. calculate the positions of all my elements. I want to do this on startup too, and that is why I run my recalculateScreen() function first. Note that due to how Framer code is parsed line by line, these two lines of code have to come right at the end of the code.

#-------------------------- RUN ----------------------------------#
recalculateScreen()
Screen.onResize ->
recalculateScreen()

Here’s my recalculateScreen function, i.e. the steps to calculate the layout of each element. Each one of these is a function that I’ve written.

#function that runs to recalculate all elements
recalculateScreen = ->
selectScreenType()
drawBanner()
drawCategories()
drawFilters()
drawItems()
resizePageWrap()

I specify the screen size in numbers so that it will be easy to use them as cases in a switch statement.

#function to select screen size type
selectScreenType = ->
if Screen.width <= widthSmall
screenType = 1
else if Screen.width <= widthMedium && Screen.width > widthSmall
screenType = 2
else if Screen.width <= widthMax && Screen.width > widthMedium
screenType = 3
else
screenType = 4

For example, specifying the size of the banner depending on the screen size:

#draw banner
drawBanner = ->
banner.x = 0
banner.width = Screen.width
switch screenType
when 1
banner.height = 260
when 2
banner.height = 260
when 3
banner.height = 350
when 4
banner.height = 350
banner.width = widthMax
banner.x = (Screen.width - widthMax) / 2
banner_image.width = banner.width
banner_image.height = banner.height

I continue to use switch statements to position the elements according to the screen size — for the categories, filters and items.

Finally, there is a step that is easily missed, but very important. You will need to resize the page component and scroll component to the new size of the window, or things will not work right. This is the code to do that:

resizePageWrap = ->
browsePage.width = Screen.width
scrollBrowse.width = Screen.width
scrollBrowse.height = Screen.height

Annex: The full code, for reference

# Function change cursor
Cursor = (style) ->
document.body.style.cursor = style
# Set the cursor to normal
Cursor("auto")
Framer.Extras.Hints.disable()
#-------------------- set up variables -----------------------------
widthSmall = 992
widthMedium = 1200
widthMax = 2322
catMinPadding = 20
itemSmallOriginalWidth = item_small.width
itemMediumOriginalWidth = item_medium.width
itemLargeOriginalWidth = item_large.width
bannerOriginalWidth = banner_image.width
bannerOriginalHeight = banner_image.height
#1 = small, 2 = medium, 3 = large 4 = max
screenType = 0
#----------------put everything into a page-------------------------
browsePage = new Layer
backgroundColor: null
height: 3000
#----------------------------- drawing -----------------------------
banner = new Layer
backgroundColor: null
clip: true
parent: browsePage
banner_image.parent = banner
banner_image.x = 0
banner_image.y = 0
#create all the categories
categoryBar = new Layer
backgroundColor: null
parent: browsePage
category1.parent = categoryBar
for index in [2..12]
category = category1.copy()
category.parent = categoryBar
#create the filters
filterBar = new Layer
backgroundColor: null
parent: browsePage
filter1.parent = filterBar
filter2.parent = filterBar
filter3.parent = filterBar
filter4.parent = filterBar
#create many items
allItemsSmall = new Layer
backgroundColor: null
parent: browsePage
item_small.parent = allItemsSmall
for index in [1..20]
item = item_small.copy()
item.parent = allItemsSmall
allItemsMedium = new Layer
backgroundColor: null
parent: browsePage
item_medium.parent = allItemsMedium
for index in [1..20]
item = item_medium.copy()
item.parent = allItemsMedium
allItemsLarge = new Layer
backgroundColor: null
parent: browsePage
item_large.parent = allItemsLarge
for index in [1..20]
item = item_large.copy()
item.parent = allItemsLarge

moreCatArrow.parent = browsePage
#---------------create Scroll Component for page--------------------
scrollBrowse = new ScrollComponent
scrollHorizontal: false
mouseWheelEnabled: true
browsePage.parent = scrollBrowse.content
#----------------------------- functions ---------------------------
#function that runs to recalculate all elements
recalculateScreen = ->
selectScreenType()
drawBanner()
drawCategories()
drawFilters()
drawItems()
resizePageWrap()
resizePageWrap = ->
browsePage.width = Screen.width
scrollBrowse.width = Screen.width
scrollBrowse.height = Screen.height
#function to select screen size type
selectScreenType = ->
if Screen.width <= widthSmall
screenType = 1
else if Screen.width <= widthMedium && Screen.width > widthSmall
screenType = 2
else if Screen.width <= widthMax && Screen.width > widthMedium
screenType = 3
else
screenType = 4
#draw banner
drawBanner = ->
banner.x = 0
banner.width = Screen.width
switch screenType
when 1
banner.height = 260
when 2
banner.height = 260
when 3
banner.height = 350
when 4
banner.height = 350
banner.width = widthMax
banner.x = (Screen.width - widthMax) / 2
banner_image.width = banner.width
banner_image.height = banner.height
#position the categories
drawCategories = ->
if screenType == 4
screenPadding = (Screen.width - widthMax) / 2
for child, i in categoryBar.subLayers
moreCatArrow.visible = false
catPadding = (widthMax - (12*category1.width) - (2* catMinPadding))/11
child.visible = true
child.x = (i * (category1.width + catPadding)) + screenPadding
child.y = banner.height + 30
else
#categories don't fit on screen
if (12*(category1.width + catMinPadding) + catMinPadding + moreCatArrow.width) > Screen.width
moreCatArrow.visible = true
moreCatArrow.y = banner.height + 50
moreCatArrow.minX = Screen.width - 50

for child, i in categoryBar.subLayers
child.x = i*(category1.width + catMinPadding)
child.y = banner.height + 30

if i == 0 || (i+1) * (category1.width + catMinPadding) + moreCatArrow.width < Screen.width
child.x = i * (category1.width + catMinPadding) + catMinPadding
child.visible = true
else
child.visible = false

#categories fit on screen
else
moreCatArrow.visible = false
for child, i in categoryBar.subLayers
catPadding = (Screen.width - ((12*category1.width)+(2*catMinPadding)))/11
child.visible = true
child.y = banner.height + 30
if i == 0
child.x = catMinPadding
else
child.x = i * (category1.width + catPadding) + catMinPadding
#position filter buttons
drawFilters = ->
for child, i in filterBar.subLayers
child.y = category1.maxY + 30
if i == 0
if screenType == 4 then child.x = (Screen.width - widthMax) / 2 else child.x = catMinPadding
else
if screenType == 1
filterPadding = catMinPadding
else if screenType == 4
filterPadding = widthMax / 768 * catMinPadding
else
filterPadding = Screen.width / 768 * catMinPadding
if filterPadding < catMinPadding then filterPadding = catMinPadding

child.x = filterBar.subLayers[i-1].maxX + filterPadding
#position items
drawItems = ->
switch screenType
when 1
allItemsLarge.visible = false
allItemsMedium.visible = false
allItemsSmall.visible = true
numColumns = Math.floor((Screen.width - 2*(catMinPadding)) / (item_small.width + 30))
sidePadding = (Screen.width - (numColumns * item_small.width) - (numColumns * 30)) / 2
for child, i in allItemsSmall.subLayers
newWidth = (Screen.width - (numColumns * 30)) / numColumns
newScale = newWidth / itemSmallOriginalWidth
#child.scale = newScale
child.x = sidePadding + (i % numColumns) * (item_small.width + 30)
child.y = (filter1.maxY + 40) + Math.floor(i/numColumns) * (item_small.height + 50)

when 2
allItemsLarge.visible = false
allItemsMedium.visible = true
allItemsSmall.visible = false
numColumns = Math.floor((Screen.width - 2*(catMinPadding)) / (item_medium.width + 30))
sidePadding = (Screen.width - (numColumns * item_medium.width) - (numColumns * 30)) / 2
for child, i in allItemsMedium.subLayers
child.x = sidePadding + (i % numColumns) * (item_medium.width + 30)
child.y = (filter1.maxY + 40) + Math.floor(i/numColumns) * (item_medium.height + 50)

when 3
allItemsLarge.visible = true
allItemsMedium.visible = false
allItemsSmall.visible = false
numColumns = Math.floor((Screen.width - 2*(catMinPadding)) / (item_large.width + 30))
sidePadding = (Screen.width - (numColumns * item_large.width) - (numColumns * 30)) / 2
for child, i in allItemsLarge.subLayers
child.x = sidePadding + (i % numColumns) * (item_large.width + 30)
child.y = (filter1.maxY + 40) + Math.floor(i/numColumns) * (item_large.height + 50)

when 4
allItemsLarge.visible = true
allItemsMedium.visible = false
allItemsSmall.visible = false
numColumns = 8
sidePadding = (Screen.width - (numColumns * item_large.width) - (numColumns * 30)) / 2
for child, i in allItemsLarge.subLayers
child.x = sidePadding + (i % numColumns) * (item_large.width + 30)
child.y = (filter1.maxY + 40) + Math.floor(i/numColumns) * (item_large.height + 50)
#-------------------------- RUN ----------------------------------#
recalculateScreen()
Screen.onResize ->
recalculateScreen()

Conclusion

For testing out and tweaking responsive elements for web, Framer can be a good tool for doing it. It may seem like a lot of work, but I built the prototype in a day, and it formed my foundation for delving into the full design of the page. It was also very easy and convenient to share exactly what I intend to my developers — I just had to pass them the Framer cloud link. All in all, I definitely see myself using Framer again for prototyping complex responsive web behaviours.

--

--

UX & UI designer for PowderMonkey, a digital design studio. Animator for The Last Crystal indie game. I love all things digital, and dabble in code too!