Code the iPhone 14 Landing Page, Gallery Grid

Jon Lehman
8 min readDec 7, 2022

Apple is known for launching their products alongside excellently designed landing pages. The pages usually include beautiful product photography, slick scrolling animations, and engaging interactivity to highlight key features.

Jon Lehman — Medium
Scrolling through Apple’s iPhone 14 landing page
Examples of Apple’s landing page titles

Today I would like to cover how I recreated a section of the iPhone 14 landing page. Apple appears to call this section the “design gallery” (based on what I can inspect in the browser). This section stuck out to me because of its elegant grid and snappy interaction.

TL;DR: If you’re here for the code, feel free to scroll to the bottom for the completed codepen. Admittedly, I kind of rambled.

Gallery interaction on Apple’s iPhone 14 landing page

Global Setup & Shared Styles

The first thing I like to do when I start a project like this one is to define some base CSS styles that I know I will need throughout the project. Setting these up at the beginning saves me time refactoring later and gets the project looking just like the example right at the start.

Font

As you likely know, Apple uses its own in-house font family known as ‘SF Pro’. Apple provides a way to download SF Pro, so we could host it and import it. However, to keep things simple I just used a popular alternative called ‘Inter’ that is available on Google Fonts.

I took a look at the iPhone 14 page and it appears we need just two weights, regular (400) and semibold (600). The following line of CSS imports the necessary fonts:

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');

Colors

Apple loves their shades of gray (hehe). Since these were going to be used over and over, I setup several CSS variables. The device colors (blue, purple, midnight, etc) only seemed to be used once so I didn’t bother including those.

:root {
--gray1: #f5f5f7;
--gray2: #6e6e73;
--gray3: #1d1d1f;
}

Body & Container

If this is a brand new project there are a couple of body styles I set, like changing the box-sizing to border-box, margins to zero, and default font. I also add some basic centering styles to the body tag for presentation purposes. If this was integrating into an existing project I may not mess with those styles to avoid a mess.

The primary #container is also where the CSS grid is setup. I’ll cover more about this is the next section “Layout Grid”.

*, html, body {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
margin: 0;
}

body {
min-width: 100%;
display: flex;
justify-content: center;
padding: 60px 0;
}

#container {
width: 980px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: repeat(3, 336px);
gap: 15px;
grid-auto-flow: row;
grid-template-areas:
"display durability lockScreen"
"display colorPicker camera"
"waterResistance glass glass";
}

Type Styles

If I knew how this grid was going to be used on the broader page I would setup the type styles targeting html tags (ie, <h1>, <h2>, <p>, etc). Since this is more of a snippet demo I just used <p> tags throughout with custom styles (.xl, .large, and .small). Note the use of CSS variables for the colors.

These styles were taken directly off of the official iPhone 14 page. I am not certain why the line-height and letter-spacing values are so specific. “1.0714285714”? 🥴 .. is everything ok Apple?.. Just kidding, I am sure there are practical reasons.. hopefully 🙃.

p.xl {
font-size: 56px;
line-height: 1.0714285714; /* 🥴 */
font-weight: 600;
letter-spacing: -0.005em;
}

p.large {
font-size: 40px;
line-height: 1.1;
font-weight: 600;
letter-spacing: -0.022em;
color: var(--gray3);
}

p.small {
font-size: 21px;
line-height: 1.1904761905;
font-weight: 600;
letter-spacing: .011em;
color: var(--gray2);
}

Special

There were also a couple of shared styles that were more specific to this project. First, each tile in the grid is targeted in CSS using #container > div and given predictable styling. Second, each piece of tile content that would “slide” with interaction was aptly given the class “.slide”. The styles set for the slide class absolute center the content and provide some transition. There are two transitions, one for the position and one for opacity. These transitions are taken directly from Apple’s page. However, Apple used transform translate to animate the position where I use “left”.

#container > div {
min-height: 336px;
position: relative;
background: var(--gray1);
border-radius: 18px;
overflow: hidden;
}

.slide {
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
transition: left 1s cubic-bezier(0.8,0,0.2,1), opacity 1.3s ease;
}

Layout Grid

Just in case you’re new to CSS grids I thought it was worth spending a bit more time on the #container styles shown in the “Body & Container” section above. Here’s the css again:

#container {
width: 980px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 336px);
gap: 15px;
grid-auto-flow: row;
grid-template-areas:
"display durability lockScreen"
"display colorPicker camera"
"waterResistance glass glass";
}

