In October, together with Glenn, Henry, and Alasdair, we shipped a new homepage for Vercel. Our north stars for the design were performance, constraint in visual flair, and opportunity for bridging different pages aesthetically. Yet, I wouldn't describe the page as minimal as we made use of different motifs like grid lines and pixelation, but rather an exercise in constraint and tranquil beauty. For instance, if an animation or interaction didn't perform well, felt pompous, or out of rhythm relative to the page, we didn't build it.
We anticipate conceptually trivial elements of an interface to gratuitously entertain and afford us with yet another masterpiece of an interaction. Naturally, we celebrate the minutiae of visual finesse to a degree that folks outside the rabbit hole don't relate as intensely to.
But what elements of an interface are largely universally experienced? The page speed, legible typography, information honesty, layout stability and scannability, accessible focus states, auditory feedback, and sensible DOM ordering. Alas, primitives such as performance and accessibility are not as glamorous or fruitful to obsess over — because seemingly, they are invisible, and thus are trivial to trade off when it comes to shipping quickly. Truthfully, I would much rather spend time on polishing that animation spring over tracking down what made the initial load so slow. But is having a slow website with immaculate attention to visual craft desirable?
We are proud to have struck a balance in shipping a website that feels reflective of our collective taste, yet doesn't nervously exhibit itself through artificial gimmicks. In this essay, we'd like to share said taste, convictions, and some technical implementation details.
Aesthetic Foundation
The new homepage is a sum of many parts. This year, Basement created our own typeface family. The goal was for beautiful, Swiss inspired design with optimisation for displays; chiefly to be used on our interface, documentation and code blocks. Icons are a often a precursor to text, so we also designed our own iconography set to pair with it. Add in a significant upgrade of our design system to supply us a wealth of new interface solutions, and there were three new common components to roll out across the site.

It felt apt to use these and chart ourselves new territory at the same time. What would pair best with a Grotesque sans-serif? We looked at the history of the Swiss design movement and fell in love with the raw exposure to the creation of design; blocks, grids, modularity. At its core Vercel is an infrastructure for frameworks, so the metaphor of showing a literal frame on every page became a hallmark to rally around.

At the same time, Vercel is about speed. Having a consistent grid position across all pages as a user navigates through helps the user see what content swaps out, and how quickly. It lessens the appearance of a pseudo “layout shift” happening between views, and highlights incredible performance of server rendering. With this in mind, we dabbled with a set of early ideas about how the “hero” could capture this rigidity but still allow creativity. The pitch to @rauchg ended up being a series of ideas that will come to life in the following months:

What came out of this ideation is a signature hero, and set of columns to act as content guides for the rest of the page. To help guide the eye at certain points, we opted for crosshairs inspired by traditonal print center marks. At 1080 wide, the grid also helps each column of 360 have a readable line-length for 14-16px text. Given the modularity of everything being a block, it also collapses nicely into mobile. This empowers our design and engineering teams to roll out pages with a strong identity, consistency and speed.
Combined with the slightly curious prompt of "What will you ship?", we related to representing the full spectrum of the color wheel in the hero gradient as a metaphor for creativity.

