How to build a website builder

Andy Zhang
11 min readSep 5, 2023

--

A website for building websites. Sounds easy, right?

It sure is if scoped right and if you have a great guide to break it down for you. Luckily, you’re reading a guide to build just that written by someone who built a website builder from scratch. By the end of this tutorial, you’ll be able to build a simple website builder (demo, code). You’ll also learn how to think through decisions in the context of building a product.

MVP Requirements

Let’s break down the use cases of our website builder MVP:

  • A user can view a read-only version of the website they built
  • A user can add new sections to their website
  • A user can edit the text content of their website
  • A user can view which mode they’re in (editing vs read-only)
  • A user can toggle between read-only and edit modes for their website

There’s a million more ways to enrich our website builder but let’s stick with the basics. It should take 20 minutes and will require beginner knowledge in JavaScript, React and CSS.

Out of scope

The danger in product development is trying to build too much and not launching early enough. Let’s define what will be cut out of scope from our MVP so that you can finish this tutorial in 20 minutes:

  • Styling the content
  • Server-side component
  • Database and data storage
  • Mobile-friendly

The neat thing about this MVP is that it’ll be easy to deploy the MVP live as a client-side app. In other words, launch first then iterate later.

1. Client-side design

For the client-side app, keep it simple and organized. You’ll have a Website component that takes in two parameters: mode and websiteData. The mode will define whether the website is being read or edited. The websiteData will define the content of the website.

One observation is that websiteData and mode are stored at state in the Root component and passed to Website component instead of being stored as a state within Website component. This type of pattern is called a controlled component. While the uncontrolled component option is possible, the Website component’s only focus should be to render the website and the Root should have the ability to change the data such as the mode.

Below is some skeleton code to set up the client-side application. Note that the code below will be in JavaScript but the linked source code is in TypeScript.

// page.tsx
const Root = (props) => {
// Store the two states `websiteData` and `mode`
const [websiteData, setWebsiteData] = useState([{ text: "Hello World" }]);
const [mode, setMode] = useState("edit");

// Return the `Website` component with the data it needs to render
return (
<Website websiteData={websiteData} mode={mode} />
);
}

// website.tsx
const Website (props) => {
// Props that'll determine how this component should be rendered
const { mode, websiteData } = props

// `sections` will contain a list of HTML elements with text
const sections = []
for (const section of websiteData) {
// For each section in our data, add it to the list `sections`
sections.push(
<div className="p-8">
{section.text}
</div>
);
}

return sections
}

The website is being stored as a list of objects where each object contains the text property containing the text of the section.

You can run this application on your computer by cloning this GitHub repository and then going through the README instructions.

Demo: https://website-builder-demo.vercel.app/1-setup

Source code: https://github.com/andyzg/WebsiteBuilderDemo/tree/main/src/app/1-setup

2. Website editing UI

If you want to make your website editable, two questions need answering:

  • How does a user add a new section?
  • How does a user edit a section’s content?

One of the constraints of a website builder is that the dimensions of the website should not be modified in the website builder. For example, if you add a button that said “Add section” between each section, the height of the entire website while in edit mode will be different than in read-only mode.

To overcome this constraint, add a button that hovers over the content itself without affecting the section’s dimensions. To build this, create a separate element that becomes visible upon hovering over the section.

Adding an “Add section” button

The secret sauce lies in the CSS and the position attribute. An element’s position is set to relative to define a new stack context. If that element contains a child with the position: absolute attribute, that child will be in the context of its parents’ stack. In other words, if you set left: 0 on the absolute element, it won’t hug the left side of the window but instead, the left boundary of the relative element.

How does this concept apply to our use case? What you can do is make hovering buttons at the top and bottom of each section for adding new sections. Because they’re hovering, they won’t affect the dimensions of the website. This can be accomplished by abstracting all editing tools into a Toolbar component and then creating two buttons that hug the top and bottom of the section. The example below uses TailwindCSS to apply styling.

function Website(props) {
const { mode } = props;

// Use `editing` variable to easily check if we're in edit mode
const editing = mode === "edit";
// ...

// ...
// Each section now has a content element as well as a toolbar
// One key change here is that the section's position is now set as relative
sections.push(
<section className={"relative group"}>
<Toolbar editing={editing} />
<div className="p-8">
{section.text}
</div>
</section>
);
// ...
}

