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.tsx
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 ${isOpen ? "-translate-x-full" : "w-full translate-x-0"} 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`}
66 >
67 <div className="grid gap-2 py-4">
68 <div className="flex items-center space-x-2 rounded-lg">
69 <Link
70 href="/"
71 className="hover:bg-hover cursor-pointer rounded-lg p-2"
72 >
73 <Image
74 src={logo}
75 alt={"ShipFast Guide Logo"}
76 loading="lazy"
77 width={24}
78 height={24}
79 decoding="async"
80 data-nimg="1"
81 className="scale-110"
82 style={{ color: "transparent" }}
83 />
84 </Link>
85 </div>
86 {docs.map((item, index) => (
87 <div key={index}>
88 <NavLink href={item.href} isActive={pathname === item.href}>
89 <Icons name={item.icon} />
90 <span className="text-sm font-semibold">{item.title}</span>
91 </NavLink>
92 {item.sub && (
93 <div className="my-1">
94 {item.sub.map((subItem, subIndex) => (
95 <SubLink
96 key={subIndex}
97 href={subItem.href}
98 isActive={pathname === subItem.href}
99 >
100 <span>{subItem.title}</span>
101 </SubLink>
102 ))}
103 </div>
104 )}
105 </div>
106 ))}
107 </div>
108 <div className="mb-4 border-t border-base-content/10"></div>
109 <ExternalLinks />
110 </div>
111 </>
112 );
113};
114
115const NavLink = ({
116 href,
117 isActive,
118 children,
119}: {
120 href: string;
121 isActive: boolean;
122 children: React.ReactNode;
123}) => {
124 return (
125 <Link
126 className={`flex items-center space-x-3 ${isActive ? "text-base-content" : "text-base-content/50"} rounded-lg px-2 py-1.5 transition-all duration-150 ease-in-out hover:text-base-content active:text-base-content`}
127 href={href}
128 >
129 {children}
130 </Link>
131 );
132};
133
134const SubLink = ({
135 href,
136 isActive,
137 children,
138}: {
139 href: string;
140 isActive: boolean;
141 children: React.ReactNode;
142}) => {
143 return (
144 <Link
145 className={`ml-[17px] flex items-center border-l pl-[21px] text-sm ${isActive ? "border-base-content text-base-content" : "border-l-base-content/10 text-base-content/50 hover:border-l-base-content/50"} 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`}
146 href={href}
147 >
148 {children}
149 </Link>
150 );
151};
152
153const ExternalLinks = () => {
154 return (
155 <div className="grid gap-1">
156 {external.map((link, index) => (
157 <Link
158 key={index}
159 href={link.href}
160 target={link.target}
161 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"
162 >
163 <div className="flex items-center space-x-3">
164 <Icons name={link.icon} />
165 <span className="text-sm font-medium">{link.text}</span>
166 </div>
167 <p>ā</p>
168 </Link>
169 ))}
170 </div>
171 );
172};
173
174export default Sidebar;
Usage
/layout.tsx
1<Suspense>
2 <Sidebar />
3</Suspense>
Code Block
Install
Terminal
1npm i @stianlarsen/copy-to-clipboard
Code
/CodeBlock.tsx
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";
7
8const CodeBlock = ({
9 children,
10 lang,
11 title,
12}: {
13 children: any;
14 lang: string;
15 title: string;
16}) => {
17 return (
18 <div className="overflow-hidden rounded-lg border border-base-content/20 bg-base-100 text-sm">
19 <div className="flex items-center justify-between border-b border-base-content/20 px-4 py-2">
20 <p className="text-base-content/60">{title}</p>
21 <button
22 className="btn btn-square btn-ghost btn-sm text-base-content"
23 onClick={() => copy(children)}
24 >
25 <Icons name="Copy" />
26 </button>
27 </div>
28 <SyntaxHighlighter
29 style={a11yDark}
30 language={lang}
31 showLineNumbers={true}
32 useInlineStyles={true}
33 wrapLines={true}
34 lineNumberStyle={{ color: "#7d7d7d" }}
35 PreTag={({ children }) => (
36 <pre
37 style={{
38 display: "block",
39 overflowX: "auto",
40 background: "#2b2b2b",
41 color: "#f8f8f2",
42 padding: "1.25rem",
43 }}
44 >
45 {children}
46 </pre>
47 )}
48 >
49 {children}
50 </SyntaxHighlighter>
51 </div>
52 );
53};
54
55export default CodeBlock;
Usage
/page.tsx
1<CodeBlock lang="shell" title="Terminal">{`npm i next`}</CodeBlock>
Icons
Install
Terminal
1npm i lucide-react
Code
/components/Icons.tsx
1import { icons } from "lucide-react";
2
3const Icons = ({
4 name,
5 width = "18",
6 className = "",
7}: {
8 name: string;
9 width?: string;
10 className?: string;
11}) => {
12 const LucideIcon = icons[name as keyof typeof icons];
13
14 return <LucideIcon width={width} height={24} className={className} />;
15};
16
17export default Icons;
Usage
/page.tsx
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.tsx
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 = (event: React.ChangeEvent<HTMLInputElement>) => {
32 filterOptions(event.target.value);
33 };
34
35 const filterOptions = (query: string) => {
36 if (query == undefined || query == "") {
37 setOptions([]);
38 return;
39 }
40
41 const filteredOptions: any[] = [];
42
43 const filterRecursively = (docsArray: any[], isSubOption = false) => {
44 docsArray.forEach((option: any) => {
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: string) => {
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.tsx
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 = (event: React.ChangeEvent<HTMLInputElement>) => {
38 filterOptions(event.target.value);
39 };
40
41 const filterOptions = (query: string) => {
42 if (query == undefined || query == "") {
43 setOptions([]);
44 return;
45 }
46
47 const filteredOptions: any[] = [];
48
49 const filterRecursively = (docsArray: any[], isSubOption = false) => {
50 docsArray.forEach((option: any) => {
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: string) => {
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.tsx
1<Suspense>
2 <Sidebar />
3 <Search />
4</Suspense>