Users interact with the web through different devices, from phones, and laptops to smartwatches and AR/VR headsets. As developers, we must ensure that the websites and web applications we create are not just performant and functional, but also responsive on all screen sizes.
Chakra UI is a simple, modular, and easily extensible component library made up of basic building blocks that enables us to build the front end of our web applications. Chakra UI is customizable, fully accessible, reusable, and easy to use. It also comes with useful hooks, like the useColorMode
hook, which we can use to add dark mode to our applications. Overall, Chakra UI comes packed with many incredible features that make it the right tool for the job.
In this article, we will learn how to build responsive components with Chakra UI and use that knowledge to build this dashboard application.
The focus of this article is not to introduce Chakra UI to the reader, but rather to show how to build responsive components with this great tool. While it is not strictly required, experience working with Chakra UI will be advantageous.
When it comes to writing responsive CSS, developers have the option of choosing between mobile-first and desktop-first approaches. Chakra UI takes the mobile-first approach using the @media(min-width)
media query.
Responsive styling in Chakra UI relies on breakpoints defined in the theme
object. Chakra UI’s theme
object ships with the following breakpoints by default:
//Breakpoints for responsive design { sm: "30em", md: "48em", lg: "62em", xl: "80em", "2xl": "96em", }
We can switch from the default breakpoints to breakpoints that fit our application’s specifications using the createBreakpoints
theme tool, like so:
import { createBreakpoints } from "@chakra-ui/theme-tools" const breakpoints = createBreakpoints({ sm: "320px", md: "768px", lg: "960px", xl: "1200px", })
While we can use createBreakpoints
to create custom breakpoints, it will be deprecated in the future. The Chakra UI team advises we define the breakpoints as an object in a custom theme object we create:
import { extendTheme } from "@chakra-ui/react"; const customeTheme = extendTheme({ colors: {}, fonts: {}, fontSizes: {}, breakpoints: { sm: "320px", md: "768px", lg: "960px", xl: "1200px", }, }); const theme = extendTheme({ customeTheme }); export default customeTheme;
Chakra UI provides two syntaxes for creating responsive styles: the array syntax, and the object syntax. These syntaxes abstract the complexities of writing media queries and provide a great developer experience when developing responsive components.
The following is an example of these syntaxes:
//unresponsive width styles <Box bg="red.200" w="400px"> This is a box </Box> //responsive width styles using the Array syntax <Box bg="red.200" w={[300, 400, 500]}> This is a box </Box> //responsive width style susing the Object syntax <Box bg="red.200" w={{ base: "300px", md: "400px", lg: "500px" }}> This is a box </Box>
For the array syntax, the width of the Box
translates to:
For the object syntax, the width of the Box
translates to:
We can see that aside from the difference in syntax, the array and object responsive style definitions perform the same function.
useMediaQuery
hookuseMediaQuery
is a custom hook used to help detect whether a single media query or multiple media queries individually match. It returns a boolean based on the media query we define.
Let’s see how the useMediaQuery
hook works:
import { useMediaQuery } from "@chakra-ui/react" function Home() { const [isMobile] = useMediaQuery("(max-width: 768px)") return ( <Text> {isMobile ? "This is a mobile device" : "This is a desktop device"} </Text> ) }
In the code snippet above, we define a max-width: 768px
media query and access the isMobile
boolean. Next, we conditionally render some text based on the value of isMobile
.
Now that we understand how to create responsive styles in Chakra UI and how the useMediaQuery
works, let’s start building our dashboard application.
For the dashboard, we will start by creating the layout. The dashboard layout consists of the Sidebar
and Header
components.
Let’s break down the functionality and components of the layout as seen here:
import Header from "@components/Header"; import Sidebar from "@components/Sidebar"; import { Box, Drawer, DrawerContent, useDisclosure } from "@chakra-ui/react"; export default function Layout({ children }) { const { isOpen, onOpen, onClose } = useDisclosure(); return ( <Box minH="100vh" bg="gray.100"> <Sidebar onClose={() => onClose} display={{ base: "none", md: "block" }} /> <Drawer autoFocus={false} isOpen={isOpen} placement="left" onClose={onClose} returnFocusOnClose={false} onOverlayClick={onClose} size="full" > <DrawerContent> <Sidebar onClose={onClose} /> </DrawerContent> </Drawer> {/* Header */} <Header onOpen={onOpen} /> <Box ml={{ base: 0, md: 60 }} p="4"> {children} </Box> </Box> ); }
There are a few things to notice from the demo about the Header
and Sidebar
components.
First, there is a hamburger menu that appears on the header on mobile. When the hamburger is clicked, the sidebar slides into view. Finally, when the sidebar slides into view, there is a “close” icon button that, when clicked, causes the sidebar to slide back out of view.
We can set up this functionality using Chakra UI’s useDisclosure
hook. As seen in the code snippet above, we access isOpen
, onOpen
, and onClose
from useDisclosure
.
Next, we pass the onOpen
function to Header
so we can use it in the hamburger menu. The hamburger will be visible only on mobile; we will see this when we develop the Header
component later in the article.
Now let’s break down the Sidebar
component. We see from the code above that Sidebar
is used twice.
The first Sidebar
is the desktop sidebar. We want this sidebar to show only on large screen devices, so we set its display to “none” on mobile and “block” on larger devices. We also pass the onClose
function to the sidebar so we can use it to close the sidebar on mobile.
The second Sidebar
is the mobile sidebar. We use Chakra UI’s Drawer
component to set this up. The sidebar will be a child of the drawer, and the drawer will only open when isOpen
is true
. isOpen
will be true only when the header’s hamburger is clicked.
With these steps, we have created the layout for the dashboard and made the sidebar responsive.
Another method we can use to create a responsive sidebar is to make use of the useMediaQuery
hook. With this method, we create a desktop and a mobile sidebar and and conditionally display them based on the current screen size, like so:
import { Box, Stack } from "@chakra-ui/layout"; import Header from "./navbar"; import DesktopSidebar from "./DesktopSidebar"; import MobileSidebar from "./DesktopSidebar"; import { useMediaQuery } from "@chakra-ui/media-query"; export default function Layout({ children }) { const [isSmallScreen] = useMediaQuery("(max-width: 768px)"); return ( <Box> <Header /> <Box> <Stack> {isSmallScreen ? <MobileSidebar /> : <DesktopSidebar />} {children} </Stack> </Box> </Box> ); }
Now that we have created the dashboard layout, let’s use it in our _app.js
file:
import Head from "next/head"; import { ChakraProvider } from "@chakra-ui/react"; import Layout from "@layout/index"; function MyApp({ Component, pageProps }) { return ( <> <Head> <title>Chakra UI Dashboard</title> </Head> <ChakraProvider> <Layout> <Component {...pageProps} /> </Layout> </ChakraProvider> </> ); } export default MyApp;
Header
componentThe Header
component consists of a logo, a UserProfile
component, and a hamburger menu which we will use to toggle the sidebar on mobile devices. UserProfile
consists of a user’s name, their avatar image, and their role. UserProfile
triggers a dropdown menu when clicked.
Let’s start with UserProfile
:
import { IconButton, Avatar, Box, Flex, HStack, VStack, Text, Menu, MenuButton, MenuDivider, MenuItem, MenuList, } from "@chakra-ui/react"; import { FiChevronDown, FiBell } from "react-icons/fi"; export default function UserProfile() { return ( <HStack spacing={{ base: "0", md: "6" }}> <IconButton size="lg" variant="ghost" aria-label="open menu" icon={<FiBell />} /> <Flex alignItems="center"> <Menu> <MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: "none" }} > <HStack spacing="4"> <Avatar size="md" src={ "https://images.unsplash.com/photo-1619946794135-5bc917a27793?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&s=b616b2c5b373a80ffc9636ba24f7a4a9" } /> <VStack display={{ base: "none", md: "flex" }} alignItems="flex-start" spacing="1px" ml="2" > <Text fontSize="lg">Ademola Jones</Text> <Text fontSize="md" color="gray.600"> Admin </Text> </VStack> <Box display={{ base: "none", md: "flex" }}> <FiChevronDown /> </Box> </HStack> </MenuButton> <MenuList fontSize="lg" bg="white" borderColor="gray.200"> <MenuItem>Profile</MenuItem> <MenuItem>Settings</MenuItem> <MenuItem>Billing</MenuItem> <MenuDivider /> <MenuItem>Sign out</MenuItem> </MenuList> </Menu> </Flex> </HStack> ); }
Here, we set the spacing of the HStack
component using the object syntax. We remove HStack
‘s spacing for small screen and mobile devices and set it to 6
for larger devices. Net, we set the display of the VStack
and Box
components to "none"
on small devices and to "flex"
on larger devices.
Now for the Header
:
import { IconButton, Flex, Text } from "@chakra-ui/react"; import { FiMenu } from "react-icons/fi"; import UserProfile from "./UserProfile"; export default function Header({ onOpen }) { return ( <Flex ml={{ base: 0, md: 60 }} px="4" position="sticky" top="0" height="20" zIndex="1" alignItems="center" bg="white" borderBottomWidth="1px" borderBottomColor="gray.200" justifyContent={{ base: "space-between", md: "flex-end" }} > <IconButton display={{ base: "flex", md: "none" }} onClick={onOpen} variant="outline" aria-label="open menu" icon={<FiMenu />} /> <Text display={{ base: "flex", md: "none" }} fontSize="2xl" fontFamily="monospace" fontWeight="bold" > Logo </Text> <UserProfile /> </Flex> ); }
By using the Flex
component, we set Header
‘s display to "flex"
. We want to adjust the left margin, ml
, of the Flex
component based on the breakpoints. We set the left margin to 0
on small devices and 60
on larger devices. You can learn more about Chakra spaces in the docs.
We want the hamburger and the logo to be visible only on mobile, so we set them to display only on mobile devices through their display
prop.
Finally, we pass the onClose
function to the hamburger menu’s onClick
event to trigger the sidebar on mobile, like we explained earlier.
Sidebar
componentThe Sidebar
component consists of a logo, a list of links, and a “close” button, which we will use to close the sidebar on mobile.
Let’s start setting up the sidebar:
import { Box, CloseButton, Flex, Text } from "@chakra-ui/react"; import { FiHome, FiTrendingUp, FiCompass, FiStar, FiSettings, } from "react-icons/fi"; import NavLink from "./NavLink"; const LinkItems = [ { label: "Home", icon: FiHome, href: "/" }, { label: "Trending", icon: FiTrendingUp, href: "/" }, { label: "Explore", icon: FiCompass, href: "/" }, { label: "Favourites", icon: FiStar, href: "/" }, { label: "Settings", icon: FiSettings, href: "/" }, ]; export default function Sidebar({ onClose, ...rest }) { return ( <Box transition="3s ease" bg="white" borderRight="1px" borderRightColor="gray.200" w={{ base: "full", md: 60 }} pos="fixed" h="full" {...rest} > <Flex h="20" alignItems="center" mx="8" justifyContent="space-between"> <Text fontSize="2xl" fontFamily="monospace" fontWeight="bold"> Logo </Text> <CloseButton display={{ base: "flex", md: "none" }} onClick={onClose} /> </Flex> {LinkItems.map((link, i) => ( <NavLink key={i} link={link} /> ))} </Box> ); }
We want the sidebar to take the fullscreen width on mobile, so we set its base width to "full"
. Next, we set its width to 60
on larger devices.
We want the CloseButton
to be visible only on mobile devices, so we set its display to "flex"
on mobile devices and "none"
on larger devices.
We also pass the onClose
function to the CloseButton
to close the sidebar when needed. CloseButton
is needed to close the drawer we set up earlier in the Layout
component.
We have created the sidebar, and it’s responsive, but there is a problem. When we click the sidebar links on mobile, the sidebar does not slide back out of view. How can we fix that and ensure we hide the sidebar if a link is clicked? We address this issue with Next.js’ router.events API.
This API allows us to listen to different events happening inside the Next.js router and react to them.
Let’s update the Sidebar
component’s code:
import { useEffect } from "react"; import { useRouter } from "next/router"; export default function Sidebar({ onClose, ...rest }) { const router = useRouter(); useEffect(() => { router.events.on("routeChangeComplete", onClose); return () => { router.events.off("routeChangeComplete", onClose); }; }, [router.events, onClose]); return ( <Box transition="3s ease" bg="white" borderRight="1px" borderRightColor="gray.200" w={{ base: "full", md: 60 }} pos="fixed" h="full" {...rest} > //other elements of the sidebar go here.... </Box> ); }
Here, we use the useEffect
hook to register the routeChangeComplete
router event. routeChangeComplete
fires when a route changes completely, in our case, when we click on the sidebar links. When routeChangeComplete
takes place, we call onClose
, which causes the sidebar to close.
Finally, you will notice that we defined a LinkItems
array. We loop through the array, and for each element in the array, we render a NavLink
component. NavLink
will contain the data for every link in the sidebar.
Let’s create NavLink
:
import NextLink from "next/link"; import { Flex, Icon, Text } from "@chakra-ui/react"; export default function NavLink({ link, ...rest }) { const { label, icon, href } = link; return ( <NextLink href={href} passHref> <a> <Flex align="center" p="4" mx="4" borderRadius="lg" role="group" cursor="pointer" _hover={{ bg: "cyan.400", color: "white", }} {...rest} > {icon && ( <Icon mr="4" fontSize="16" _groupHover={{ color: "white", }} as={icon} /> )} <Text fontSize="1.2rem">{label}</Text> </Flex> </a> </NextLink> ); }
There are no responsive styles for NavLink
. Here, we pass in the label, icon, and URL from we get when we map through the LinkItems
array.
Now that we have set up the dashboard layout and its components, let’s look at the building blocks of the homepage:
import { useState } from "react"; import { cardVariant, parentVariant } from "@root/motion"; import ProductModal from "@components/ProductModal"; import { motion } from "framer-motion"; import data from "@root/data"; import ProductCard from "@components/ProductCard"; import { Box, SimpleGrid } from "@chakra-ui/react"; const MotionSimpleGrid = motion(SimpleGrid); const MotionBox = motion(Box); export default function Home() { const [modalData, setModalData] = useState(null); return ( <Box> <MotionSimpleGrid mt="4" minChildWidth="250px" spacing="2em" minH="full" variants={parentVariant} initial="initial" animate="animate" > {data.map((product, i) => ( <MotionBox variants={cardVariant} key={i}> <ProductCard product={product} setModalData={setModalData} /> </MotionBox> ))} </MotionSimpleGrid> <ProductModal isOpen={modalData ? true : false} onClose={() => setModalData(null)} modalData={modalData} /> </Box> ); } //sample of product cards array const data = [ { title: "First Product", price: 250, img: "https://res.cloudinary.com/nefejames/image/upload/v1593631406/market%20square/clothes/cloth1.jpg", }, { title: "Second Product", price: 250, img: "https://res.cloudinary.com/nefejames/image/upload/v1593631406/market%20square/clothes/cloth2.jpg", }, //other product objects below
The homepage view consists of two components, ProductCard
and ProductModal
.
We loop through a product data array and create a ProductCard
grid. You can see a sample of the array in the code above.
When a product card is clicked, we want a modal filled with data about the product to pop up. To do that, we define a modalData
state, which will hold the data of the product that was clicked.
We pass the product
object containing the product data and the setModalData
method to ProductCard
.
Next, we pass isOpen
, onClose
, and the modalData
state to ProductModal
. isOpen
is true when modalData
holds a product’s data. The onClose
method sets modalData
to null.
Now that we understand how the ProductCard
and ProductModal
components work, let’s create them.
ProductCard
componentThe ProductCard
component consists of an upper and a lower section. The upper section is an image of the product, and the lower section contains information about the product – its title, price, and number of reviews:
import Image from "next/image"; import { Box, Flex, chakra } from "@chakra-ui/react"; import { AiTwotoneStar } from "react-icons/ai"; const ChakraStar = chakra(AiTwotoneStar); export default function ProductCard({ product, setModalData }) { const { img, title, price } = product; return ( <Flex w="full" h="full" alignItems="center" justifyContent="center" cursor="pointer" bg="white" rounded="xl" shadow="lg" borderWidth="1px" onClick={() => setModalData(product)} > <Box w="full" h="full"> <Box w="100%" height="200px" position="relative" overflow="hidden" roundedTop="lg" > <Image src={img} objectFit="cover" alt="picture of a house" layout="fill" /> </Box> <Box p="6"> <Box fontWeight="semibold" as="h4" lineHeight="tight" isTruncated> {title} </Box> <Box>${price}</Box> </Box> </Box> </Flex> ); }
Like we saw earlier, we pass in the product
object and the setModalData
method to ProductCard
. We call the setModalData
method to update the modalData
state with the product’s data when the product is clicked.
ProductModal
componentThe ProductModal
component consists of the product’s image, title, and price. It also holds a purchase button to model the purchase flow of an actual ecommerce application:
import Image from "next/image"; import { Box, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, Button, useToast, Flex, } from "@chakra-ui/react"; export default function ProductModal({ isOpen, onClose, modalData }) { const { title, price, img } = modalData || {}; const toast = useToast(); const handleModalClose = () => { toast({ title: "Purchase successsful.", description: "Fashion ++", status: "success", duration: 3000, isClosable: true, }); setTimeout(() => { onClose(); }, 1000); }; return ( <Modal isOpen={isOpen} onClose={onClose} size="xl"> <ModalOverlay /> <ModalContent> <ModalCloseButton /> <ModalHeader>Product Details</ModalHeader> <ModalBody> <Box w="full" h="full"> <Flex w="full" h="300px" position="relative"> <Image src={img} alt="a house" objectFit="cover" layout="fill" /> </Flex> <Box pt="3"> <Box mt="3" fontWeight="semibold" as="h4" lineHeight="tight" isTruncated> {title} </Box> ${price} </Box> </Box> </ModalBody> <ModalFooter> <Button bg="cyan.700" color="white" w="150px" size="lg" onClick={handleModalClose} _hover={{ bg: "cyan.800" }} > Purchase </Button> </ModalFooter> </ModalContent> </Modal> ); }
Like we saw earlier, we pass the isOpen
, onClose
, and the modalData
state to ProductModal
. We use Chakra UI’s Modal
here.
modalData
contains the data of the product that was clicked. We access the product’s title, image, and price from the modalData
state.
We also define a handleModalClose
function and pass it to the purchase button. When the button is clicked, a toast is displayed, and the onClose
method is called.
I love using Chakra UI because it helps me stay productive when building the front end of applications. Not only does the style props pattern make Chakra UI a joy to work with, but I also don’t have to deal with the complexities of setting up media queries for different sizes. Chakra UI was designed to provide a great developer experience and is always my go-to component library.
In this article, we’ve learned how to compose responsive components in Chakra UI. We learned how the useMediaQuery
and useDisclosure
hook work and used them to create a responsive dashboard.
The source code for the dashboard application we built is available on GitHub.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.