Page-builders and spacing

Page-builders and spacing

JonoJonoFounder

If you've ever worked with page builders and padding, or worse-still, user defined padding, you'll know why we wrote this.

I'm going to start with a trip down memory lane, and later on, get into the technical, so if you're a dev, you'll probably want to skip the first section, or stay along for the ride.

Standard spacing rule system

Ever wondered why we often use 8px inside design? Well, there isn't really a definitive reason, but I'm going to tell you why we harp on so much about it. It's because we design most websites for desktop. Yes, I know there was that whole "design for mobile first", but go walk over to your design team, ask them to show you a wireframe for your website, and I'll bet dollars to donuts they don't show you a mobile wireframe first.

So why desktop then? Well, as you probably know, most monitors aren't 4:3 anymore, they're nearly all 16:9, unless you're one of those weird "gamers" who have to go and get a 21:9 monitor and break the whole pattern. You know the interesting thing about 16:9 - it's divisible by 8. Why is that important? Well, it means you're never going to get a blurry line if you put a line on a whole pixel. Let's not get into sub-pixel rendering, because that's a whole different ball game. Let's go from here on out and assume 8px is the best, and people who use 10px in their designs should get replaced by AI, sooner than later.

Another handy part of using an 8px grid, or more specifically, moving onto Tailwind CSS default setup, is that we can very easily work out visual rhythm, do you want something to be double the distance from X to Y? Well you just double the number.

Why is visual rhythm important? Well, let me show you:

Image

This is Ling's cars. Funnily enough, this website didn't convert well because it lacks rhythm. The content looks as though somebody accidentally kicked over a LEGO box and I personally thought it was a joke website (and younger me nearly ended up leasing a car).

Here's what it looks like today

Image

Yes, boring, I know. However, there is a good reason it resembles all the other websites on the internet. It's because we're used to this rhythm. We start at the left and work our way across in a Z pattern.

One more part of understanding of visual rhytm is not screwing up the way you code page builders. Have you ever seen a conditional in JavaScript before? Well this little baby is going to save you from having varying spacing, and as a bonus, it's basically necessary if you don't want to break your preview with Next/Image.

{title && <h1 className="mb-8">{title}</h1>}

With the above, if you don't have a title. You won't have the space and the margin bottom where that <h1> would have ultimately been. This is especially important when working with live previews. So heed my advice - most, if not all, of your string content should be rendered in a similar manner to the above code.

One last thing for those of you old enough to have remembered the glory days of using Bootstrap: base 8 works wonderfully with a 12-column grid. Now, if you use a 12-column grid in this day and age, there's something very wrong with your layouts... But at least it's nice to share that blast from the past.

Never, ever, allow user-defined padding

Want to know the best way to ruin my day? User-defined padding. If you so much as utter this word, I feel the vein in my forehead ready to burst

Image

Why would somebody be so upset over pixels you might ask? Well it's probably because once you add this god-foresaken functionality into your page builder, you will never know the concept of consistency ever again.

It all starts with something looking slightly off in a pagebuilder layout, and quickly spirals out of control until everything is 32px on the bottom, 72px on the top, and for some reason 12px on the right, overflowing.

Believe me when I tell you this: we've seen folks change their website design three times. First with a colorful, brand-focused look, then with mostly white backgrounds, and finally with dark backgrounds.

As you might imagine, by the time it was finished, there was black and white everywhere, with occasional pops of red... Like a penguin in a traffic accident. Guess what the next question was to resolve this?

Can we add padding to our page builders?

But we really want user-defined padding

So you ignored everything I said above and still wanted it anyway. That's fine, that's ok. I'll spell it out passive-aggressively for this next paragraph.

If you absolutely have to do this you will want to ensure you're using pre-defined strings... never number inputs.

Here's the Sanity side of things

worst-schema-ever.tsx
// schemaTypes/blocks/standard-block.ts
import { defineType } from 'sanity';