As the design of this project commenced directly after @rauchg's Figma Config 2023 talk, we also made use of Figma’s new variables feature throughout the entire system. This meant content div sizing, column sizing, inner paddings, and margins between sections were all set up to adapt when dragged between desktop, tablet and mobile size classes.
Color variables are also rife throughout — we adopted @kevvy's new Vercel color system, optimized for accessibility, as color variables. With these new tools, the entire team can see pages adapted to dark mode at different sizes in seconds, rather than minutes.
Grid System
After the initial designs were signed off we very quickly recognized the pivotal role of the grid and knew its foundation would be a key to our success. It was imperative for us to craft a grid system that seamlessly combined performance, responsiveness, and a strong DX while offering a suite of out of the box defaults to cater to common layout needs. We wanted to create a grid system component that could be used to build out entire pages of grid based layouts:
{/* The system is only rendered at the root of the page */}
<Grid.System guideWidth={1}>
<Grid columns={3} rows={3}>
{/*
Each cell can contain arbitrary JSX, and will render
grid lines automatically
*/}
<Grid.Cell column={1} row={1}>1</Grid.Cell>
<Grid.Cell column={2} row={2}>2</Grid.Cell>
<Grid.Cell column={3} row={3}>3</Grid.Cell>
{/* Crosses can be set between the intersection of cells */}
<Grid.Cross column={1} row={1} />
<Grid.Cross column={-1} row={-1} />
</Grid>
</Grid.System>
The actual implementation of this component turned out to be anything but straight forward. The first significant hurdle emerged when it came to a crucial design element of the grid—drawing grid lines. Drawing guides even for a simple grid is an incredibly non-trivial task. The most common method involves bordering every child in the grid on two adjacent perpendicular sides, such as the right and bottom sides. Assuming every cell in the grid is filled, the result will have properly drawn guides but will lack the top and left borders on the grid itself. To address this issue, we can simply add a top and left border on the grid and voila, we have a grid with guides.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 50px);
border: 2px solid #666;
border-right: none;
border-bottom: none;
}
.grid > div {
border: 2px solid #666;
border-left: none;
border-top: none;
}
For simple grids, this method works well. However, it quickly falls apart as soon as you begin to stray away from this basic model. For example, a major drawback of this method is that it requires every cell to be filled in the grid. For the above grid the markdown looks like this:
<div className="grid">
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
</div>
We don't always want to explicitly define and fill each cell with content. The grid component already knows how many rows and columns we want from props, so it should just render the guides regardless of how many cells are actually given as children. So how can we draw guides around non-existent cells and ensure proper behavior when cells extend across multiple rows or columns?
Faced with this challenge, I eventually stumbled my way across a CSS property that is incredibly useful for this exact circumstance: display: contents. After learning about this property it really felt like I had discovered a hidden gem. This property causes an element's children to appear as if they were direct children of the element's parent, ignoring the element itself. You might be wondering why this is so useful, so let's work through how we can implement a React component that generates the guides for a grid given a set of rows and columns. First lets define the skeleton of our component:
interface GridProps {
rows: number;
columns: number;
children: ReactElement<GridCellProps>[];
}
function Grid({ rows, columns, children }: GridProps) {
return (
// ...
);
};
We know that there are 3 main parts at play here. The parent grid element, the children of the grid, and the grid guides. The first step is to create the parent grid element and pass along our rows and columns as CSS variables:
function Grid({ rows, columns, children }: GridProps) {
return (
<div className="grid" style={{ "--rows": rows, "--columns": columns }}>
{children}
</div>
);
};
Next, we need to render the grid guides. We can do this by creating a div with a class of grid-guides and rendering it as a direct child of the grid. Inside of this div, we will want to create rows * columns number of elements to fill our grid. Lastly, we just need to applyposition: relative to the grid class, and display: contents property to the grid-guides class. This will render the children of the grid-guides div as if they were direct children of the grid. However to ensure that these children don't interfere with the actual children on the grid, we need to apply a position: absolute and inset: 0px to every guide cell.
function Grid({ rows, columns, children }: GridProps) {
return (
<div className="grid" style={{ '--rows': rows, '--columns': columns }}>
<div className="grid-guides">
{Array.from({ length: rows * columns }, (_, index) => {
// Calculate the x and y position of the cell
const x = (index % columns) + 1;
const y = Math.floor(index / columns) + 1;
return (
<div
className="grid-guide"
style={{ '--x': x, '--y': y }}
/>
);
})}
</div>
{/* Cells will render here */}
{children}
</div>
);
};
.grid {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
grid-template-rows: repeat(var(--rows), 1fr);
border: 2px solid #666;
border-right: none;
border-bottom: none;
position: relative;
}
.grid-guides {
display: contents;
}
.grid-guide {
inset: 0px;
position: absolute;
grid-column-start: var(--x);
grid-column-end: span 1;
grid-row-start: var(--y);
grid-row-end: span 1;
border: 2px solid #666;
border-left: none;
border-top: none;
}
And with that, our basic component is done! Notice how we no longer need to define any cells to receive guides. Let's see what it looks like in practice. Feel free to edit the number of columns or rows:
() => <Grid rows={5} columns={5} />
Now that we have our main Grid component done, we can move onto creating a simple React abstraction for grid cells. Take a look at the following:
interface CellProps {
row: number;
column: number;
children: ReactNode;
}
function Cell({ row, column, children }: CellProps) {
return (
<div
className="grid-cell"
style={{ gridRow: row, gridColumn: column }}
>
{children}
</div>
);
};
This component is incredibly simple, but it allows us to easily render arbitrary JSX inside a cell on the grid by specifying the row and column of the cell and that is it!
<Grid rows={4} columns={4}>
<Grid.Cell row="auto" column={1}>
What
</Grid.Cell>
<Grid.Cell row="auto" column={4}>
will
</Grid.Cell>
<Grid.Cell row={2} column={3}>
you
</Grid.Cell>
<Grid.Cell row={3} column={4}>
ship
</Grid.Cell>
<Grid.Cell row={4} column="auto">
?
</Grid.Cell>
</Grid>
One very cool thing to note here is that passing the "auto" prop as a row/column is working here. If we were not using display: contents on our guides, setting "auto" as a row/column would break the grid entirely.
Another component to use with the grid is a cross which has the same API as the cell—you specify the row and column for which the cross appears:
<Grid rows={4} columns={4}>
<Grid.Cross column={1} row={1} />
<Grid.Cross column={-1} row={-1} />
</Grid>
Because the crosses are simply absolutely positioned relative to the specificed cell, they can be placed not only on the edges of the grid, but on any column and row combination:
<Grid rows={4} columns={4}>
<Grid.Cross column={1} row={4} />
<Grid.Cross column={2} row={3} />
<Grid.Cross column={2} row={4} />
<Grid.Cross column={3} row={2} />
<Grid.Cross column={3} row={3} />
<Grid.Cross column={3} row={4} />
<Grid.Cross column={4} row={3} />
<Grid.Cross column={4} row={4} />
<Grid.Cross column={5} row={4} />
</Grid>
At it's core, this is how we constructed our grid component for use on all our websites. Hopefully one could see how this component could be extended to support more advanced features such as cells that span multiple rows or columns, or responsive behaviors, i.e. setting the rows and columns to different values at different breakpoints. Additionally, this component and solution can be entirely server rendered since the component doesn't rely on client-side API-s at all.
Hero Composition
The hero visual is composed of multiple stacked layers of CSS, SVG, and progressively a shader. It does not rely on any client side code to display the essence of it, such as rendering a canvas element. The reason why this is important is to have a fast initial paint on the screen for something so crucial and center stage.
In descending order, the layer stacking order looks something like this:
- Heading
- SVG triangle
- CSS grid lines
- SVG rays
- CSS rainbow gradient
- GLSL shader

