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.

/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;

/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

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>