Sticky Sidebar Layout

Installation

Add the following Soul components

The sticky-sidebar-layout component uses the streamable, skeleton and select components. Make sure you have added them to your project.

Install the following dependencies

npm install clsx

Copy and paste the following code into your project

sections/sticky-sidebar-layout/index.tsx

import { clsx } from 'clsx';import { ReactNode } from 'react';export interface StickySidebarLayoutProps {  className?: string;  sidebar: ReactNode;  children: ReactNode;  containerSize?: 'md' | 'lg' | 'xl' | '2xl';  sidebarSize?: '1/4' | '1/3' | '1/2' | 'sm' | 'md' | 'lg';  sidebarPosition?: 'before' | 'after';  hideOverflow?: boolean;}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { *   --section-max-width-medium: 768px; *   --section-max-width-large: 1024px; *   --section-max-width-x-large: 1280px; *   --section-max-width-2x-large: 1536px; * } * ``` */export function StickySidebarLayout({  className,  sidebar,  children,  sidebarSize = '1/3',  sidebarPosition = 'before',  containerSize = '2xl',  hideOverflow = false,}: StickySidebarLayoutProps) {  return (    <section className={clsx('@container', hideOverflow && 'overflow-hidden', className)}>      <div        className={clsx(          'mx-auto flex flex-col items-stretch gap-x-16 gap-y-10 px-4 py-10 @xl:px-6 @xl:py-14 @4xl:flex-row @4xl:px-8 @4xl:py-20',          {            md: 'max-w-[var(--section-max-width-medium,768px)]',            lg: 'max-w-[var(--section-max-width-large,1024px)]',            xl: 'max-w-[var(--section-max-width-x-large,1280px)]',            '2xl': 'max-w-[var(--section-max-width-2x-large,1536px)]',          }[containerSize],        )}      >        <div          className={clsx(            'min-w-0',            sidebarPosition === 'after' ? 'order-2' : 'order-1',            {              '1/3': '@4xl:w-1/3',              '1/2': '@4xl:w-1/2',              '1/4': '@4xl:w-1/4',              sm: '@4xl:w-48',              md: '@4xl:w-60',              lg: '@4xl:w-80',            }[sidebarSize],          )}        >          <div className="group/sidebar-menu sticky top-10">{sidebar}</div>        </div>        <div          className={clsx(            'min-w-0',            sidebarPosition === 'after' ? 'order-1' : 'order-2',            {              '1/3': '@4xl:w-2/3',              '1/2': '@4xl:w-1/2',              '1/4': '@4xl:w-3/4',              sm: '@4xl:flex-1',              md: '@4xl:flex-1',              lg: '@4xl:flex-1',            }[sidebarSize],          )}        >          {children}        </div>      </div>    </section>  );}

sections/sticky-sidebar-layout/sidebar-menu/index.tsx

import { ComponentPropsWithoutRef } from 'react';import { Stream, Streamable } from '@/vibes/soul/lib/streamable';import * as Skeleton from '@/vibes/soul/primitives/skeleton';import { SidebarMenuLink } from './sidebar-menu-link';import { SidebarMenuSelect } from './sidebar-menu-select';interface MenuLink {  href: string;  label: string;  prefetch?: ComponentPropsWithoutRef<typeof SidebarMenuLink>['prefetch'];}export interface SidebarMenuProps {  links: Streamable<MenuLink[]>;  placeholderCount?: number;}export function SidebarMenu({ links: streamableLinks, placeholderCount = 5 }: SidebarMenuProps) {  return (    <Stream      fallback={<SidebarMenuSkeleton placeholderCount={placeholderCount} />}      value={streamableLinks}    >      {(links) => {        if (!links.length) {          return null;        }        return (          <nav>            <ul className="hidden @2xl:block">              {links.map((link, index) => (                <li key={index}>                  <SidebarMenuLink href={link.href} prefetch={link.prefetch}>                    {link.label}                  </SidebarMenuLink>                </li>              ))}            </ul>            <div className="@2xl:hidden">              <SidebarMenuSelect links={links} />            </div>          </nav>        );      }}    </Stream>  );}export function SidebarMenuSkeleton({  placeholderCount = 5,}: Pick<SidebarMenuProps, 'placeholderCount'>) {  return (    <>      <Skeleton.Root        className="hidden group-has-[[data-pending]]/sidebar-menu:animate-pulse @4xl:block"        pending      >        <div className="w-full" data-pending>          {Array.from({ length: placeholderCount }).map((_, index) => (            <div className="flex h-10 items-center px-3 text-sm" key={index}>              <Skeleton.Text characterCount={10} className="rounded-md" />            </div>          ))}        </div>      </Skeleton.Root>      <Skeleton.Root        className="group-has-[[data-pending]]/sidebar-menu:animate-pulse @4xl:hidden"        pending      >        <div data-pending>          <Skeleton.Box className="h-[50px] w-full rounded-lg" />        </div>      </Skeleton.Root>    </>  );}

sections/sticky-sidebar-layout/sidebar-menu/sidebar-menu-link.tsx

'use client';import { clsx } from 'clsx';import Link from 'next/link';import { usePathname } from 'next/navigation';import React from 'react';export function SidebarMenuLink({  className,  href,  ...rest}: React.ComponentPropsWithoutRef<typeof Link>) {  const pathname = usePathname();  const linkPathname = typeof href === 'string' ? href : (href.pathname ?? null);  return (    <Link      {...rest}      className={clsx(        'flex min-h-10 items-center rounded-md px-3 text-sm font-semibold',        linkPathname !== null && pathname.includes(linkPathname)          ? 'bg-contrast-100'          : 'hover:bg-contrast-100',        className,      )}      href={href}    />  );}

sections/sticky-sidebar-layout/sidebar-menu/sidebar-menu-select.tsx

'use client';import { usePathname, useRouter } from 'next/navigation';import { Select } from '@/vibes/soul/form/select';export function SidebarMenuSelect({ links }: { links: Array<{ href: string; label: string }> }) {  const pathname = usePathname();  const router = useRouter();  return (    <Select      name="sidebar-layout-link-select"      onValueChange={(value) => {        router.push(value);      }}      options={links.map((link) => ({ value: link.href, label: link.label }))}      value={pathname}    />  );}

Usage

import { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';function Usage() {  return (      <StickySidebarLayout          sidebar={              <nav>                  <ul>                      <li><a href="/orders">Orders<a></li>                      <li><a href="/addresses">Addresses<a></li>                      <li><a href="/account">Account<a></li>                  </ul>              </nav>          }      >          <div>              <p>Content</p>          </div>      </StickySidebarLayout>  );}

API Reference

StickySidebarLayoutProps

PropTypeDefault
className
string
sidebar*
ReactNode
children*
ReactNode
containerSize
'md' | 'lg' | 'xl' | '2xl'
'2xl'
sidebarSize
'1/4' | '1/3' | '1/2' | 'sm' | 'md' | 'lg'
'1/3'
sidebarPosition
'before' | 'after'
'before'
hideOverflow
boolean
false

CSS Variables

This component supports various CSS variables for theming. Here's a comprehensive list.

:root {  --section-max-width-medium: 768px;  --section-max-width-large: 1024px;  --section-max-width-x-large: 1280px;  --section-max-width-2x-large: 1536px;}