Web InnoventixFreeCode

Floating Sidebar

Original · free

floating rounded sidebar

byWeb InnoventixReact + Tailwind
sidefloatingsidebars
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/side-floating.json
side-floating.tsx
"use client";

import {
  useState,
  useRef,
  useEffect,
  useMemo,
  type ReactNode,
  type KeyboardEvent as ReactKeyboardEvent,
} from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";

/* ---------------------------------- icons --------------------------------- */

type IconCmp = (props: { className?: string }) => ReactNode;

const IconDashboard: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <rect x="3" y="3" width="7" height="9" rx="1.5" />
    <rect x="14" y="3" width="7" height="5" rx="1.5" />
    <rect x="14" y="12" width="7" height="9" rx="1.5" />
    <rect x="3" y="16" width="7" height="5" rx="1.5" />
  </svg>
);

const IconAnalytics: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M4 20h16" />
    <path d="M7 20v-6" />
    <path d="M12 20V8" />
    <path d="M17 20v-9" />
    <path d="M5 10l4-4 3 3 5-6" />
  </svg>
);

const IconInbox: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M4 13l2-7a2 2 0 0 1 1.9-1.4h8.2A2 2 0 0 1 18 6l2 7" />
    <path d="M4 13v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4" />
    <path d="M4 13h4l1.5 2.5h5L16 13h4" />
  </svg>
);

const IconProjects: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M3 7a2 2 0 0 1 2-2h3.6a2 2 0 0 1 1.6.8L11.4 7H19a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />
  </svg>
);

const IconCalendar: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <rect x="3.5" y="5" width="17" height="16" rx="2.5" />
    <path d="M3.5 10h17" />
    <path d="M8 3v4M16 3v4" />
    <path d="M8 15h.01M12 15h.01M16 15h.01" />
  </svg>
);

const IconTeam: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <circle cx="9" cy="8" r="3.2" />
    <path d="M3.5 19a5.5 5.5 0 0 1 11 0" />
    <path d="M16 5.2a3.2 3.2 0 0 1 0 5.9" />
    <path d="M17 14.4A5.5 5.5 0 0 1 20.5 19" />
  </svg>
);

const IconChevron: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M9 6l6 6-6 6" />
  </svg>
);

const IconSettings: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M5 7h9M18 7h1" />
    <circle cx="16" cy="7" r="2.2" />
    <path d="M5 17h4M13 17h6" />
    <circle cx="10.5" cy="17" r="2.2" />
  </svg>
);

const IconSpark: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M12 3l1.8 4.6L18.5 9l-4.7 1.4L12 15l-1.8-4.6L5.5 9l4.7-1.4L12 3Z" />
    <path d="M18.5 15l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8 .8-2Z" />
  </svg>
);

const IconSearch: IconCmp = ({ className }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <circle cx="11" cy="11" r="6.5" />
    <path d="m20 20-3.6-3.6" />
  </svg>
);

/* ------------------------------- nav model -------------------------------- */

type NavLeaf = { type: "link"; id: string; label: string; icon: IconCmp; badge?: string };
type NavGroup = { type: "group"; id: string; label: string; icon: IconCmp; children: { id: string; label: string }[] };
type NavNode = NavLeaf | NavGroup;

const NAV: NavNode[] = [
  { type: "link", id: "dashboard", label: "Dashboard", icon: IconDashboard },
  { type: "link", id: "analytics", label: "Analytics", icon: IconAnalytics },
  { type: "link", id: "inbox", label: "Inbox", icon: IconInbox, badge: "4" },
  {
    type: "group",
    id: "projects",
    label: "Projects",
    icon: IconProjects,
    children: [
      { id: "roadmap", label: "Roadmap" },
      { id: "backlog", label: "Backlog" },
      { id: "releases", label: "Releases" },
    ],
  },
  { type: "link", id: "calendar", label: "Calendar", icon: IconCalendar },
  { type: "link", id: "team", label: "Team", icon: IconTeam },
];