For an in-depth review of CSS grids check out CSS-Trick’s Complete Guide to CSS Grids. Start by setting both your grid-template-columns and grid-template-rows. This is a fairly simple 3x3 grid, each column is just 1fr (one fraction), while each row is 336px. To make some tiles stretch past the column/row grid (“Vibrant Super Retina XDR display” tile and the “Ceramic Shield” tile) I used grid-template-areas. I like using named areas anytime I am creating a grid with more than 4 areas and they’re static. Notice how “display” and “glass” are repeated to get that area/tile to extend into the neighboring area.

Images

There are many ways to gather the images used on the iPhone 14 landing page. Right clicking and saving the images may work for some cases but this may have mixed results if the implementation uses a <picture> tag with multiple sources, or a CSS background. Inspecting the code with browser devtools is a workaround but can take a long time. My “Pro tip” is to use a browser extension such as CSS Peeper to collect and export images from webpages. CSS Peeper (and likely other extensions) make this process super quick and painless.

Chrome extension “CSS Peeper” asset window

“Pro tip: Use a browser extension such as CSS Peeper to collect and export images from webpages.”

For the codepen creation I uploaded the images to cloudinary. Cloudinary is my go-to easy-peezy image host.

Card Design

I won’t go too in-depth on each individual grid card/tile’s design. Each one is slightly unique. Check out the codepen’s CSS for more details, each card/tile’s styles are comment separated.

The most complicated card/tile was the center “Color Picker Card”, which includes the interactions. I’ll go over that more below.

Interaction

Most of what I will cover in this section is the entirety of the JS in the codepen. If you prefer, check out the code. Each part of the JS is comment described.

Using a forEach method paired with a query selector, every time a color picker button is clicked, the following occurs:

  1. The ‘active’ class is removed from any of the other color picker buttons, and the ‘active’ class is added to the one that was just clicked.
// On click, remove the "active" class (black ring) from color circle button
buttons.forEach(button => {
button.classList.remove('active');
})
// On click, add the "active" class (black ring) to the clicked color circle button
button.classList.add('active');

2. Set the “newColor” variable based on which button was clicked using the custom “data-color” attribute.

// On click, get the newly clicked color circle button's color
let newColor = button.getAttribute('data-color')

3. Remove the “rotate” and “rotate-reverse” classes from the content shown on the center tile that feature a perspective animation when a new color is selected. We also make a call to the “offsetHeight” for these pieces of content; this forces the browser to repaint the content. If the browser does not repaint the content, the animation will not play on repeated uses.

// On click, remove the "rotate" and "rotate-reverse" classes (perspective animations) from the center tile text
perspectiveSlides.forEach(el=>el.classList.remove('rotate', 'rotate-reverse'))
perspectiveSlides.forEach(el=>el.offsetHeight) // This call to "offsetHeight" forces the browser to repaint the styles

4. Next, a function that is triggered on each color picker click. The function takes an array of the sub sequential colors (“nextColors”). The function checks if this array includes the “currentColor” (color before button was clicked) to determine if the class “rotate” or “rotate-reverse” should be added. This is what creates the perspective animation that rotates right vs left depending on the color button clicked.

// Reusable function for adding classes "rotate" or "rotate-reverse" depending on color selected (depends on if it was selected to left or right of the currently selected color)
function rotateColor(nextColors) {
if (nextColors.includes(currentColor)) {
perspectiveSlides.forEach(el=>el.classList.add('rotate'));
} else {
perspectiveSlides.forEach(el=>el.classList.add('rotate-reverse'));
}
currentColor = newColor
}

5. Finally, it all comes together with an if, if else sequence for each color (“newColor”). The slide content is given the necessary inline styles to transition them left/right and fade them in/out. The “rotateColor” function that we created in the last step is called with an array parameter that lists the colors that are sub sequential to the “newColor”.

// If the color picked is X style the tile "slide" content to the left, bring opacity from 0 to 1, and do the opposite for the others. Trigger reusable function that adds "rotate" or "rotate-reverse" classes to the center tile text.
if (newColor == "blue") {
document.querySelectorAll('.blue-slide').forEach(el=>{el.style.left = '0'; el.style.opacity = "1"});
document.querySelectorAll('.purple-slide, .midnight-slide, .starlight-slide, .red-slide').forEach(el=>{el.style.left = '150%'; el.style.opacity = "0"});
rotateColor(["purple", "midnight", "starlight", "red"])
. . .
// See following code on codepen

Final

Here’s the final codepen:

There are a couple of features that are not included in this codepen that are used on the official iPhone 14 page. The official page has an on-scroll reveal that I didn’t bother including. The official page also is more accessible through keyboard navigation.

That’s it! I hope you enjoyed this breakdown! If you found this useful please considering following me here on Medium. This is the best way to support me and more articles like this one.

--

--