function Toolbar(props) {
const { editing } = props;

// If we're not editing, then don't show any editing tools
if (!editing) {
return null;
}

// The toolbar is transparent by default (opacity-0)
// If the cursor hovers over the section, then it becomes opaque (group-hover:opacity-100)
return (
<div className={"opacity-0 group-hover:opacity-100 transition-opacity"}>
<AddSection className="bottom-0" />
<AddSection className="top-0" />
</div>
);
}

function AddSection(props) {
const { className = "" } = props;

// We're storing the classes in an array so that it's easier to read
// by grouping the classes by function.
const classes = [
// Applying the absolute position attribute to the buttons
// `absolute` sets the position as absolute
// `left-1/2` adds `left: 50%`
// `-translate-x-1/2` adds `transform: translate(-50%, 0)`
`absolute left-1/2 -translate-x-1/2`, // Positioning
`bg-blue-400 text-white rounded-sm cursor-pointer `, // Cosmetic styling
`flex justify-center`, // Layout
`w-6 h-6`, // Size
className // Additional classes
]

return (
<div className={classes.join(" ")}>
+
</div>
);
}

The most important line in the code above is this one:

`absolute left-1/2 -translate-x-1/2`,

These are classes provided by TailwindCSS that essentially create the below behavior and add the CSS documented on the right:

The TailwindCSS classes translate to the CSS properties on the right

If we open our app, we now get this:

A section with two hovering “Add Section” buttons

Now we have a section that’s editable with buttons to add new sections above and below! Clicking on the buttons won’t do anything yet but they will in the next section. The hardest part of this guide is done now, time to take this to the finish line!

Demo: https://website-builder-demo.vercel.app/2-section

Source code: https://github.com/andyzg/WebsiteBuilderDemo/tree/main/src/app/2-section

3. Enabling website edits

To enable website edits, two changes need to be added:

  • When a user clicks on the “+” button, a new section needs to be added
  • When a user selects a section, the section’s text should become editable

Adding a section

To enable this, attach an onClick event on both “+” buttons and modify the website’s structure. Do this by passing down a onAddSection() function that adds a new section to the website when a button is clicked.

There are many ways this can be approached: you can pass down the setWebsiteData function to the button and allow it to be responsible for updating the website data. You can also create abstractions so that each component needs to worry about only one responsibility:

  • AddSection: Am I being clicked or not?
  • Toolbar: Where should the new section be added? Above or below?
  • Section: Pass the callbacks to the children.
  • Website: When a new section is added, how is the website’s data structure modified?

Separating the concerns and ensuring that each component only has to worry about one thing will make it much easier to manage bugs in the future. For example, if there were many components including AddSection that were responsible for modifying the entire website and a bug was introduced, it would be hard to narrow down which component is causing the bug. Modularizing and minimizing the responsibilities of each component ensures that the code is as clean and organized as possible and improves testability.

Editing section text

One of the original constraints is to ensure that the website being edited has the same dimensions as the website that’s read-only. To ensure that the dimensions stay the same, use the same components if possible. In other words, if the read-only section is displayed with a div, let’s edit the content within the same div.

function Section(props) {
const { editing } = props;

// ...
return (
// [section code]
<div contentEditable={editing} onInput={onSectionTextChange} />
// [more section code]
);
}

Debugging React behavior

Upon testing this, you’ll run into a bug. Every time the content is edited, the cursor moves back to the beginning. To solve this, we do a quick search, decipher a highly upvoted answer into code, and test it again.

When building MVPs, it’s easy to get caught up in the details of the work or of a bug but since the goal is to build an MVP in 20 minutes, a solution that solves the problem is all we need.

function Section(props) {
// ...
const { onSectionTextChange } = props;
const ref = useRef(null); // reference to the editable div

// The below useEffect fixes the bug
useEffect(() => {
// Whenever the state is updated, update the div's text
if (ref.current) {
ref.current.textContent = sectionData.text;
}
// Run the useEffect whenever the section's text changes
}, [sectionData.text]);

return (
// [section code]
// Note that we removed the child contents of the <div>. Instead,
// we set the div content in the useEffect above
<div
ref={ref}
suppressContentEditableWarning={true}
onInput={onSectionTextChange}
contentEditable={editing}
className="p-8"
/>
// [more section code]
);
}