export default defineType({
  name: 'standardBlock',
  title: 'Standard Content Block',
  type: 'object',
  fields: [
    {
      name: 'title',
      title: 'Title',
      type: 'string',
      description: 'Optional heading for the content block',
    },
    {
      name: 'paddingSize',
      title: 'Vertical Padding',
      type: 'string',
      options: {
        list: [
          { title: 'None (0px)', value: 'none' },
          { title: 'Small (16px)', value: 'small' },
          { title: 'Medium (32px)', value: 'medium' },
          { title: 'Large (48px)', value: 'large' },
          { title: 'Extra Large (64px)', value: 'xlarge' },
          { title: 'Page Builder (128px)', value: 'pagebuilder' },
        ],
        layout: 'radio',
      },
      initialValue: 'pagebuilder',
      description: 'Choose the vertical padding for this content block',
    },
    // ... other fields (content, images, etc.)
  ],
  preview: {
    select: {
      title: 'title',
      paddingSize: 'paddingSize',
    },
    prepare({ title, paddingSize }) {
      const subtitle = paddingSize 
        ? `Padding: ${paddingSize}` 
        : 'Default padding';
      
      return {
        title: title || 'Standard Content Block',
        subtitle,
      };
    },
  },
});

And here's the component side of things

seriously-dont-do-this.tsx
// Padding mapping for user-defined values
const paddingMap = {
  'none': 'py-0',
  'small': 'py-4',   // 16px
  'medium': 'py-8',  // 32px
  'large': 'py-12',  // 48px
  'xlarge': 'py-16', // 64px
  'pagebuilder': 'py-pagebuilder', // 8rem (custom)
} as const;

type PaddingSize = keyof typeof paddingMap;

interface StandardBlockProps {
  title?: string | null;
  paddingSize?: PaddingSize;
}

export function StandardBlock({ 
  title, 
  paddingSize = 'medium' // Default to 32px
}: StandardBlockProps) {
  const paddingClass = paddingMap[paddingSize] || paddingMap.medium;

  return (
    <section>
      <div className={`container ${paddingClass}`}>
        {title && (
          <h2 className="mb-8 text-center text-2xl font-medium">
            {title}
          </h2>
        )}
        
        {/* Your content goes here */}
        <div>
          Content placeholder
        </div>
      </div>
    </section>
  );
}

(or you'd never know this pain if you just used our Sanity Services)

So how do I do it correctly?

You'd be wrong in thinking we haven't already looked into finding the best solution for this. We've spent days and weeks testing this across a range of projects, and the simplest way of making sure this never happens is to simply add a pagebuilder value inside of Tailwind CSS and never break convention.

Here's how you can do it for Tailwind CSS v3

tailwind.config.ts
import type { Config } from "tailwindcss";

const config = {
  content: [
    // Add your content paths here
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      container: {
        center: true,
        padding: "2rem",
        screens: {
          "2xl": "1920px",
        },
      },
      spacing: {
        pagebuilder: "8rem",
      },
    },
  },
  plugins: [],
} satisfies Config;

export default config;

Or if you're using the latest Tailwind CSS v4

app.css
@import "tailwindcss";

@theme {
  --breakpoint-2xl: 1920px;
  --spacing-pagebuilder: 8rem;
}

@utility container {
  margin-inline: auto;
  padding-inline: 2rem;
}

So now you've got this, how do we use this for a page-builder block? Well, it's pretty simple. Here's how you would apply it to a regular page builder block.

standard-block.tsx
import { PortableText } from '@portabletext/react';

export function StandardBlock({ 
  title, 
  content // This should be portable text content from Sanity
}) {
  return (
    <section>
      <div className="container py-pagebuilder">
        {title && (
          <h2 className="mb-8 text-center text-2xl font-medium">
            {title}
          </h2>
        )}
        
        {content && (
          <div className="prose prose-lg mx-auto">
            <PortableText value={content} />
          </div>
        )}
      </div>
    </section>
  );
}

But as with anything padding-related, there's always a curveball. How do we handle page builder blocks when there's a background image, because then it's going to be flush to the block next to it? Well, that's nice and easy, we double the number of py-pagebuilder classes and add an additional div.

