Web InnoventixFreeCode

Error 403

Original · free

403 access denied page

byWeb InnoventixReact + Tailwind
error403pages
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/error-403.json
error-403.tsx
"use client";

import { type FormEvent, useId, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type CopyKey = "reference" | "ticket";
type AccessLevel = "read" | "export" | "admin";

const LEVELS: { value: AccessLevel; label: string; hint: string }[] = [
  { value: "read", label: "Read-only", hint: "View reports and exports" },
  { value: "export", label: "Read & export", hint: "Download billing exports" },
  { value: "admin", label: "Admin", hint: "Manage members and scopes" },
];

const REASONS: string[] = [
  "Your role (Analyst) doesn’t include the billing:read scope.",
  "This resource is limited to members of the Finance group.",
  "Elevated access needs sign-off from a workspace admin.",
];

const REFERENCE = "ACL-403-7F2A91";

export default function Error403() {
  const reduce = useReducedMotion();
  const uid = useId();
  const liveRef = useRef<HTMLParagraphElement | null>(null);

  const [copied, setCopied] = useState<CopyKey | null>(null);
  const [showWhy, setShowWhy] = useState(false);

  const [email, setEmail] = useState("");
  const [level, setLevel] = useState<AccessLevel>("export");
  const [reason, setReason] = useState("");
  const [errors, setErrors] = useState<{ email?: string; reason?: string }>({});
  const [ticket, setTicket] = useState<string | null>(null);

  const emailId = `${uid}-email`;
  const levelId = `${uid}-level`;
  const reasonId = `${uid}-reason`;
  const emailErrId = `${uid}-email-err`;
  const reasonErrId = `${uid}-reason-err`;
  const whyId = `${uid}-why`;

  async function copy(text: string, key: CopyKey) {
    try {
      if (typeof navigator !== "undefined" && navigator.clipboard) {
        await navigator.clipboard.writeText(text);
      }
      setCopied(key);
      window.setTimeout(() => setCopied((c) => (c === key ? null : c)), 1800);
    } catch {
      setCopied(null);
    }
  }

  function submit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const next: { email?: string; reason?: string } = {};
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
      next.email = "Enter the work email tied to your account.";
    }
    if (reason.trim().length < 12) {
      next.reason = "Add at least a sentence so the admin can review quickly.";
    }
    setErrors(next);
    if (Object.keys(next).length > 0) return;
    const id = "REQ-" + Math.random().toString(36).slice(2, 8).toUpperCase();
    setTicket(id);
  }

  function reset() {
    setTicket(null);
    setErrors({});
  }

  const fade = (delay: number) =>
    reduce
      ? { initial: false as const, animate: { opacity: 1, y: 0 } }
      : {
          initial: { opacity: 0, y: 14 },
          animate: { opacity: 1, y: 0 },
          transition: { duration: 0.55, delay, ease: "easeOut" as const },
        };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 text-slate-900 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        .e403-scan{animation:e403-scan 3.4s ease-in-out infinite}
        @keyframes e403-scan{0%{transform:translateY(6px);opacity:0}12%{opacity:.85}50%{transform:translateY(104px);opacity:.85}88%{opacity:.85}100%{transform:translateY(6px);opacity:0}}
        .e403-glow{animation:e403-glow 3.2s ease-in-out infinite}
        @keyframes e403-glow{0%,100%{opacity:.3}50%{opacity:.7}}
        .e403-float{animation:e403-float 6.5s ease-in-out infinite}
        @keyframes e403-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
        @media (prefers-reduced-motion: reduce){.e403-scan,.e403-glow,.e403-float{animation:none!important}}
      `}</style>

      {/* Decorative backdrop */}
      <div aria-hidden className="pointer-events-none absolute inset-0">
        <svg className="absolute inset-0 h-full w-full text-slate-300/60 dark:text-slate-700/40" width="100%" height="100%">
          <defs>
            <pattern id={`${uid}-grid`} width="34" height="34" patternUnits="userSpaceOnUse">
              <path d="M34 0H0V34" fill="none" stroke="currentColor" strokeWidth="1" />
            </pattern>
          </defs>
          <rect width="100%" height="100%" fill={`url(#${uid}-grid)`} />
        </svg>
        <div className="e403-glow absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-rose-400/20 blur-3xl dark:bg-rose-500/20" />
        <div className="e403-glow absolute bottom-0 right-0 h-64 w-96 translate-x-1/4 translate-y-1/4 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/15" />
      </div>

      <div className="relative mx-auto max-w-5xl">
        {/* Header / hero */}
        <div className="flex flex-col items-center text-center">
          <motion.span
            {...fade(0)}
            className="inline-flex items-center gap-2 rounded-full border border-rose-300/70 bg-rose-100/70 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-widest text-rose-700 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-300"
          >
            <span className="relative flex h-2 w-2">
              <span className="e403-glow absolute inline-flex h-full w-full rounded-full bg-rose-500/70" />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-rose-500" />
            </span>
            Error 403 &middot; Access denied
          </motion.span>

          <motion.div {...fade(0.06)} className="e403-float mt-8">
            <ShieldMark uid={uid} />
          </motion.div>

          <motion.h1
            {...fade(0.12)}
            className="mt-7 bg-gradient-to-b from-slate-900 to-slate-600 bg-clip-text text-5xl font-black tracking-tight text-transparent sm:text-6xl dark:from-white dark:to-slate-400"
          >
            Access denied
          </motion.h1>

          <motion.p {...fade(0.18)} className="mt-4 max-w-xl text-balance text-base leading-relaxed text-slate-600 dark:text-slate-400">
            You&rsquo;re signed in, but your account doesn&rsquo;t have permission to open{" "}
            <span className="font-semibold text-slate-800 dark:text-slate-200">Production Billing Exports</span>. Request
            access below or head back to where you belong.
          </motion.p>

          <motion.div {...fade(0.24)} className="mt-7 flex flex-col items-center gap-3 sm:flex-row">
            <div className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white/80 px-3 py-2 text-sm shadow-sm dark:border-slate-800 dark:bg-slate-900/70">
              <span className="text-slate-500 dark:text-slate-500">Reference</span>
              <code className="font-mono font-semibold tracking-tight text-slate-800 dark:text-slate-200">{REFERENCE}</code>
              <button
                type="button"
                onClick={() => copy(REFERENCE, "reference")}
                className="ml-1 inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-xs font-medium text-indigo-600 transition-colors hover:bg-indigo-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-300 dark:hover:bg-indigo-500/10 dark:focus-visible:ring-offset-slate-950"
              >
                {copied === "reference" ? <CheckIcon /> : <CopyIcon />}
                {copied === "reference" ? "Copied" : "Copy"}
              </button>
            </div>

            <div className="flex items-center gap-3">
              <button
                type="button"
                onClick={() => {
                  if (typeof window !== "undefined") window.history.back();
                }}
                className="inline-flex items-center gap-2 rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-transform hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:focus-visible:ring-offset-slate-950"
              >
                <ArrowLeftIcon />
                Go back
              </button>
              <a
                href="/dashboard"
                className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
              >
                Dashboard
              </a>
            </div>
          </motion.div>
        </div>

        {/* Two-column body */}
        <div className="mt-14 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
          {/* Request access card */}
          <motion.div
            {...fade(0.3)}
            className="rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur sm:p-8 dark:border-slate-800 dark:bg-slate-900/60"
          >
            <div className="flex items-center gap-3">
              <span className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-100 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-300">
                <KeyIcon />
              </span>
              <div>
                <h2 className="text-lg font-bold tracking-tight">Request access</h2>
                <p className="text-sm text-slate-500 dark:text-slate-400">Goes to your workspace admin for review.</p>
              </div>
            </div>

            {ticket ? (
              <div className="mt-6 rounded-xl border border-emerald-300/70 bg-emerald-50 p-5 text-sm dark:border-emerald-500/30 dark:bg-emerald-500/10">
                <div className="flex items-center gap-2 font-semibold text-emerald-700 dark:text-emerald-300">
                  <CheckIcon />
                  Request sent
                </div>
                <p className="mt-2 text-emerald-800/90 dark:text-emerald-200/80">
                  We routed your request to the Finance admins. Most reviews finish within one business day.
                </p>
                <div className="mt-4 flex items-center justify-between gap-3 rounded-lg border border-emerald-300/60 bg-white/70 px-3 py-2 dark:border-emerald-500/20 dark:bg-slate-900/50">
                  <span className="font-mono text-sm font-semibold text-emerald-800 dark:text-emerald-200">{ticket}</span>
                  <button
                    type="button"
                    onClick={() => copy(ticket, "ticket")}
                    className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-semibold text-emerald-700 transition-colors hover:bg-emerald-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-50 dark:text-emerald-300 dark:hover:bg-emerald-500/10 dark:focus-visible:ring-offset-slate-950"
                  >
                    {copied === "ticket" ? <CheckIcon /> : <CopyIcon />}
                    {copied === "ticket" ? "Copied" : "Copy ID"}
                  </button>
                </div>
                <button
                  type="button"
                  onClick={reset}
                  className="mt-4 text-sm font-semibold text-emerald-700 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-50 dark:text-emerald-300 dark:focus-visible:ring-offset-slate-950"
                >
                  Submit another request
                </button>
              </div>
            ) : (
              <form onSubmit={submit} noValidate className="mt-6 space-y-5">
                <div>
                  <label htmlFor={emailId} className="block text-sm font-medium text-slate-700 dark:text-slate-300">
                    Work email
                  </label>
                  <input
                    id={emailId}
                    type="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    aria-invalid={Boolean(errors.email)}
                    aria-describedby={errors.email ? emailErrId : undefined}
                    placeholder="you@northwind.example"
                    className="mt-1.5 w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus-visible:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-950/50 dark:text-slate-100 dark:placeholder:text-slate-500"
                  />
                  {errors.email && (
                    <p id={emailErrId} role="alert" className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400">
                      {errors.email}
                    </p>
                  )}
                </div>

                <div>
                  <label htmlFor={levelId} className="block text-sm font-medium text-slate-700 dark:text-slate-300">
                    Access level
                  </label>
                  <div className="relative mt-1.5">
                    <select
                      id={levelId}
                      value={level}
                      onChange={(e) => setLevel(e.target.value as AccessLevel)}
                      className="w-full appearance-none rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 pr-10 text-sm text-slate-900 shadow-sm focus-visible:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-950/50 dark:text-slate-100"
                    >
                      {LEVELS.map((l) => (
                        <option key={l.value} value={l.value}>
                          {l.label} — {l.hint}
                        </option>
                      ))}
                    </select>
                    <span aria-hidden className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-slate-400">
                      <ChevronIcon />
                    </span>
                  </div>
                </div>

                <div>
                  <label htmlFor={reasonId} className="block text-sm font-medium text-slate-700 dark:text-slate-300">
                    Why do you need it?
                  </label>
                  <textarea
                    id={reasonId}
                    value={reason}
                    onChange={(e) => setReason(e.target.value)}
                    rows={3}
                    aria-invalid={Boolean(errors.reason)}
                    aria-describedby={errors.reason ? reasonErrId : undefined}
                    placeholder="Reconciling Q3 vendor invoices before the board review on Friday."
                    className="mt-1.5 w-full resize-none rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus-visible:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-950/50 dark:text-slate-100 dark:placeholder:text-slate-500"
                  />
                  {errors.reason && (
                    <p id={reasonErrId} role="alert" className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400">
                      {errors.reason}
                    </p>
                  )}
                </div>

                <button
                  type="submit"
                  className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 focus-visible:outline-none 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-950"
                >
                  <SendIcon />
                  Send access request
                </button>
              </form>
            )}
            <p ref={liveRef} aria-live="polite" className="sr-only">
              {copied ? "Copied to clipboard." : ""}
            </p>
          </motion.div>

          {/* Options + disclosure */}
          <motion.div {...fade(0.36)} className="flex flex-col gap-6">
            <div className="rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur sm:p-8 dark:border-slate-800 dark:bg-slate-900/60">
              <h2 className="text-lg font-bold tracking-tight">What you can do</h2>
              <ul className="mt-4 space-y-1">
                {[
                  {
                    icon: <GridIcon />,
                    title: "Return to your dashboard",
                    body: "Everything your Analyst role can open, in one place.",
                    href: "/dashboard",
                    cta: "Open dashboard",
                  },
                  {
                    icon: <SwapIcon />,
                    title: "Switch account",
                    body: "Sign in with an account that has Finance access.",
                    href: "/login",
                    cta: "Switch account",
                  },
                  {
                    icon: <BookIcon />,
                    title: "Read the access policy",
                    body: "See how workspace permissions and scopes are granted.",
                    href: "/docs/permissions",
                    cta: "View policy",
                  },
                ].map((it) => (
                  <li key={it.title}>
                    <a
                      href={it.href}
                      className="group -mx-2 flex items-start gap-3 rounded-xl px-2 py-3 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:hover:bg-slate-800/60 dark:focus-visible:ring-offset-slate-950"
                    >
                      <span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-slate-600 transition-colors group-hover:bg-indigo-100 group-hover:text-indigo-600 dark:bg-slate-800 dark:text-slate-300 dark:group-hover:bg-indigo-500/15 dark:group-hover:text-indigo-300">
                        {it.icon}
                      </span>
                      <span className="min-w-0 flex-1">
                        <span className="flex items-center justify-between gap-2">
                          <span className="text-sm font-semibold text-slate-800 dark:text-slate-100">{it.title}</span>
                          <span className="translate-x-0 text-slate-400 transition-transform group-hover:translate-x-0.5 dark:text-slate-500">
                            <ArrowRightIcon />
                          </span>
                        </span>
                        <span className="mt-0.5 block text-sm text-slate-500 dark:text-slate-400">{it.body}</span>
                      </span>
                    </a>
                  </li>
                ))}
              </ul>
            </div>

            <div className="rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur sm:p-8 dark:border-slate-800 dark:bg-slate-900/60">
              <button
                type="button"
                onClick={() => setShowWhy((v) => !v)}
                aria-expanded={showWhy}
                aria-controls={whyId}
                className="flex w-full items-center justify-between gap-3 rounded-lg text-left focus-visible:outline-none 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-950"
              >
                <span className="text-sm font-bold tracking-tight text-slate-800 dark:text-slate-100">
                  Why am I seeing this?
                </span>
                <span
                  aria-hidden
                  className={`text-slate-400 transition-transform duration-200 ${showWhy ? "rotate-180" : ""}`}
                >
                  <ChevronIcon />
                </span>
              </button>
              <div id={whyId} hidden={!showWhy} className="mt-4">
                <ul className="space-y-2.5">
                  {REASONS.map((r) => (
                    <li key={r} className="flex items-start gap-2.5 text-sm text-slate-600 dark:text-slate-400">
                      <span className="mt-0.5 text-amber-500 dark:text-amber-400">
                        <DotIcon />
                      </span>
                      {r}
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          </motion.div>
        </div>

        {/* Support footer */}
        <motion.p
          {...fade(0.42)}
          className="mt-10 text-center text-sm text-slate-500 dark:text-slate-500"
        >
          Still blocked? Reach your workspace admin at{" "}
          <a
            href="mailto:access@northwind.example"
            className="font-semibold text-indigo-600 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-300 dark:focus-visible:ring-offset-slate-950"
          >
            access@northwind.example
          </a>
          .
        </motion.p>
      </div>
    </section>
  );
}

function ShieldMark({ uid }: { uid: string }) {
  const clip = `${uid}-shield-clip`;
  const grad = `${uid}-shield-grad`;
  return (
    <div className="relative flex h-32 w-32 items-center justify-center rounded-3xl border border-slate-200 bg-white shadow-lg shadow-rose-500/10 dark:border-slate-800 dark:bg-slate-900">
      <div className="e403-glow absolute inset-0 rounded-3xl bg-rose-400/25 blur-xl dark:bg-rose-500/20" />
      <svg viewBox="0 0 120 140" className="relative h-24 w-24" role="img" aria-label="Locked security shield">
        <defs>
          <linearGradient id={grad} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stopColor="#f43f5e" />
            <stop offset="1" stopColor="#e11d48" />
          </linearGradient>
          <clipPath id={clip}>
            <path d="M60 8 L108 26 V64 C108 96 88 120 60 132 C32 120 12 96 12 64 V26 Z" />
          </clipPath>
        </defs>
        <path
          d="M60 8 L108 26 V64 C108 96 88 120 60 132 C32 120 12 96 12 64 V26 Z"
          className="fill-rose-50 stroke-rose-300 dark:fill-rose-500/10 dark:stroke-rose-500/40"
          strokeWidth="3"
        />
        <g clipPath={`url(#${clip})`}>
          <rect x="0" y="0" width="120" height="10" className="e403-scan fill-rose-400/70 dark:fill-rose-400/60" />
        </g>
        {/* Padlock */}
        <path
          d="M48 64 V56 a12 12 0 0 1 24 0 V64"
          fill="none"
          stroke={`url(#${grad})`}
          strokeWidth="6"
          strokeLinecap="round"
        />
        <rect x="42" y="64" width="36" height="30" rx="6" fill={`url(#${grad})`} />
        <circle cx="60" cy="76" r="4.5" className="fill-white" />
        <rect x="57.8" y="78" width="4.4" height="9" rx="2.2" className="fill-white" />
      </svg>
    </div>
  );
}

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

function CopyIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <rect x="9" y="9" width="13" height="13" rx="2" />
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
    </svg>
  );
}

function CheckIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M20 6 9 17l-5-5" />
    </svg>
  );
}

function ArrowLeftIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M19 12H5" />
      <path d="M12 19l-7-7 7-7" />
    </svg>
  );
}

function ArrowRightIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M5 12h14" />
      <path d="M12 5l7 7-7 7" />
    </svg>
  );
}

function ChevronIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M6 9l6 6 6-6" />
    </svg>
  );
}

function KeyIcon() {
  return (
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <circle cx="7.5" cy="15.5" r="4.5" />
      <path d="M10.5 12.5 21 2" />
      <path d="M16 7l3 3" />
    </svg>
  );
}

function SendIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M22 2 11 13" />
      <path d="M22 2 15 22l-4-9-9-4 20-7Z" />
    </svg>
  );
}

function GridIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <rect x="3" y="3" width="7" height="7" rx="1.5" />
      <rect x="14" y="3" width="7" height="7" rx="1.5" />
      <rect x="3" y="14" width="7" height="7" rx="1.5" />
      <rect x="14" y="14" width="7" height="7" rx="1.5" />
    </svg>
  );
}

function SwapIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M17 4l3 3-3 3" />
      <path d="M20 7H9a5 5 0 0 0-5 5" />
      <path d="M7 20l-3-3 3-3" />
      <path d="M4 17h11a5 5 0 0 0 5-5" />
    </svg>
  );
}

function BookIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
      <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z" />
    </svg>
  );
}

function DotIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
      <circle cx="12" cy="12" r="5" />
    </svg>
  );
}

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 →