Grow your SaaS organically with Content Marketing.
Try for free →This guide walks through how to create a landing page for a launched SaaS app using React and Typescript.
Chakra UI is a UI component library that is very easy to use, has accessibility out of the box, and most importantly, looks incredible.
In this guide, we will be creating a landing page for a SaaS product with:
npm create vite@latest saasbase-chakraui-landing-page -- --template react-ts
cd saasbase-chakraui-landing-page
npm install
npm run dev
Chakra UI is a fantastic UI kit that comes with easy-to-use Front-end components that also look good.
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/icons
We can set it up by wrapping our entire application with the ChakraUIProvider
.
Install the Inter font with:
npm i @fontsource/inter
Edit src/main.tsx
:
import {
ChakraProvider,
extendTheme
} from "@chakra-ui/react";
import React from "react";
import ReactDOM from "react-dom/client";
import {
App
} from "./App";
const theme = extendTheme({
colors: {
brand: {
50: "#f0e4ff",
100: "#cbb2ff",
200: "#a480ff",
300: "#7a4dff",
400: "#641bfe",
500: "#5a01e5",
600: "#5200b3",
700: "#430081",
800: "#2d004f",
900: "#14001f",
},
},
fonts: {
heading: `'Inter', sans-serif`,
body: `'Inter', sans-serif`,
},
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<App />
</ChakraProvider>
</React.StrictMode>
);
The hero section of your landing page is the most important above-the-fold item you can have. The title and the description should hook the reader in to learn more about your offering within 4-5 secs.
Our Hero section will have a title, description, and a strong CTA to get the readers salivating. A trust builder that shows that others are already using your product goes a long way.
Create a new file
src/components/HeroSection.tsx
:
import {
Button,
Center,
Container,
Heading,
Text,
VStack,
} from "@chakra-ui/react";
import { FunctionComponent } from "react";
interface HeroSectionProps {}
export const HeroSection: FunctionComponent<HeroSectionProps> = () => {
return (
<Container maxW="container.lg">
<Center p={4} minHeight="70vh">
<VStack>
<Container maxW="container.md" textAlign="center">
<Heading size="2xl" mb={4} color="gray.700">
You don't have to chase your clients around to get paid
</Heading>
<Text fontSize="xl" color="gray.500">
Freelancers use Biller to accept payments and send invoices to
clients with a single click
</Text>
<Button
mt={8}
colorScheme="brand"
onClick={() => {
window.open("<https://launchman.com>", "_blank");
}}
>
I need this for $10/month →
</Button>
<Text my={2} fontSize="sm" color="gray.500">
102+ builders have signed up in the last 30 days
</Text>
</Container>
</VStack>
</Center>
</Container>
);
};
Update the src/App.tsx
with:
import { Box } from "@chakra-ui/react";
import { HeroSection } from "./components/HeroSection";
export const App = () => {
return (
<Box bg="gray.50">
<HeroSection />
</Box>
);
};
We will continue to add new components to this page as we build them out.
Now that we have some content in the page, we can add a header/navigation bar. This will show the name of our product, quick links for navigating to parts of the page, and a link to my Twitter.
To make it truly responsive, we need to make it so that it collapses on smaller devices and the navigation goes into a side drawer like so:
Download the SVG image of the Twitter Icon from here and place it in the public
folder. Any file in this folder is served as is. This is where you want to keep all the static assets your site uses.
Let's start by creating a new component that will create a responsive header bar. Call it src/components/Header.tsx
:
import { HamburgerIcon } from "@chakra-ui/icons";
import {
Box,
chakra,
Container,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
Flex,
Heading,
IconButton,
Image,
Link,
LinkBox,
LinkOverlay,
Spacer,
Stack,
useDisclosure,
} from "@chakra-ui/react";
const navLinks = [
{ name: "Home", link: "/" },
{ name: "Features", link: "#features" },
{ name: "Pricing", link: "#pricing" },
];
const DesktopSidebarContents = ({ name }: any) => {
return (
<Container maxW={["full", "container.lg"]} p={0}>
<Stack
justify="space-between"
p={[0, 4]}
w="full"
direction={["column", "row"]}
>
<Box display={{ base: "none", md: "flex" }}>
<Heading fontSize="xl">{name}</Heading>
</Box>
<Spacer />
<Stack
align="flex-start"
spacing={[4, 10]}
direction={["column", "row"]}
>
{navLinks.map((navLink: any, i: number) => {
return (
<Link
href={navLink.link}
key={`navlink_${i}`}
fontWeight={500}
variant="ghost"
>
{navLink.name}
</Link>
);
})}
</Stack>
<Spacer />
<LinkBox>
<LinkOverlay href={`https://twitter.com/thisissukh_`} isExternal>
<Image src="twitter.svg"></Image>
</LinkOverlay>
</LinkBox>
</Stack>
</Container>
);
};
const MobileSidebar = ({ name }: any) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Flex w="full" align="center">
<Heading fontSize="xl">{name}</Heading>
<Spacer />
<IconButton
aria-label="Search database"
icon={<HamburgerIcon />}
onClick={onOpen}
/>
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="xs">
<DrawerOverlay />
<DrawerContent bg="gray.50">
<DrawerCloseButton />
<DrawerHeader>{name}</DrawerHeader>
<DrawerBody>
<DesktopSidebarContents />
</DrawerBody>
</DrawerContent>
</Drawer>
</Flex>
</>
);
};
interface SidebarProps {
name: string;
}
const Sidebar = ({ name }: SidebarProps) => {
return (
<chakra.header id="header">
<Box display={{ base: "flex", md: "none" }} p={4}>
<MobileSidebar name={name} />
</Box>
<Box display={{ base: "none", md: "flex" }} bg="gray.50">
<DesktopSidebarContents name={name} />
</Box>
</chakra.header>
);
};
interface HeaderProps {
name: string;
}
export const Header = ({ name }: HeaderProps) => {
return (
<Box w="full">
<Sidebar name={name} />
</Box>
);
};
Add a src/components/Layout.tsx
:
import { Box, VStack } from "@chakra-ui/react";
import { FunctionComponent } from "react";
import { Header } from "./Header";
interface LayoutProps {
children: React.ReactNode;
}
export const Layout: FunctionComponent<LayoutProps> = ({
children,
}: LayoutProps) => {
return (
<Box bg="gray.50">
<VStack spacing={10} w="full" align="center">
<Header name="Biller" />
{children}
</VStack>
</Box>
);
};
Update the src/App.tsx
with:
import { Box } from "@chakra-ui/react";
import { HeroSection } from "./components/HeroSection";
import { Layout } from "./components/Layout";
export const App = () => {
return (
<Layout>
<Box bg="gray.50">
<HeroSection />
</Box>
</Layout>
);
};
A demo video lets the reader know what to expect when they do signup for your product.
Record an MP4 video of your product and place it in the public
folder as video.mp4
. Next.js serves everything from the public
folder as is.
If you don't have one,
and a
you can use.
We can also add a
poster
option in the
video
tag that shows a static image if the video isn't yet loaded. Using a static frame of the video as the poster works well.
Update src/App.tsx
with:
import { Box } from "@chakra-ui/react";
import { HeroSection } from "./components/HeroSection";
import { Layout } from "./components/Layout";
export const App = () => {
return (
<Layout>
<Box bg="gray.50">
<HeroSection />
<Container maxW="container.xl">
<Center p={[0, 10]}>
<video playsInline autoPlay muted poster="/image.png" loop>
<source src="/video.mp4" type="video/mp4" />
</video>
</Center>
</Container>
</Box>
</Layout>
);
};
Now that the reader knows what the offering is, you can answer their next logical question - who else is using it?
Adding logos of customers at recognizable companies using your product will build trust with the reader.
Download the SVG logos for
and
and place them in
public
folder just like before.
Update the
src/App.tsx
with:
import {
Box,
Center,
Container,
Wrap,
WrapItem,
Text,
Image,
} from "@chakra-ui/react";
// ...
export const App = () => {
return (
<Layout>
<Box bg="gray.50">
// ...
<Container maxW="container.2xl" centerContent py={[20]}>
<Text color="gray.600" fontSize="lg">
Used by teams worldwide
</Text>
<Wrap
spacing={[10, 20]}
mt={8}
align="center"
justify="center"
w="full"
>
<WrapItem>
<Image src="microsoft-logo.svg" alt="Microsoft logo" />
</WrapItem>
<WrapItem>
<Image src="adobe-logo.svg" alt="Adobe logo" />
</WrapItem>
<WrapItem>
<Image src="microsoft-logo.svg" alt="Microsoft logo" />
</WrapItem>
<WrapItem>
<Image src="adobe-logo.svg" alt="Adobe logo" />
</WrapItem>
</Wrap>
</Container>
</Box>
</Layout>
);
};
The features section is where you can flaunt the top 3 ways your product will help a potential user. I like to be as direct as possible.
Create a new file called
src/components/Feature.tsx
import {
Box,
Button,
Center,
Container,
Stack,
Text,
VStack,
Image,
} from "@chakra-ui/react";
import { FunctionComponent } from "react";
interface FeatureProps {
title: string;
description: string;
image: string;
reverse?: boolean;
}
export const Feature: FunctionComponent<FeatureProps> = ({
title,
description,
image,
reverse,
}: FeatureProps) => {
const rowDirection = reverse ? "row-reverse" : "row";
return (
<Center w="full" minH={[null, "90vh"]}>
<Container maxW="container.xl" rounded="lg">
<Stack
spacing={[4, 16]}
alignItems="center"
direction={["column", null, rowDirection]}
w="full"
h="full"
>
<Box rounded="lg">
<Image
src={image}
width={684}
height={433}
alt={`Feature: ${title}`}
/>
</Box>
<VStack maxW={500} spacing={4} align={["center", "flex-start"]}>
<Box>
<Text fontSize="3xl" fontWeight={600} align={["center", "left"]}>
{title}
</Text>
</Box>
<Text fontSize="md" color="gray.500" textAlign={["center", "left"]}>
{description}
</Text>
<Button
colorScheme="brand"
variant="link"
textAlign={["center", "left"]}
>
Learn more →
</Button>
</VStack>
</Stack>
</Container>
</Center>
);
};
Add to src/App.tsx
:
import { Feature } from "./components/Feature";
// ...
interface FeatureType {
title: string;
description: string;
image: string;
}
const features: FeatureType[] = [
{
title: "Detailed Analytics",
description:
"No more spending hours writing formulas in Excel to figure out how much you're making. We surface important metrics to keep your business going strong.",
image:
"<https://launchman-space.nyc3.digitaloceanspaces.com/chakra-ui-landing-page-feature-1.png>",
},
{
title: "Track your clients",
description:
"Know when and how your projects are going so you can stay on top of delivery dates.",
image:
"<https://launchman-space.nyc3.digitaloceanspaces.com/chakra-ui-landing-page-feature-2.png>",
},
{
title: "Manage projects",
description:
"You don't have to hunt your email inbox to find that one conversation. Every task, project, and client information is just a click away.",
image:
"<https://launchman-space.nyc3.digitaloceanspaces.com/chakra-ui-landing-page-feature-3.png>",
},
];
export const App = () => {
return (
<Layout>
<Box bg="gray.50">
// ...
<VStack
backgroundColor="white"
w="full"
id="features"
spacing={16}
py={[16, 0]}
>
{features.map(
({ title, description, image }: FeatureType, i: number) => {
return (
<Feature
key={`feature_${i}`}
title={title}
description={description}
image={image}
reverse={i % 2 === 1}
/>
);
}
)}
</VStack>
</Box>
</Layout>
);
};
Add a new file called Highlight.tsx
import {
Box,
Center,
Container,
Wrap,
WrapItem,
Text,
Image,
VStack,
SimpleGrid,
} from "@chakra-ui/react";
// ...
export interface HighlightType {
icon: string;
title: string;
description: string;
}
const highlights: HighlightType[] = [
{
icon: "✨",
title: "No-code",
description:
"We are No-Code friendly. There is no coding required to get started. Launchman connects with Airtable and lets you generate a new page per row. It's just that easy!",
},
{
icon: "🎉",
title: "Make Google happy",
description:
"We render all our pages server-side; when Google's robots come to index your site, the page does not have to wait for JS to be fetched. This helps you get ranked higher.",
},
{
icon: "😃",
title: "Rapid experimenting",
description:
"You don't have to wait hours to update your hard-coded landing pages. Figure out what resonates with your customers the most and update the copy in seconds",
},
{
icon: "🔌",
title: "Rapid experimenting",
description:
"You don't have to wait hours to update your hard-coded landing pages. Figure out what resonates with your customers the most and update the copy in seconds",
},
];
export const App = () => {
return (
<Box bg="gray.50">
// ...
<Container maxW="container.md" centerContent py={[8, 28]}>
<SimpleGrid spacingX={10} spacingY={20} minChildWidth="300px">
{highlights.map(({ title, description, icon }, i: number) => (
<Box p={4} rounded="md" key={`highlight_${i}`}>
<Text fontSize="4xl">{icon}</Text>
<Text fontWeight={500}>{title}</Text>
<Text color="gray.500" mt={4}>
{description}
</Text>
</Box>
))}
</SimpleGrid>
</Container>
</Box>
);
};
Here you can add a pricing section to your page by clearly showing what features are available at what price point. Offering an Annual subscription can be beneficial for both yourself and the customer.
Create a new component in
src/components/PricingSection.tsx
import { CheckCircleIcon } from "@chakra-ui/icons";
import {
Box,
Button,
ButtonGroup,
HStack,
List,
ListIcon,
ListItem,
SimpleGrid,
Text,
VStack,
} from "@chakra-ui/react";
import { FunctionComponent, useState } from "react";
interface PricingBoxProps {
pro: boolean;
name: string;
isBilledAnnually: boolean;
}
export const PricingBox: FunctionComponent<PricingBoxProps> = ({
pro,
name,
isBilledAnnually,
}: PricingBoxProps) => {
return (
<Box
boxShadow="sm"
p={6}
rounded="lg"
bg={pro ? "white" : "white"}
borderColor={pro ? "brand.500" : "gray.200"}
backgroundColor={pro ? "brand.50" : "white"}
borderWidth={2}
>
<VStack spacing={3} align="flex-start">
<Text fontWeight={600} casing="uppercase" fontSize="sm">
{name}
</Text>
<Box w="full">
{isBilledAnnually ? (
<Text fontSize="3xl" fontWeight="medium">
$89
</Text>
) : (
<Text fontSize="3xl" fontWeight="medium">
$99
</Text>
)}
<Text fontSize="sm">per month per site</Text>
</Box>
<Text>Unlock key features and higher usage limits</Text>
<VStack>
<Button size="sm" colorScheme="brand">
Free 14-day trial →
</Button>
</VStack>
<VStack pt={8} spacing={4} align="flex-start">
<Text fontWeight="medium">Everything in Basic, plus:</Text>
<List spacing={3}>
<ListItem>
<HStack align="flex-start" spacing={1}>
<ListIcon as={CheckCircleIcon} color="brand.500" mt={1} />
<Text>
Lorem ipsum dolor sit amet, consectetur adipisicing elit
</Text>
</HStack>
</ListItem>
</List>
</VStack>
</VStack>
</Box>
);
};
interface PricingSectionProps {}
export const PricingSection: FunctionComponent<PricingSectionProps> = () => {
const [isBilledAnnually, setIsBilledAnnually] = useState < boolean > true;
return (
<VStack spacing={10} align="center">
<ButtonGroup isAttached>
<Button
onClick={() => {
setIsBilledAnnually(true);
}}
colorScheme={isBilledAnnually ? "brand" : "gray"}
>
Annually (-10%)
</Button>
<Button
onClick={() => {
setIsBilledAnnually(false);
}}
colorScheme={isBilledAnnually ? "gray" : "brand"}
>
Monthly
</Button>
</ButtonGroup>
<SimpleGrid columns={[1, null, 3]} spacing={10}>
<PricingBox
pro={false}
name="Starter"
isBilledAnnually={isBilledAnnually}
/>
<PricingBox
pro={true}
name="Creator"
isBilledAnnually={isBilledAnnually}
/>
<PricingBox
pro={false}
name="Enterprise"
isBilledAnnually={isBilledAnnually}
/>
</SimpleGrid>
</VStack>
);
};
Add the component to src/App.tsx
import { PricingSection } from "./components/PricingSection";
// ...
export const App = () => {
return (
<Box bg="gray.50">
// ...
<Container py={28} maxW="container.lg" w="full" id="pricing">
<PricingSection />
</Container>
</Box>
);
};
I'm building a new SaaS to automate content marketing for your SaaS
Tools for SaaS Devs