ComponentsDocs
Purchase the template here if you want all the components and the starter guide.
Get some of the same components used on this site in your own project.
Table of Contents:
Sidebar
Code
/Sidebar.js
1"use client";
2
3import { useState, useEffect } from "react";
4import { usePathname } from "next/navigation";
5import Image from "next/image";
6import Link from "next/link";
7import config from "@/config";
8import Icons from "@/components/Icons";
9import logo from "@/app/icon.png";
10
11const docs = [
12 {
13 title: "Getting Started",
14 href: "/docs/getting-started",
15 icon: "Play",
16 },
17 {
18 slug: "tutorials",
19 title: "Tutorials",
20 href: "/docs/tutorials",
21 icon: "Book",
22 sub: [
23 {
24 title: "Ship in 5 minutes",
25 href: "/docs/tutorials/ship-in-5-minutes",
26 },
27 ],
28 },
29];
30
31const external = [
32 {
33 href: "https://shipfa.st/?via=shipfast_guide",
34 text: "Buy ShipFast",
35 icon: "ShoppingCart",
36 target: "_blank",
37 },
38];
39
40const Sidebar = () => {
41 const [isOpen, setIsOpen] = useState(true);
42 const pathname = usePathname() ?? "/";
43
44 const toggleSidebar = () => {
45 setIsOpen(!isOpen);
46 };
47
48 useEffect(() => {
49 setIsOpen(true);
50 }, [pathname]);
51
52 return (
53 <>
54 <div
55 className={`fixed z-50 sm:hidden ${isOpen ? "left-5 top-5" : "right-5 top-5"}`}
56 >
57 <button
58 onClick={toggleSidebar}
59 className="btn btn-square btn-ghost btn-sm bg-base-100"
60 >
61 <Icons name="Menu" width="20" />
62 </button>
63 </div>
64 <div
65 className={`transform overflow-y-auto overflow-x-visible pb-12 ${
66 isOpen ? "-translate-x-full" : "w-full translate-x-0"
67 } fixed z-40 flex h-full flex-col border-r border-base-content/10 bg-base-100 p-4 transition-all sm:w-64 sm:translate-x-0`}
68 >
69 <div className="grid gap-2 py-4">
70 <div className="flex items-center space-x-2 rounded-lg">
71 <Link
72 href="/"
73 className="hover:bg-hover cursor-pointer rounded-lg p-2"
74 >
75 <Image
76 src={logo}
77 alt={"ShipFast Guide Logo"}
78 loading="lazy"
79 width={24}
80 height={24}
81 decoding="async"
82 data-nimg="1"
83 className="scale-110"
84 style={{ color: "transparent" }}
85 />
86 </Link>
87 </div>
88 {docs.map((item, index) => (
89 <div key={index}>
90 <NavLink href={item.href} isActive={pathname === item.href}>
91 <Icons name={item.icon} />
92 <span className="text-sm font-semibold">{item.title}</span>
93 </NavLink>
94 {item.sub && (
95 <div className="my-1">
96 {item.sub.map((subItem, subIndex) => (
97 <SubLink
98 key={subIndex}
99 href={subItem.href}
100 isActive={pathname === subItem.href}
101 >
102 <span>{subItem.title}</span>
103 </SubLink>
104 ))}
105 </div>
106 )}
107 </div>
108 ))}
109 </div>
110 <div className="mb-4 border-t border-base-content/10"></div>
111 <ExternalLinks />
112 </div>
113 </>
114 );
115};
116
117const NavLink = ({ href, isActive, children }) => {
118 return (
119 <Link
120 className={`flex items-center space-x-3 ${
121 isActive ? "text-base-content" : "text-base-content/50"
122 } rounded-lg px-2 py-1.5 transition-all duration-150 ease-in-out hover:text-base-content active:text-base-content`}
123 href={href}
124 >
125 {children}
126 </Link>
127 );
128};
129
130const SubLink = ({ href, isActive, children }) => {
131 return (
132 <Link
133 className={`ml-[17px] flex items-center border-l pl-[21px] text-sm ${
134 isActive
135 ? "border-base-content text-base-content"
136 : "border-l-base-content/10 text-base-content/50 hover:border-l-base-content/50"
137 } space-x-3 py-1.5 transition-all duration-150 ease-in-out hover:text-base-content active:border-l-base-content active:text-base-content`}
138 href={href}
139 >
140 {children}
141 </Link>
142 );
143};
144
145const ExternalLinks = () => {
146 return (
147 <div className="grid gap-1">
148 {external.map((link, index) => (
149 <Link
150 key={index}
151 href={link.href}
152 target={link.target}
153 className="hover:bg-hover active:bg-active flex items-center justify-between rounded-lg px-2 py-1.5 text-base-content/60 transition-all duration-150 ease-in-out hover:text-base-content active:text-base-content"
154 >
155 <div className="flex items-center space-x-3">
156 <Icons name={link.icon} />
157 <span className="text-sm font-medium">{link.text}</span>
158 </div>
159 <p>ā</p>
160 </Link>
161 ))}
162 </div>
163 );
164};
165
166export default Sidebar;
Usage
/layout.js
1<Suspense>
2 <Sidebar />
3</Suspense>
Code Block
Install
Terminal
1npm i @stianlarsen/copy-to-clipboard
Code
/CodeBlock.js
1"use client";
2
3import { default as SyntaxHighlighter } from "react-syntax-highlighter";
4import { a11yDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
5import { copy } from "@stianlarsen/copy-to-clipboard";
6import Icons from "./Icons";
7import toast from "react-hot-toast";
8
9const CodeBlock = ({ children, lang, title }) => {
10 return (
11 <div className="overflow-hidden rounded-lg border border-base-content/20 bg-base-100 text-sm">
12 <div className="flex items-center justify-between border-b border-base-content/20 px-4 py-2">
13 <p className="text-base-content/60">{title}</p>
14 <button
15 className="btn btn-square btn-ghost btn-sm text-base-content"
16 onClick={() => copy(children, toast.success("Copied to clipboard!"))}
17 >
18 <Icons name="Copy" />
19 </button>
20 </div>
21 <SyntaxHighlighter
22 style={a11yDark}
23 language={lang}
24 showLineNumbers={true}
25 useInlineStyles={true}
26 wrapLines={true}
27 lineNumberStyle={{ color: "#7d7d7d" }}
28 PreTag={({ children }) => (
29 <pre
30 style={{
31 display: "block",
32 overflowX: "auto",
33 background: "#2b2b2b",
34 color: "#f8f8f2",
35 padding: "1.25rem",
36 }}
37 >
38 {children}
39 </pre>
40 )}
41 >
42 {children}
43 </SyntaxHighlighter>
44 </div>
45 );
46};
47
48export default CodeBlock;
Usage
/page.js
1<CodeBlock lang="shell" title="Terminal">{`npm i next`}</CodeBlock>
Icons
Install
Terminal
1npm i lucide-react
Code
/components/Icons.js
1import { icons } from "lucide-react";
2
3const Icons = ({ name, width = 18, className = "" }) => {
4 const LucideIcon = icons[name];
5
6 return <LucideIcon width={width} height={24} className={className} />;
7};
8
9export default Icons;
Usage
/page.js
1<Icon name="Menu" width="20" /> // The menu button icon for open and close the sidebar
2<Icon name={item.icon} /> // The sidebar icons for each category
3<Icon name="Copy" /> // The copy icon for the code block
Search
Headless UI 1.7
/Search.js
1"use client";
2
3import React, { useState } from "react";
4import { useRouter } from "next/navigation";
5import { Combobox } from "@headlessui/react";
6
7const docs = [
8 {
9 slug: "get-started",
10 title: "Get Started",
11 href: "/docs/get-started",
12 },
13 {
14 slug: "tutorials",
15 title: "Tutorials",
16 href: "/docs/tutorials",
17 sub: [
18 {
19 slug: "ship-in-5-minutes",
20 title: "Ship In 5 Minutes",
21 href: "/docs/tutorials/ship-in-5-minutes",
22 },
23 ],
24 },
25];
26
27const Search = () => {
28 const { push } = useRouter();
29 const [options, setOptions] = useState([]);
30
31 const handleInputChange = (e) => {
32 filterOptions(e.target.value);
33 };
34
35 const filterOptions = (query) => {
36 if (query == undefined || query == "") {
37 setOptions([]); // Clear options if query is empty or undefined
38 return;
39 }
40
41 const filteredOptions = [];
42
43 const filterRecursively = (docsArray, isSubOption = false) => {
44 docsArray.forEach((option) => {
45 if (
46 option.title.toLowerCase().includes(query.toLowerCase()) ||
47 option.slug.toLowerCase().includes(query.toLowerCase())
48 ) {
49 filteredOptions.push({ ...option, isSubOption });
50 }
51 if (option.sub) {
52 filterRecursively(option.sub, true);
53 }
54 });
55 };
56
57 filterRecursively(docs);
58
59 setOptions(filteredOptions);
60 };
61
62 const handleSelectionChange = (change) => {
63 if (change == undefined || change == "") return;
64 const selected = options.find((option) => option.slug == change);
65 push(selected.href);
66 };
67
68 return (
69 <div className="fixed right-6 top-6 z-50 max-md:hidden">
70 <div className="items-center justify-start gap-4 lg:gap-12">
71 <div className="z-10 w-72">
72 <div className="relative">
73 <div className="group relative w-full cursor-default rounded-lg bg-base-100 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-base-100/20 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-primary sm:text-sm">
74 <Combobox onChange={handleSelectionChange}>
75 <Combobox.Input
76 onChange={handleInputChange}
77 className="w-full rounded-lg border border-base-content/20 bg-base-100 py-3 pl-10 pr-3 text-sm leading-5 text-base-content focus:outline-primary focus:ring-0"
78 placeholder="Search"
79 />
80 <Combobox.Button
81 data-headlessui-state=""
82 className="absolute inset-y-0 left-0 flex items-center pl-3 text-sm"
83 >
84 <svg
85 xmlns="http://www.w3.org/2000/svg"
86 width="18"
87 height="24"
88 viewBox="0 0 24 24"
89 fill="none"
90 stroke="currentColor"
91 strokeWidth="2"
92 strokeLinecap="round"
93 strokeLinejoin="round"
94 className="text-base-content/80"
95 >
96 <circle cx="11" cy="11" r="8"></circle>
97 <path d="m21 21-4.3-4.3"></path>
98 </svg>
99 </Combobox.Button>
100 <Combobox.Options
101 as="ul"
102 className="absolute z-50 mt-1 max-h-96 w-full overflow-auto rounded-lg bg-base-100 py-1 text-base shadow-lg ring-1 ring-base-content ring-opacity-20 focus:outline-none sm:text-sm"
103 >
104 {options.map((option) => (
105 <Combobox.Option
106 as="li"
107 key={option.slug}
108 value={option.slug}
109 className="text-base-content-secondary relative cursor-pointer select-none px-4 py-2 data-[focus]:bg-[#4a4949]"
110 >
111 <p
112 className={
113 option.isSubOption
114 ? "ml-3 truncate border-l border-base-content/20 pl-3 font-normal"
115 : "truncate font-normal"
116 }
117 >
118 {option.title}
119 </p>
120 </Combobox.Option>
121 ))}
122 </Combobox.Options>
123 </Combobox>
124 </div>
125 </div>
126 </div>
127 </div>
128 </div>
129 );
130};
131
132export default Search;
Headless UI 2.0
/Search.js
1"use client";
2
3import React, { useState } from "react";
4import { useRouter } from "next/navigation";
5import {
6 Combobox,
7 ComboboxInput,
8 ComboboxButton,
9 ComboboxOptions,
10 ComboboxOption,
11} from "@headlessui/react";
12
13const docs = [
14 {
15 slug: "get-started",
16 title: "Get Started",
17 href: "/docs/get-started",
18 },
19 {
20 slug: "tutorials",
21 title: "Tutorials",
22 href: "/docs/tutorials",
23 sub: [
24 {
25 slug: "ship-in-5-minutes",
26 title: "Ship In 5 Minutes",
27 href: "/docs/tutorials/ship-in-5-minutes",
28 },
29 ],
30 },
31];
32
33const Search = () => {
34 const { push } = useRouter();
35 const [options, setOptions] = useState([]);
36
37 const handleInputChange = (e) => {
38 filterOptions(e.target.value);
39 };
40
41 const filterOptions = (query) => {
42 if (query == undefined || query == "") {
43 setOptions([]); // Clear options if query is empty or undefined
44 return;
45 }
46
47 const filteredOptions = [];
48
49 const filterRecursively = (docsArray, isSubOption = false) => {
50 docsArray.forEach((option) => {
51 if (
52 option.title.toLowerCase().includes(query.toLowerCase()) ||
53 option.slug.toLowerCase().includes(query.toLowerCase())
54 ) {
55 filteredOptions.push({ ...option, isSubOption });
56 }
57 if (option.sub) {
58 filterRecursively(option.sub, true);
59 }
60 });
61 };
62
63 filterRecursively(docs);
64
65 setOptions(filteredOptions);
66 };
67
68 const handleSelectionChange = (change) => {
69 if (change == undefined || change == "") return;
70 const selected = options.find((option) => option.slug == change);
71 push(selected.href);
72 };
73
74 return (
75 <div className="fixed right-6 top-6 z-50 max-md:hidden">
76 <div className="items-center justify-start gap-4 lg:gap-12">
77 <div className="z-10 w-72">
78 <div className="relative">
79 <div className="group relative w-full cursor-default rounded-lg bg-base-100 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-base-100/20 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-primary sm:text-sm">
80 <Combobox onChange={handleSelectionChange}>
81 <ComboboxInput
82 onChange={handleInputChange}
83 className="w-full rounded-lg border border-base-content/20 bg-base-100 py-3 pl-10 pr-3 text-sm leading-5 text-base-content focus:outline-primary focus:ring-0"
84 placeholder="Search"
85 />
86 <ComboboxButton
87 data-headlessui-state=""
88 className="absolute inset-y-0 left-0 flex items-center pl-3 text-sm"
89 >
90 <svg
91 xmlns="http://www.w3.org/2000/svg"
92 width="18"
93 height="24"
94 viewBox="0 0 24 24"
95 fill="none"
96 stroke="currentColor"
97 strokeWidth="2"
98 strokeLinecap="round"
99 strokeLinejoin="round"
100 className="text-base-content/80"
101 >
102 <circle cx="11" cy="11" r="8"></circle>
103 <path d="m21 21-4.3-4.3"></path>
104 </svg>
105 </ComboboxButton>
106 <ComboboxOptions
107 as="ul"
108 className="absolute z-50 mt-1 max-h-96 w-full overflow-auto rounded-lg bg-base-100 py-1 text-base shadow-lg ring-1 ring-base-content ring-opacity-20 focus:outline-none sm:text-sm"
109 >
110 {options.map((option) => (
111 <ComboboxOption
112 as="li"
113 key={option.slug}
114 value={option.slug}
115 className="text-base-content-secondary relative cursor-pointer select-none px-4 py-2 data-[focus]:bg-[#4a4949]"
116 >
117 <p
118 className={
119 option.isSubOption
120 ? "ml-3 truncate border-l border-base-content/20 pl-3 font-normal"
121 : "truncate font-normal"
122 }
123 >
124 {option.title}
125 </p>
126 </ComboboxOption>
127 ))}
128 </ComboboxOptions>
129 </Combobox>
130 </div>
131 </div>
132 </div>
133 </div>
134 </div>
135 );
136};
137
138export default Search;
Usage
/page.js
1<Suspense>
2 <Sidebar />
3 <Search />
4</Suspense>