How to Design Responsive Layouts in Framer

Using key-value objects to make multi-device prototypes.

BPXL Craft
Published in
5 min readMar 29, 2017

--

If you work long enough with Framer prototypes, eventually you’ll encounter the problem of how to adapt a prototype’s layout to handle multiple devices and orientations. Rather than create a separate prototype for every situation — say, one for iPhone in portrait orientation and one for iPad in landscape — it’s possible to design a responsive prototype that adapts its metrics as the situation demands. This requires some setup, but the process can be greatly simplified by a data structure known as an object.

“Object” is a vague term that could refer to many things. Here, we’re using it to refer to a collection of key-value pairs. These work like variables: The key lets you reference the pair by a name, while the value holds its associated data.

In CoffeeScript, the language used to make Framer prototypes, you can construct an object like:

objectName =
key: value
anotherKey: anotherValue

(Note that the equal sign follows the name of the object, but a colon is used to separate keys and values.)

More advanced hierarchies are possible with nested objects, which is where things get interesting:

objectName =
keySet1:
key: value
anotherKey: anotherValue
keySet2:
key: value
anotherKey: anotherValue

It’s easy to see how this may translate into a solution for our layout problem:

sizing =
iphone:
viewSidePadding: 20
ipad:
viewSidePadding: 40

But how do we access these values? Conveniently, CoffeeScript provides two methods: dot notation and bracket notation. For dot notation, we begin with the object name and then walk down the hierarchy, separating references with dots:

print sizing.iphone.viewSidePadding

With bracket notation, we instead wrap each reference in brackets and quotation marks:

print sizing["iphone"]["viewSidePadding"]

(Note that the object name is not wrapped in anything.)

The quotation marks seem like a hassle at first, but in fact they’re invaluable. They allow us to construct the names of our keys via expressions. You could, for example, do something like this:

print sizing["iphone"]["view" + "SidePadding"]

So far, this isn’t any more useful, but you may begin to see how these references can be split up and reconstructed as needed.

It’s also possible to mix and match these notations any which way you like:

print sizing["iphone"].viewSidePaddingprint sizing.iphone["viewSidePadding"]

To make this approach to responsive layouts work, we need three things:

  1. A method for determining device type.
  2. A method for determining device orientation.
  3. A method for grabbing the value that matches these.

For the sake of brevity, here we’ll just look at the first and last items. (One of the attached prototypes at the end will demonstrate the complete package.)

Create a new prototype in Framer and add the following code:

# specifications
sizing =
viewTopPadding: 30
iphone:
viewSidePadding: 20
itemMargin: 16
itemSize: 250
ipad:
viewSidePadding: 40
itemMargin: 64
itemSize: 400

(Note that some key-value pairs live right in the top level. These are common to both iPhone and iPad and their values won’t change with devices.)

Next, add a function to detect the device type. How you go about this will depend on where you want your prototype to be seen, but here’s a method that works within Framer:

# get device type
checkDevice = (deviceType = "iphone") ->
framerDevice = Framer.Device.deviceType
deviceType = "iphone" if _.includes(framerDevice, "iphone")
deviceType = "ipad" if _.includes(framerDevice, "ipad")
return deviceType

If your prototype is intended for preview on mobile devices, detecting Screen.width may prove to be a more reliable method.

Our checkDevice() function will return either the string “iphone” or “ipad.” Supplying the key in a key-value pair is all we have to add to get our numerical spec. For example, this will now work:

deviceType = checkDevice()
value = "viewSidePadding"
print sizing[deviceType][value]

We can expand this into a general-use function that accepts a key name and returns the appropriate value for the current device. You can use the code below, but note two things about it:

  • Values deeper in the object hierarchy will override those higher up. If you define a cross-platform specification value, it’s best not to repeat that value at the device level.
  • If no matching values are found, the function prints an error message.
# check specification
getSpec = (spec) ->
if !spec then print "Need to specify a spec name."

deviceType = checkDevice()
lookup = _.assign({}, sizing, sizing[deviceType])
result = _.get(lookup, spec)

if !result then print "Value not found for spec named #{spec} on #{deviceType}."

return result

With these functions in place, we simply need to call getSpec(“ourKeyName”) where “ourKeyName” is the key name of a value we want. Thus, getSpec(“viewSidePadding”) will return 20 on iPhone but 40 on iPad. getSpec(“viewTopPadding”) will return 30 either way.

We can quickly knock out a PageComponent-based carousel of items using our specifications and substituting function calls for any of the dimensions:

carousel = new PageComponent
width: Screen.width
x: getSpec("viewSidePadding")
y: getSpec("viewTopPadding")
height: getSpec("itemSize")
width: getSpec("itemSize")
clip: false
scrollVertical: false
for i in [0..4]
carouselItem = new Layer
parent: carousel.content
width: getSpec("itemSize")
height: getSpec("itemSize")
x: i * (getSpec("itemSize") + getSpec("itemMargin"))

This is enough to build layouts that respond to changes in device type. If you want to adapt to orientation as well, you will need to detect orientation and then redraw all layers any time that changes. The simplest way to achieve this is to wrap all layer creation in a layout function. Any orientation change should first destroy all layers and then call the layout function to rebuild them, or the layout function can handle the layer destruction itself as a first step.

If existing layers aren’t destroyed on orientation switch, you’ll end up with stacks of duplicate layers as your layout function is repeatedly called.

Wrapping our carousel creation in a layout function might look like so:

# the view layout function
doLayout = () ->
# destroy all layers, to avoid duplication
for layer in Framer.CurrentContext.layers
layer.destroy()

# create a carousel
carousel = new PageComponent
width: Screen.width
x: getSpec("viewSidePadding")
y: getSpec("viewTopPadding")
height: getSpec("itemSize")
width: getSpec("itemSize")
clip: false
scrollVertical: false

# populate the carousel with cells
for i in [0..4]
carouselItem = new Layer
parent: carousel.content
width: getSpec("itemSize")
height: getSpec("itemSize")
x: i * (getSpec("itemSize") + getSpec("itemMargin"))
# initialize
doLayout()

To experiment with the prototypes and see how these mechanisms work together, download the simpler prototype and download one featuring orientation switch support.

Another approach is to store iPad-specific sizing specs in a separate, override object. Download the override prototype to see that method in action.

For more insights on design and development, subscribe to BPXL Craft and follow Black Pixel on Twitter.

Black Pixel is a creative digital products agency. Learn more at blackpixel.com.

--

--

BPXL Craft

Designer. Xplane | Firewheel Design | Gowalla | Black Pixel | Kaleidoscope | NetNewsWire | Hypergiant