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

/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

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>