bg-image-block.tsx
import { PortableText } from '@portabletext/react';

export function BackgroundImageBlock({ 
  title, 
  content, // This should be portable text content from Sanity
  backgroundImage 
}) {
  return (
    <section 
      className="my-pagebuilder"
      style={{
        backgroundImage: backgroundImage ? `url(${backgroundImage})` : undefined,
        backgroundSize: "cover",
        backgroundPosition: "center",
      }}
    >
      {/* Inner container with its own pagebuilder spacing */}
      <div className="container py-pagebuilder bg-white/90 backdrop-blur-sm rounded-lg">
        {title && (
          <h2 className="mb-8 text-center text-2xl font-medium">
            {title}
          </h2>
        )}
        
        {content && (
          <div className="prose prose-lg mx-auto">
            <PortableText value={content} />
          </div>
        )}
      </div>
    </section>
  );
}

Again, you'll probably want to use py-pagebuilder or my-pagebuilder, depending on how you want to display your background image. You can figure that bit out; we're not going to spoon-feed you the whole way, or we wouldn't be in business.

Wait, the client just said the blocks are too close together

Thank goodness, we built this whole spacing the way we did. Otherwise, we'd probably have to do a monster find-and-replace and break something as part of the process.

Because we set a global variant for this page-builder spacing, we can simply go ahead and change one value, and it will permeate throughout the entire project.

app.css
@import "tailwindcss";

@theme {
  --spacing-pagebuilder: 8rem;
  --spacing-pagebuilder: 6rem; // client says too big
  --spacing-pagebuilder: 12rem; // client says too small
  *{
    display: none; // client says make it pop
  }
}

What if I already screwed this up and built my project

Oh, didn't you know? We inherit way over half of the projects we maintain. Why do you think I put so much vitriol into these blogs? It's the only way I can vent after seeing the average agencies' code quality. Let me give you the quick-fire process with the help of AI.

Basically, you have to be careful of context windows, because that's what you'll be battling with as we have to migrate 20+ page builder blocks. So how do we handle that?

Use a markdown file to keep your AI agent in check.

migration.mdc
---
description: Whenever a user is migrating a large chunk of content from one format to another
alwaysApply: false
---

Create a _migration.mdx file at the top of each respective folder, the content should be as follows:

# Schema Migration Checklist

This file tracks the migration status of schema files from the old Sanity structure to the new structure.

## Migration Rules

- Make sure the outermost container in each of these components is a <section>
- Make sure each `section` has the className <section className="py-pagebuilder"/>
- If there's a an image background, or absolutely positioned image inside of a relative container make sure there's two containers to ensure there's the correct amount of padding from the next respective page builder block. See example below.

```
<section className="container py-pagebuilder">
  <div className="py-pagebuilder relative">
    <Image fill .../>
  </div>
</section>

## Migration Status

List migration items in alphabetical order as a checklist

## Flag any irregularities or exceptions to the rule

If you come across pattern breaking files, flag them underneath a warning heading beneath and require user confirmation to make changes

## Notes

If you have any additional notes leave them here, otherwise remove heading above and leave off

Remember to read through the migration script to ensure you haven't missed anything you want to achieve. The one above is a simple template and needs a little tweaking specific to your project.

Closing notes

The reason we get so flabbergasted by this kind of issue is that it's so avoidable, and effectively the only person that loses is the client, if you build it incorrectly. Oh, and obviously us, when we inherit it and have to fix it.

That being said, we want you to succeed, so if you're looking at building a solid page builder, we always advise the same thing. Take our Turbo Start Sanity template, and read an opinionated guide to Sanity studio. You'll get more than enough information about how to structure everything. If you're still struggling, we also wrote a whole Sanity learn course to help with page builders.

If you'd rather just get it done perfectly the first time, you could always just pay us to build it. I heard Roboto Studio is pretty good at building Sanity websites or something. Drop us a message below.


Get in touch

Book a meeting with us to discuss how we can help or fill out a form to get in touch