With layering we can progressively enhance the hero with a GLSL shader that gracefully fades in after the page has loaded. The async nature of the shader also allows us to code split it, perform light hardware detection to not render it at all on low powered devices while still retaining the core of the visual.
Visual Rhythm
Using an accent color for call-to-action buttons and other important elements has become an industry standard. If every element made repetitive use of a strong accent color, the color would no longer feel as significant.
For a consistent rhythm, we not only made deliberate effort not to overuse the grid and cross aesthetic. Subconsciously, we also made use of white space for a consistent rhythm in animation. When every element on a given section is signalling itself as novel or attractive, the novelty is diminished.
For example, consider this map of highlighted sections that either respond with motion to input or move independently. Orange sections signal high novelty, like the graph tooltip animating a long distance or icons pixelating on hover. Blue sections represent low novelty micro-animations like floating cursors or scaling icons on hover.

Scrolling through the page creates a consistent pattern of featuring an interlude following each animation segment. In this graphic, high novelty animations never appear consecutively between sections, but may be paired with lower novelty animations.
Pixelated Iconography
One of the most beloved parts of the home page were the pixelated icons. They are drawn in Figma, and extracted with a Ruby script that takes bitmaps as input, reads the pixel color at set intervals, and then creates a matrix for each icon (source). One of the matrices would look like this in code. Can you tell what framework logo this is for?
[
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
]
The fact that we have icons available as pixel data means we can render them in different formats.
For example, on the home page we render a canvas element for a smaller DOM footprint and animation. As a fallback, we make use of a base64 string to display a placeholder image which ends up costing merely ~1kb per icon. Optionally, the icon can also render as SVG for when they are above the fold.
The approach to rendering based on the matrix is pretty much the same between formats. For instance, the way we render them in canvas is by iterating over each row and pixel, and drawing a circle with arc() for a given coordinate:
matrix.forEach((row: number[], y: number) => {
row.forEach((pixel: number, x: number) => {
// 0 means transparent, no need to render
if (pixel !== 0) {
render(ctx, x, y, pixel);
}
// When done, hide the placeholder
if (y === matrix.length - 1 && x === row.length - 1) {
setLoading(false);
}
});
});
The rendering happens very fast but to illustrate what's happening we can slow rendering down. And funnily enough, it kind of reminds me of an old computer monitor painting each frame painstakingly slowly:
And of course, we treat the icons as images so they recieve an aria-label for proper semantics and screen readers:
<div
role="img"
aria-label="Next.js logo"
>
<canvas aria-hidden />
</div>
Accessible Code Blocks
We invested a lot of care into accessibility and went over the page with VoiceOver dozens of times to make sure we design the website for everyone as best as we could. VoiceOver is a screen reader that helps folks with visual impairements to understand the page and receive auditory feedback of text, images, and any other elements on a given page.
On the home page, we display a block of code. We had a component from our design system to pull in for this. Though, navigating the section together with @johnphamous we found that the experience could be more deliberate.
A couple of issues that surfaced were:
The file icon is non-descriptive, it just says "image" The copy button does not audibly describe it's purpose nor provide feedback on press The line numbers are road blocks of noise and can be confused with being part of the code
Most of the fixes were trivial: we could hide the icon with aria-hidden since the file name already includes the file type. And place an aria-label on the copy button.
On copying the code, we also want to make sure to provide auditory feedback that the code indeed was successfully copied. We can use ARIA live regions to expose dynamic content changes to screen readers. In this case, we want to conditionally render a message:
<>
{isCopied && (
<div
// We are showing a message, so `log` is appropriate
role="log"
// The update is low priority, and should report when the user is idle
// For compatibility we can explicitly set this
aria-live="polite"
// Hide the element visually, but don't
// use `display` or `visibility` for this
className="visually-hidden"
>
Copied code to clipboard
</div>
)}
</>
Since the block of code on the given page is complementary, we can also make the tag into an aside, and give it a label that describes what the code does at a high level. This way someone could decide to either skip the section or dive deeper:
<aside
aria-label="This is a code block of a React Server Component.
The component is an async function that reads data directly
from a SQL function in the render block of the component."
>
{...}
</aside>
The line numbers are self-incrementing pseudo elements. Here's a quick example:
.code { counter-reset: line; }
.line { counter-increment: line; }
.line:before { content: counter(line); }
Now, since there's no DOM element rendered we can't just throw aria-hidden on it. Instead, theres a second value for content which is alternative text for pseudo elements. By setting the content to an empty string we effectively are saying that nothing should be announced for this element:
.line:before {
content: counter(line) / "";
}
When all of these improvements are combined, we end up receiving more information from a screen reader. And not only for a single page, but for every piece of code that we present as the component powers hundreds of examples.