function Website(props) {
// ...

const onSectionTextChange = (sectionIndex, e) => {
// Copy the immutable state.
// JSON.parse(JSON.stringify(...)) is an easy native way to deep clone
// an immutable object. In practice, use a library or be mindful of
// how you update state. Read more here:
// https://legacy.reactjs.org/docs/state-and-lifecycle.html#do-not-modify-state-directly
const newWebsiteData = JSON.parse(JSON.stringify(websiteData));

// Set the new value
newWebsiteData[sectionIndex].text = e.target.innerText;
setWebsiteData(newWebsiteData);
}

const onAddSection = (newSectionIndex) => {
const newWebsiteData = [...websiteData];
newWebsiteData.splice(newSectionIndex, 0, { text: "Hello World" });

setWebsiteData(newWebsiteData);
};

for (let index = 0; index < websiteData.length; index += 1) {
sections.push(
<Section
key={index}
sectionData={sectionData}
onSectionTextChange={onSectionTextChange.bind(this, index)}
onAddSection={onAddSection.bind(this, index)}
editing={editing} />
);
}
// ...
}

With that bug fixed, you now have a website editor that allows us to edit the website’s text and add new sections. You’re 90% there!

Demo: https://website-builder-demo.vercel.app/3-edit

Source code: https://github.com/andyzg/WebsiteBuilderDemo/tree/main/src/app/3-edit

4. Previewing our website

The website is always in edit mode because there isn’t a way to toggle the mode between read-only and edit. For this last step, add a fixed element at the bottom of the window that will allow the website builder to toggle between edit and read-only mode.

The reason the fixed toggle bar is fixed at the bottom is because the other best options lead to a suboptimal user experience:

  • If it has a fixed position at the top of the window, then it will block part of the first section because it is hovering over the website sections.
  • If it has absolute positioning at the top, then it will not be visible the moment a user scrolls a little bit.
  • If it’s positioned at the bottom of the window, the user is guaranteed to be able to view the top of the page and they can easily reference it at any scroll position.

Add two elements to the new footer: text indicating whether the website is in edit or read-only mode and a button to toggle the modes.

function Website(props) {
const { mode, setMode } = props;

// For now, we'll wrap the setMode function with onToggleMode.
// This will simplify the logic in the footer to only check if the
// button was clicked instead of figuring out what value to set next.
const onToggleMode = () => {
if (mode === "edit") {
setMode("read");
} else {
setMode("edit");
}
}

// ...
return (
<Fragment>
{sections}
<Footer onToggle={onToggleMode} mode={mode} />
</Fragment>
);
}

function Footer(props) {
const { mode, onToggle } = props;
const classes = [
"fixed bottom-0 left-0 right-0", // positioning
"h-16 p-4", // sizing
"border-t-2 bg-white", // cosmetics
"flex justify-between items-center", // layout
]

return (
<footer className={classes.join(" ")}>
{mode === "edit" ? (
<div>Edit mode</div>
) : null}
{mode === "read" ? (
<div>Read-only mode</div>
) : null}
<button onClick={onToggle} className="bg-slate-600 text-white text-sm rounded-md p-2">Toggle mode</button>
</footer>
);
}

And with that, you’ve crossed the finish line! You’ve successfully built an MVP of a website builder! Your website builder is able to preview a website, modify a website’s text and sections, and toggle between the two modes.

Demo: https://website-builder-demo.vercel.app/4-mode

Source code: https://github.com/andyzg/WebsiteBuilderDemo/tree/main/src/app/4-mode

Next steps

If you want to go the extra mile and try a challenge, here are some other features you can try building:

  • Color the section: Add a toolbar to each section enabling a user to set a background color and/or text color for the section.
  • Saving the data: Once a user has built their website, they can ideally persist the data. Integrate the application with a database.
  • Add columns: How can you have multiple columns of text in the same section? You’ll need to modify the data structure of the website as well as add additional UI elements to support adding a new column.
  • Deleting sections: Add a button that removes a section
  • Upgrade the editor: Instead of using a content editable div, you can integrate with a rich text editor library to allow richer formatting to the text.

Thank you for making it this far! If you liked this guide, share it with a friend who’s also learning front-end development and leave a comment!

--

--

Andy Zhang
Andy Zhang

Written by Andy Zhang

Made in Montréal. CTO at Jemi. Ex-Uber PM.

Responses (1)