I was looking for a CSS framework that could be used for React and server side rendering. Tailwind is the most used option, but sometimes I just want to use some existing components to get started more quickly. But most React frameworks don't really work yet with server rendering.
A very good CSS framework already exists, Bootstrap. Why not use that?
The javascript part of Bootstrap is not very composable with React, but there is also a good existing framework for React Components, Headless UI.
The instructions below are for combining those in Next.js. I use plain Bootstrap classes for server rendering and Headless UI for more complex client components.
Install Next.js 13 with Bootstrap 5
First install Next.js:
npx create-next-app@latest
Now add bootstrap:
npm install bootstrap
Next.js has support for sass.
npm install --save-dev sass
Create a styles
folder in the root and add sassOptions
in next.config.js
:
const path = require("path");
/** @type {import('next').NextConfig} */
const nextConfig = {
sassOptions: {
includePaths: [path.join(__dirname, "styles")],
},
};
module.exports = nextConfig;
In that styles folder create a new file bootstrap.scss
with content like this:
$theme-colors: (
'primary': #7a45d0bf,
);
$enable-cssgrid: true;
@import '/node_modules/bootstrap/scss/bootstrap.scss';
$theme-colors
is for overriding bootstrap default colors.
$enable-cssgrid: true;
for the new grid system
Let's use bootstrap now. Import the new stylesheets in app/layout.js
.
import "../styles/bootstrap.scss";
import "./globals.css";
Remove the existing styles in app/globals.css
and app/page.module.css
.
Now replace the content of app/page.js
with this:
export default function Home() {
return (
<div className="container grid gap-3 mt-3">
<button className="btn btn-primary">Bootstrap button</button>
</div>
);
}
You now have a bootstrap button with a custom color.
Headless UI
Add Headless UI now:
npm install @headlessui/react
As an example I will implement the bootstrap accordion with the disclosure from Headless UI.
Create a new file components/Accordion.jsx
,
components
is a new folder in the root folder.
Let's add the example from the Headless UI website:
import { Disclosure } from "@headlessui/react";
export default function Accordion() {
return (
<Disclosure>
<Disclosure.Button className="py-2">
Is team pricing available?
</Disclosure.Button>
<Disclosure.Panel className="text-gray-500">
Yes! You can purchase a license that you can share with your entire
team.
</Disclosure.Panel>
</Disclosure>
);
}
Now in app/page.jsx
use this new component:
import Accordion from "@/components/Accordion";
export default function Home() {
return (
<div className="container grid gap-3 mt-3">
<button className="btn btn-primary g-col-12">Bootstrap button</button>
<div className="g-col-12">
<Accordion />
</div>
</div>
);
}
Now you get an error:
You're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
The solution is easy. Add "use client";
at the top of components/Accordion.jsx
You have a working accordion now, but not themed.
Theming the accordion
Add bootstrap classes:
"use client";
import { Disclosure } from "@headlessui/react";
export default function Accordion() {
return (
<div className="accordion">
<Disclosure>
<div className="accordion-item">
<h2 className="accordion-header">
<Disclosure.Button className="accordion-button">
Is team pricing available?
</Disclosure.Button>
</h2>
<Disclosure.Panel className="accordion-collapse">
<div className="accordion-body">
Yes! You can purchase a license that you can share with your
entire team.
</div>
</Disclosure.Panel>
</div>
</Disclosure>
</div>
);
}
It kind of works, but the arrow is not changing on opening/collapsing.
Whe can use the open
render prop for this:
"use client";
import { Disclosure } from "@headlessui/react";
export default function Accordion() {
return (
<div className="accordion">
<Disclosure>
{({ open }) => (
<div className="accordion-item">
<h2 className="accordion-header">
<Disclosure.Button
className={`accordion-button ${open ? "" : "collapsed"}`}
>
Is team pricing available?
</Disclosure.Button>
</h2>
<Disclosure.Panel className="accordion-collapse">
<div className="accordion-body">
Yes! You can purchase a license that you can share with your
entire team.
</div>
</Disclosure.Panel>
</div>
)}
</Disclosure>
</div>
);
}
This works. Now add an extra disclosure for the final result:
"use client";
import { Disclosure } from "@headlessui/react";
export default function Accordion() {
return (
<div className="accordion">
<Disclosure>
{({ open }) => (
<div className="accordion-item">
<h2 className="accordion-header">
<Disclosure.Button
className={`accordion-button ${open ? "" : "collapsed"}`}
>
First item
</Disclosure.Button>
</h2>
<Disclosure.Panel className="accordion-collapse">
<div className="accordion-body">
This is the content of the first item.
</div>
</Disclosure.Panel>
</div>
)}
</Disclosure>
<Disclosure>
{({ open }) => (
<div className="accordion-item">
<h2 className="accordion-header">
<Disclosure.Button
className={`accordion-button ${open ? "" : "collapsed"}`}
>
Second item
</Disclosure.Button>
</h2>
<Disclosure.Panel className="accordion-collapse">
<div className="accordion-body">
This is the content of the second item.
</div>
</Disclosure.Panel>
</div>
)}
</Disclosure>
</div>
);
}
You should now have something like this:
This is just an example. You can make this component more reusable by adding props for the content. This makes it useable from server components like explained here