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    { href: "https://shipfa.st/?via=shipfast_guide", text: "Buy ShipFast", icon: "ShoppingCart", target: "_blank" },
33];
34
35const Sidebar = () => {
36    const [isOpen, setIsOpen] = useState(true);
37    const pathname = usePathname() ?? '/';
38
39    const toggleSidebar = () => {
40        setIsOpen(!isOpen);
41    };
42
43    useEffect(() => {
44        setIsOpen(true);
45    }, [pathname]);
46
47    return (
48        <>
49        <div className={`fixed z-50 sm:hidden ${isOpen ? "left-5 top-5" : "right-5 top-5"}`}>
50            <button onClick={toggleSidebar} className="btn btn-sm btn-ghost btn-square bg-base-100">
51              <Icons name="Menu" width="20" />
52            </button>
53        </div>
54        <div className={`overflow-y-auto overflow-x-visible pb-12 transform ${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`}>
55          <div className="grid gap-2 py-4">
56            <div className="flex items-center space-x-2 rounded-lg">
57              <Link href="/" className="rounded-lg p-2 hover:bg-hover cursor-pointer">
58                <Image
59                  src={logo}
60                  alt={`${config.appName} logo`}
61                  loading="lazy"
62                  width={24}
63                  height={24}
64                  decoding="async"
65                  data-nimg="1"
66                  className="scale-110"
67                  style={{ color: "transparent" }}
68                />
69              </Link>
70            </div>
71            {docs.map((item, index) => (
72              <div key={index}>
73                <NavLink href={item.href} isActive={pathname === item.href}>
74                  <Icons name={item.icon} />
75                  <span className="text-sm font-semibold">{item.title}</span>
76                </NavLink>
77                {item.sub && (
78                  <div className="my-1">
79                    {item.sub.map((subItem, subIndex) => (
80                      <SubLink key={subIndex} href={subItem.href} isActive={pathname === subItem.href}>
81                        <span>{subItem.title}</span>
82                      </SubLink>
83                    ))}
84                  </div>
85                )}
86              </div>
87            ))}
88          </div>
89          <div className="mb-4 border-t border-base-content/10"></div>
90          <ExternalLinks />
91        </div>
92      </>
93    );
94  };
95
96const NavLink = ({ href, isActive, children }: { href: string; isActive: boolean; children: React.ReactNode }) => {
97  return (
98    <Link
99      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`}
100      href={href}
101    >
102      {children}
103    </Link>
104  );
105};
106
107const SubLink = ({ href, isActive, children }: { href: string; isActive: boolean; children: React.ReactNode }) => {
108  return (
109    <Link
110      className={`ml-[17px] pl-[21px] flex items-center border-l text-sm ${isActive ? "text-base-content border-base-content" : "text-base-content/50 border-l-base-content/10 hover:border-l-base-content/50"} py-1.5 transition-all duration-150 ease-in-out hover:text-base-content active:text-base-content active:border-l-base-content space-x-3`}
111      href={href}
112    >
113      {children}
114    </Link>
115  );
116};
117
118const ExternalLinks = () => {
119  return (
120    <div className="grid gap-1">
121      {external.map((link, index) => (
122        <Link
123          key={index}
124          href={link.href}
125          target={link.target}
126          className="flex items-center justify-between rounded-lg px-2 py-1.5 transition-all duration-150 ease-in-out text-base-content/60 hover:bg-hover hover:text-base-content active:bg-active active:text-base-content"
127        >
128          <div className="flex items-center space-x-3">
129            <Icons name={link.icon} />
130            <span className="text-sm font-medium">{link.text}</span>
131          </div>
132          <p>ā†—</p>
133        </Link>
134      ))}
135    </div>
136  );
137};
138
139export 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 = ({ children, lang, title }: { children: any, lang: string, title: string }) => {
9    return (
10        <div className="text-sm bg-base-100 rounded-lg overflow-hidden border border-base-content/20">
11            <div className="px-4 py-2 flex justify-between items-center border-b border-base-content/20">
12                <p className="text-base-content/60">{title}</p>
13                <button
14                    className="btn btn-sm btn-square text-base-content btn-ghost"
15                    onClick={() => copy(children)}
16                >
17                    <Icons name="Copy" />
18                </button>
19            </div>
20            <SyntaxHighlighter
21                style={a11yDark}
22                language={lang}
23                showLineNumbers={true}
24                useInlineStyles={true}
25                wrapLines={true}
26                lineNumberStyle={{ color: "#7d7d7d" }}
27                PreTag={({ children }) => <pre style={{ display: "block", overflowX: "auto", background: "#2b2b2b", color: "#f8f8f2", padding: "1.25rem" }}>{children}</pre>}
28            >
29                {children}
30            </SyntaxHighlighter>
31        </div>
32    );
33};
34
35export 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 = ({ name, width = "18", className = "" }: { name: string, width?: string, className?: string }) => {
4    const LucideIcon = icons[name as keyof typeof icons];
5
6    return <LucideIcon width={width} height={24} className={className} />;
7};
8
9export 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
27
28const Search = () => {
29    const { push } = useRouter();
30    const [options, setOptions] = useState([]);
31
32    const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
33        filterOptions(event.target.value);
34    };
35
36    const filterOptions = (query: string) => {
37        if (query == undefined || query == "") {
38            setOptions([]);
39            return;
40        }
41
42        const filteredOptions: any[] = [];
43
44        const filterRecursively = (docsArray: any[], isSubOption = false) => {
45            docsArray.forEach((option: any) => {
46                if (option.title.toLowerCase().includes(query.toLowerCase()) ||
47                    option.slug.toLowerCase().includes(query.toLowerCase())) {
48                    filteredOptions.push({ ...option, isSubOption });
49                }
50                if (option.sub) {
51                    filterRecursively(option.sub, true);
52                }
53            });
54        };
55
56        filterRecursively(docs);
57
58        setOptions(filteredOptions);
59    };
60
61    const handleSelectionChange = (change: string) => {
62        if (change == undefined || change == "") return;
63        const selected = options.find((option) => option.slug == change);
64        push(selected.href);
65    };
66
67    return (
68        <div className="z-50 max-md:hidden fixed top-6 right-6" >
69            <div className="items-center justify-start gap-4 lg:gap-12">
70                <div className="w-72 z-10">
71                    <div className="relative">
72                        <div className="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 group">
73                            <Combobox onChange={handleSelectionChange}>
74                                <Combobox.Input onChange={handleInputChange} className="bg-base-100 w-full py-3 pl-10 pr-3 text-sm leading-5 text-base-content border border-base-content/20 rounded-lg focus:ring-0 focus:outline-primary" placeholder="Search" />
75                                <Combobox.Button data-headlessui-state="" className="absolute inset-y-0 left-0 flex items-center pl-3 text-sm">
76                                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-base-content/80"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>
77                                </Combobox.Button>
78                                <Combobox.Options as="ul" className="z-50 absolute 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">
79                                    {options.map((option) => (
80                                        <Combobox.Option as="li" key={option.slug} value={option.slug} className="relative cursor-pointer select-none py-2 px-4 text-base-content-secondary data-[focus]:bg-[#4a4949]">
81                                            <p className={option.isSubOption ? "border-l border-base-content/20 pl-3 ml-3 truncate font-normal" : "truncate font-normal"}>{option.title}</p>
82                                        </Combobox.Option>
83                                    ))}
84                                </Combobox.Options>
85                            </Combobox>
86                        </div>
87                    </div>
88                </div>
89            </div>
90        </div >
91    );
92};
93
94export default Search;

Headless UI 2.0

/Search.tsx

1"use client";
2
3import React, { useState } from 'react';
4import { useRouter } from 'next/navigation';
5import { Combobox, ComboboxInput, ComboboxButton, ComboboxOptions, ComboboxOption } 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
27
28const Search = () => {
29    const { push } = useRouter();
30    const [options, setOptions] = useState([]);
31
32    const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
33        filterOptions(event.target.value);
34    };
35
36    const filterOptions = (query: string) => {
37        if (query == undefined || query == "") {
38            setOptions([]);
39            return;
40        }
41
42        const filteredOptions: any[] = [];
43
44        const filterRecursively = (docsArray: any[], isSubOption = false) => {
45            docsArray.forEach((option: any) => {
46                if (option.title.toLowerCase().includes(query.toLowerCase()) ||
47                    option.slug.toLowerCase().includes(query.toLowerCase())) {
48                    filteredOptions.push({ ...option, isSubOption });
49                }
50                if (option.sub) {
51                    filterRecursively(option.sub, true);
52                }
53            });
54        };
55
56        filterRecursively(docs);
57
58        setOptions(filteredOptions);
59    };
60
61    const handleSelectionChange = (change: string) => {
62        if (change == undefined || change == "") return;
63        const selected = options.find((option) => option.slug == change);
64        push(selected.href);
65    };
66
67    return (
68        <div className="z-50 max-md:hidden fixed top-6 right-6" >
69            <div className="items-center justify-start gap-4 lg:gap-12">
70                <div className="w-72 z-10">
71                    <div className="relative">
72                        <div className="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 group">
73                            <Combobox onChange={handleSelectionChange}>
74                                <ComboboxInput onChange={handleInputChange} className="bg-base-100 w-full py-3 pl-10 pr-3 text-sm leading-5 text-base-content border border-base-content/20 rounded-lg focus:ring-0 focus:outline-primary" placeholder="Search" />
75                                <ComboboxButton data-headlessui-state="" className="absolute inset-y-0 left-0 flex items-center pl-3 text-sm">
76                                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-base-content/80"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>
77                                </ComboboxButton>
78                                <ComboboxOptions as="ul" className="z-50 absolute 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">
79                                    {options.map((option) => (
80                                        <ComboboxOption as="li" key={option.slug} value={option.slug} className="relative cursor-pointer select-none py-2 px-4 text-base-content-secondary data-[focus]:bg-[#4a4949]">
81                                            <p className={option.isSubOption ? "border-l border-base-content/20 pl-3 ml-3 truncate font-normal" : "truncate font-normal"}>{option.title}</p>
82                                        </ComboboxOption>
83                                    ))}
84                                </ComboboxOptions>
85                            </Combobox>
86                        </div>
87                    </div>
88                </div>
89            </div>
90        </div >
91    );
92};
93
94export default Search;

Usage

/page.tsx

1<Suspense>
2  <Sidebar />
3  <Search />
4</Suspense>