const groupIds = new Set(NAV.filter((n): n is NavGroup => n.type === "group").map((n) => n.id));
const isGroupId = (id: string) => groupIds.has(id);
const parentGroupOf = (id: string): NavGroup | undefined =>
  NAV.find((n): n is NavGroup => n.type === "group" && n.children.some((c) => c.id === id));
const labelOf = (id: string): string => {
  for (const n of NAV) {
    if (n.id === id) return n.label;
    if (n.type === "group") {
      const c = n.children.find((ch) => ch.id === id);
      if (c) return c.label;
    }
  }
  return id;
};

/* ------------------------------- component -------------------------------- */

const COLLAPSED_W = 76;
const EXPANDED_W = 272;

export default function SideFloating() {
  const reduce = useReducedMotion();

  const [collapsed, setCollapsed] = useState(false);
  const [active, setActive] = useState("roadmap");
  const [openGroups, setOpenGroups] = useState<string[]>(["projects"]);
  const [focusedId, setFocusedId] = useState("dashboard");

  const itemRefs = useRef<Record<string, HTMLButtonElement | null>>({});
  const focusFlag = useRef(false);

  // Roving-tabindex order in current visible sequence.
  const orderedIds = useMemo(() => {
    const ids: string[] = [];
    for (const n of NAV) {
      ids.push(n.id);
      if (n.type === "group" && !collapsed && openGroups.includes(n.id)) {
        for (const c of n.children) ids.push(c.id);
      }
    }
    return ids;
  }, [collapsed, openGroups]);

  useEffect(() => {
    if (focusFlag.current) {
      itemRefs.current[focusedId]?.focus();
      focusFlag.current = false;
    }
  }, [focusedId]);

  const focusId = (id: string) => {
    focusFlag.current = true;
    setFocusedId(id);
  };
  const focusAt = (i: number) => {
    const id = orderedIds[i];
    if (id) focusId(id);
  };

  const openGroup = (id: string) => setOpenGroups((p) => (p.includes(id) ? p : [...p, id]));
  const closeGroup = (id: string) => setOpenGroups((p) => p.filter((x) => x !== id));
  const toggleGroup = (id: string) => {
    if (collapsed) {
      setCollapsed(false);
      openGroup(id);
    } else {
      setOpenGroups((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
    }
    setFocusedId(id);
  };

  const toggleCollapsed = () =>
    setCollapsed((c) => {
      const next = !c;
      if (next) {
        const p = parentGroupOf(focusedId);
        if (p) setFocusedId(p.id);
      }
      return next;
    });

  const onMenuKeyDown = (e: ReactKeyboardEvent<HTMLUListElement>) => {
    const ids = orderedIds;
    const current = ids.indexOf(focusedId);
    const idx = current === -1 ? 0 : current;
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        focusAt(Math.min(ids.length - 1, idx + 1));
        break;
      case "ArrowUp":
        e.preventDefault();
        focusAt(Math.max(0, idx - 1));
        break;
      case "Home":
        e.preventDefault();
        focusAt(0);
        break;
      case "End":
        e.preventDefault();
        focusAt(ids.length - 1);
        break;
      case "ArrowRight":
        if (isGroupId(focusedId)) {
          e.preventDefault();
          if (collapsed) setCollapsed(false);
          openGroup(focusedId);
        }
        break;
      case "ArrowLeft": {
        if (isGroupId(focusedId) && openGroups.includes(focusedId)) {
          e.preventDefault();
          closeGroup(focusedId);
        } else {
          const p = parentGroupOf(focusedId);
          if (p) {
            e.preventDefault();
            focusId(p.id);
          }
        }
        break;
      }
      case "Escape":
        if (openGroups.length) {
          e.preventDefault();
          setOpenGroups([]);
        }
        break;
      default:
        break;
    }
  };

  const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 500, damping: 38 };
  const submenuT = reduce ? { duration: 0 } : { duration: 0.26, ease: [0.22, 1, 0.36, 1] as const };

  const setRef = (id: string) => (el: HTMLButtonElement | null) => {
    itemRefs.current[id] = el;
  };

  const btnBase =
    "relative flex w-full items-center rounded-xl text-sm font-medium outline-none transition-colors " +
    "focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 " +
    "focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";

  return (
    <section
      aria-labelledby="sf-heading"
      className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:py-24 dark:bg-slate-950 dark:text-slate-100"
    >
      <style>{`
        @keyframes sf-pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.35;transform:scale(.72)} }
        @keyframes sf-rise { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:none} }
        .sf-live-dot{ animation: sf-pulse 1.9s ease-in-out infinite; }
        .sf-rise{ animation: sf-rise .55s cubic-bezier(.22,1,.36,1) both; }
        @media (prefers-reduced-motion: reduce){
          .sf-live-dot, .sf-rise{ animation: none !important; }
        }
      `}</style>

      {/* atmosphere */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
        <div className="absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/15" />
        <div className="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-300/25 blur-3xl dark:bg-violet-700/15" />
      </div>

      <div className="mx-auto max-w-6xl">
        <header className="mb-8 flex flex-col gap-3 px-1 sm:mb-10 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
              Navigation
            </p>
            <h2 id="sf-heading" className="mt-1 text-2xl font-semibold tracking-tight sm:text-3xl">
              Floating rounded sidebar
            </h2>
          </div>
          <p className="max-w-xs text-sm text-slate-500 dark:text-slate-400">
            A detached, collapsible navigation rail. Focus it, then use{" "}
            <kbd className="rounded border border-slate-300 bg-white px-1 font-mono text-[11px] text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">↑</kbd>{" "}
            <kbd className="rounded border border-slate-300 bg-white px-1 font-mono text-[11px] text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">↓</kbd>{" "}
            to move, <kbd className="rounded border border-slate-300 bg-white px-1 font-mono text-[11px] text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">→</kbd> to expand groups, and{" "}
            <kbd className="rounded border border-slate-300 bg-white px-1 font-mono text-[11px] text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">Esc</kbd> to close.
          </p>
        </header>

        {/* app canvas — the rail floats above this surface */}
        <div className="relative flex min-h-[560px] gap-0 overflow-hidden rounded-3xl border border-slate-200/80 bg-slate-100/70 p-3 shadow-[0_30px_80px_-40px_rgba(30,27,75,0.45)] backdrop-blur-sm sm:p-4 dark:border-slate-800/80 dark:bg-slate-900/40">
          {/* ---------------------------- floating rail --------------------------- */}
          <aside
            aria-label="Application sidebar"
            style={{ width: collapsed ? COLLAPSED_W : EXPANDED_W }}
            className={
              "z-10 flex shrink-0 flex-col rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-xl shadow-slate-900/5 backdrop-blur-xl dark:border-slate-800 dark:bg-slate-900/95 dark:shadow-black/30 " +
              (reduce ? "" : "transition-[width] duration-300 ease-out")
            }
          >
            {/* brand + collapse */}
            <div className={"flex gap-2 " + (collapsed ? "flex-col items-center" : "items-center justify-between")}>
              <div className="flex items-center gap-2.5 overflow-hidden">
                <span className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-md shadow-indigo-500/30">
                  <svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="M4 15l5-9 5 9M9 15l5-9 5 9" />
                  </svg>
                </span>
                {!collapsed && (
                  <span className="whitespace-nowrap text-[15px] font-semibold tracking-tight">Northwind</span>
                )}
              </div>
              <button
                type="button"
                onClick={toggleCollapsed}
                aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
                className="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-800 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900"
              >
                <IconChevron className={"h-4 w-4 transition-transform duration-300 " + (collapsed ? "rotate-0" : "rotate-180")} />
              </button>
            </div>

            <div className="my-3 h-px bg-slate-200 dark:bg-slate-800" />

            {/* menu */}
            <nav aria-label="Primary" className="min-h-0 flex-1">
              <ul role="menu" aria-orientation="vertical" onKeyDown={onMenuKeyDown} className="flex flex-col gap-1">
                {NAV.map((node) => {
                  const Icon = node.icon;
                  const isOpen = openGroups.includes(node.id);
                  const childActive = node.type === "group" && node.children.some((c) => c.id === active);
                  const isActive = active === node.id;

                  return (
                    <li key={node.id} role="none" className="group/item relative">
                      <button
                        ref={setRef(node.id)}
                        role="menuitem"
                        type="button"
                        tabIndex={focusedId === node.id ? 0 : -1}
                        aria-current={isActive ? "page" : undefined}
                        aria-haspopup={node.type === "group" ? "menu" : undefined}
                        aria-expanded={node.type === "group" ? (collapsed ? false : isOpen) : undefined}
                        aria-controls={node.type === "group" ? `sf-sub-${node.id}` : undefined}
                        aria-label={collapsed ? node.label : undefined}
                        onClick={() => {
                          if (node.type === "group") toggleGroup(node.id);
                          else {
                            setActive(node.id);
                            setFocusedId(node.id);
                          }
                        }}
                        className={
                          btnBase +
                          " gap-3 py-2.5 " +
                          (collapsed ? "justify-center px-0 " : "px-3 ") +
                          (isActive
                            ? "text-white "
                            : childActive
                            ? "text-indigo-700 hover:bg-slate-100 dark:text-indigo-300 dark:hover:bg-slate-800/70 "
                            : "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800/70 dark:hover:text-white ")
                        }
                      >
                        {isActive && (
                          <motion.span
                            layoutId="sf-active-pill"
                            transition={spring}
                            className="pointer-events-none absolute inset-0 -z-0 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 shadow-md shadow-indigo-500/30 dark:from-indigo-500 dark:to-violet-500"
                          />
                        )}
                        <span className="relative z-10 grid h-5 w-5 shrink-0 place-items-center">
                          <Icon className="h-5 w-5" />
                          {collapsed && node.type === "link" && node.badge && (
                            <span className="absolute -right-1.5 -top-1.5 h-2 w-2 rounded-full bg-rose-500 ring-2 ring-white dark:ring-slate-900" />
                          )}
                        </span>

                        {!collapsed && (
                          <span className="relative z-10 flex-1 whitespace-nowrap text-left">{node.label}</span>
                        )}

                        {!collapsed && node.type === "link" && node.badge && (
                          <span
                            className={
                              "relative z-10 ml-auto rounded-full px-2 py-0.5 text-[11px] font-semibold " +
                              (isActive
                                ? "bg-white/20 text-white"
                                : "bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-300")
                            }
                          >
                            {node.badge}
                          </span>
                        )}

                        {!collapsed && node.type === "group" && (
                          <IconChevron
                            className={
                              "relative z-10 ml-auto h-4 w-4 shrink-0 transition-transform duration-200 " +
                              (isOpen ? "rotate-90" : "rotate-0") +
                              (childActive ? " text-indigo-500" : " text-slate-400")
                            }
                          />
                        )}
                      </button>

                      {/* tooltip when collapsed */}
                      {collapsed && (
                        <span
                          aria-hidden="true"
                          className="pointer-events-none absolute left-[calc(100%+14px)] top-1/2 z-30 -translate-y-1/2 whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs font-medium text-white opacity-0 shadow-lg transition-opacity duration-150 group-hover/item:opacity-100 group-focus-within/item:opacity-100 dark:bg-slate-700"
                        >
                          {node.label}
                          {node.type === "link" && node.badge ? ` · ${node.badge}` : ""}
                        </span>
                      )}

                      {/* submenu */}
                      {node.type === "group" && (
                        <AnimatePresence initial={false}>
                          {!collapsed && isOpen && (
                            <motion.ul
                              id={`sf-sub-${node.id}`}
                              role="menu"
                              aria-label={node.label}
                              initial={{ height: 0, opacity: 0 }}
                              animate={{ height: "auto", opacity: 1 }}
                              exit={{ height: 0, opacity: 0 }}
                              transition={submenuT}
                              className="overflow-hidden"
                            >
                              <li role="none" className="mt-1 ml-[22px] flex flex-col gap-0.5 border-l border-slate-200 pl-3 dark:border-slate-800">
                                {node.children.map((child) => {
                                  const childIsActive = active === child.id;
                                  return (
                                    <div key={child.id} role="none">
                                      <button
                                        ref={setRef(child.id)}
                                        role="menuitem"
                                        type="button"
                                        tabIndex={focusedId === child.id ? 0 : -1}
                                        aria-current={childIsActive ? "page" : undefined}
                                        onClick={() => {
                                          setActive(child.id);
                                          setFocusedId(child.id);
                                        }}
                                        className={
                                          btnBase +
                                          " gap-2.5 py-2 pl-3 pr-3 " +
                                          (childIsActive
                                            ? "text-white "
                                            : "text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800/70 dark:hover:text-white ")
                                        }
                                      >
                                        {childIsActive && (
                                          <motion.span
                                            layoutId="sf-active-pill"
                                            transition={spring}
                                            className="pointer-events-none absolute inset-0 -z-0 rounded-lg bg-gradient-to-r from-indigo-600 to-violet-600 shadow-md shadow-indigo-500/30 dark:from-indigo-500 dark:to-violet-500"
                                          />
                                        )}
                                        <span
                                          className={
                                            "relative z-10 h-1.5 w-1.5 shrink-0 rounded-full " +
                                            (childIsActive ? "bg-white" : "bg-slate-300 dark:bg-slate-600")
                                          }
                                        />
                                        <span className="relative z-10 whitespace-nowrap">{child.label}</span>
                                      </button>
                                    </div>
                                  );
                                })}
                              </li>
                            </motion.ul>
                          )}
                        </AnimatePresence>
                      )}
                    </li>
                  );
                })}
              </ul>
            </nav>

            {/* upgrade card */}
            {!collapsed && (
              <div className="mt-3 overflow-hidden rounded-xl bg-gradient-to-br from-indigo-600 to-violet-700 p-3 text-white shadow-lg shadow-indigo-600/25">
                <div className="flex items-center gap-2">
                  <IconSpark className="h-4 w-4 text-amber-300" />
                  <span className="text-sm font-semibold">Go Pro</span>
                </div>
                <p className="mt-1 text-xs leading-relaxed text-indigo-100">
                  Unlock unlimited projects and advanced roadmaps.
                </p>
                <button
                  type="button"
                  className="mt-2.5 w-full rounded-lg bg-white/95 py-1.5 text-xs font-semibold text-indigo-700 outline-none transition hover:bg-white focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-600"
                >
                  Upgrade plan
                </button>
              </div>
            )}

            <div className="my-3 h-px bg-slate-200 dark:bg-slate-800" />

            {/* profile */}
            <div className={"flex items-center gap-2 " + (collapsed ? "justify-center" : "")}>
              <button
                type="button"
                aria-label="Maya Chen — account menu"
                className="group/prof relative flex min-w-0 flex-1 items-center gap-2.5 rounded-xl p-1.5 outline-none transition-colors hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
              >
                <span className="relative grid h-9 w-9 shrink-0 place-items-center rounded-full bg-gradient-to-br from-emerald-400 to-sky-500 text-sm font-semibold text-white">
                  MC
                  <span className="sf-live-dot absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full bg-emerald-500 ring-2 ring-white dark:ring-slate-900" />
                </span>
                {!collapsed && (
                  <span className="min-w-0 flex-1 text-left">
                    <span className="block truncate text-sm font-semibold text-slate-800 dark:text-slate-100">Maya Chen</span>
                    <span className="block truncate text-xs text-slate-500 dark:text-slate-400">Workspace admin</span>
                  </span>
                )}
              </button>
              {!collapsed && (
                <button
                  type="button"
                  aria-label="Settings"
                  className="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-800 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900"
                >
                  <IconSettings className="h-5 w-5" />
                </button>
              )}
            </div>
          </aside>

          {/* ---------------------------- content preview ------------------------- */}
          <div className="hidden min-w-0 flex-1 flex-col pl-4 lg:flex">
            <div className="flex items-center justify-between gap-3">
              <div className="min-w-0">
                <h3 className="truncate text-lg font-semibold tracking-tight">{labelOf(active)}</h3>
                <p className="truncate text-sm text-slate-500 dark:text-slate-400">Q3 delivery workspace · 12 collaborators</p>
              </div>
              <div className="flex items-center gap-2">
                <div className="hidden items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-400 sm:flex dark:border-slate-800 dark:bg-slate-900">
                  <IconSearch className="h-4 w-4" />
                  <span>Search tasks…</span>
                </div>
                <button
                  type="button"
                  className="rounded-xl bg-slate-900 px-3.5 py-2 text-sm font-semibold text-white outline-none transition hover:bg-slate-700 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
                >
                  New task
                </button>
              </div>
            </div>

            <div className="mt-5 grid grid-cols-3 gap-3">
              {[
                { k: "Active sprints", v: "6", d: "+2 this week", tone: "text-emerald-600 dark:text-emerald-400" },
                { k: "Open issues", v: "128", d: "-14 vs last", tone: "text-emerald-600 dark:text-emerald-400" },
                { k: "Cycle time", v: "3.2d", d: "+0.4d slower", tone: "text-rose-600 dark:text-rose-400" },
              ].map((s, i) => (
                <div
                  key={s.k}
                  className="sf-rise rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900"
                  style={{ animationDelay: `${i * 70}ms` }}
                >
                  <p className="text-xs font-medium text-slate-500 dark:text-slate-400">{s.k}</p>
                  <p className="mt-1 text-2xl font-semibold tracking-tight">{s.v}</p>
                  <p className={"mt-0.5 text-xs font-medium " + s.tone}>{s.d}</p>
                </div>
              ))}
            </div>

            <div className="mt-4 min-h-0 flex-1 overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
              <div className="flex items-center justify-between border-b border-slate-200 px-4 py-3 dark:border-slate-800">
                <span className="text-sm font-semibold">In progress</span>
                <span className="text-xs text-slate-400">Sorted by priority</span>
              </div>
              <ul className="divide-y divide-slate-100 dark:divide-slate-800">
                {[
                  { t: "Ship onboarding checklist v2", who: "Priya", tag: "High", tone: "bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-300" },
                  { t: "Instrument funnel analytics events", who: "Dev", tag: "Medium", tone: "bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300" },
                  { t: "Draft Q3 release notes", who: "Maya", tag: "Low", tone: "bg-sky-100 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300" },
                  { t: "Audit empty-state illustrations", who: "Leo", tag: "Medium", tone: "bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300" },
                ].map((row) => (
                  <li key={row.t} className="flex items-center gap-3 px-4 py-3">
                    <span className="h-4 w-4 shrink-0 rounded-md border-2 border-slate-300 dark:border-slate-600" />
                    <span className="min-w-0 flex-1 truncate text-sm text-slate-700 dark:text-slate-200">{row.t}</span>
                    <span className={"rounded-full px-2 py-0.5 text-[11px] font-semibold " + row.tone}>{row.tag}</span>
                    <span className="hidden w-16 truncate text-right text-xs text-slate-400 sm:block">{row.who}</span>
                  </li>
                ))}
              </ul>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →