Web InnoventixFreeCode

Label Divider

Original · free

divider with centred label

byWeb InnoventixReact + Tailwind
divlabeldividers
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/div-label.json
div-label.tsx
"use client";

import { useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type StyleId = "solid" | "dashed" | "gradient";

type Section = {
  id: string;
  label: string;
  tag: string;
  tone: string;
  items: { title: string; body: string }[];
};

const STYLES: { id: StyleId; label: string }[] = [
  { id: "solid", label: "Solid" },
  { id: "dashed", label: "Dashed" },
  { id: "gradient", label: "Gradient" },
];

const SECTIONS: Section[] = [
  {
    id: "new",
    label: "New this release",
    tag: "New",
    tone: "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300",
    items: [
      {
        title: "Timeline view",
        body: "Drag issues across a horizontal calendar to reschedule. Dependencies redraw as you move them, and weekends collapse so multi-month projects stay readable.",
      },
      {
        title: "Saved views",
        body: "Pin any combination of filter, sort, and grouping to the sidebar. Views sync across your team so everyone opens the board the same way.",
      },
    ],
  },
  {
    id: "improved",
    label: "Improvements",
    tag: "Improved",
    tone: "bg-sky-50 text-sky-700 dark:bg-sky-500/10 dark:text-sky-300",
    items: [
      {
        title: "Faster board loads",
        body: "Boards with more than 500 cards now render in under 400ms by streaming columns as you scroll instead of hydrating all of them at once.",
      },
      {
        title: "Keyboard command bar",
        body: "Press Cmd K anywhere to jump to a project, assign yourself, or change a status without ever reaching for the mouse.",
      },
    ],
  },
  {
    id: "fixed",
    label: "Fixes",
    tag: "Fixed",
    tone: "bg-violet-50 text-violet-700 dark:bg-violet-500/10 dark:text-violet-300",
    items: [
      {
        title: "Comment mentions",
        body: "Typing @ inside a fenced code block no longer opens the mention menu, so pasted snippets keep their exact formatting.",
      },
      {
        title: "Timezone drift",
        body: "Due dates set near midnight kept shifting a day for users east of UTC. Dates are now stored and displayed in your local zone.",
      },
    ],
  },
  {
    id: "deprecated",
    label: "Deprecations",
    tag: "Deprecated",
    tone: "bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-300",
    items: [
      {
        title: "Legacy API v1",
        body: "The v1 REST endpoints retire on 30 September 2026. Migrate to v2, which returns cursor-based pagination and strongly typed webhook payloads.",
      },
    ],
  },
];

export default function DivLabel() {
  const uid = useId();
  const reduced = useReducedMotion();
  const [style, setStyle] = useState<StyleId>("gradient");
  const [open, setOpen] = useState<string[]>([SECTIONS[0].id]);

  const allOpen = SECTIONS.every((s) => open.includes(s.id));

  const toggle = (id: string) =>
    setOpen((cur) =>
      cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id],
    );

  const renderLine = (side: "l" | "r") => {
    if (style === "dashed") {
      return (
        <span
          aria-hidden
          className="h-0 flex-1 border-t border-dashed border-slate-300 dark:border-slate-600"
        />
      );
    }
    if (style === "gradient") {
      const grad =
        side === "l"
          ? "bg-gradient-to-r from-transparent via-indigo-400/50 to-indigo-500/80 dark:via-indigo-400/30 dark:to-indigo-400/70"
          : "bg-gradient-to-r from-indigo-500/80 via-indigo-400/50 to-transparent dark:from-indigo-400/70 dark:via-indigo-400/30";
      return (
        <span
          aria-hidden
          className={`h-px flex-1 rounded-full ${grad} ${reduced ? "" : "divlbl-shimmer"}`}
          style={reduced ? undefined : { backgroundSize: "200% 100%" }}
        />
      );
    }
    return (
      <span
        aria-hidden
        className="h-px flex-1 rounded-full bg-slate-200 dark:bg-slate-700"
      />
    );
  };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8 sm:py-24">
      <div className="mx-auto max-w-3xl">
        <header>
          <p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
            Release notes
          </p>
          <h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
            Meridian 2.4
          </h2>
          <p className="mt-3 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Shipped 16 July 2026. Expand a section to read the details, and
            switch the divider treatment to see how a centred label sits on
            each style.
          </p>
        </header>

        <div className="mb-1 mt-10 flex flex-wrap items-center justify-between gap-4">
          <div className="flex items-center gap-3">
            <span className="text-sm font-medium text-slate-500 dark:text-slate-400">
              Style
            </span>
            <fieldset>
              <legend className="sr-only">Divider line style</legend>
              <div className="inline-flex rounded-lg border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900">
                {STYLES.map((opt) => {
                  const active = style === opt.id;
                  return (
                    <label key={opt.id} className="relative cursor-pointer">
                      <input
                        type="radio"
                        name={`${uid}-style`}
                        value={opt.id}
                        checked={active}
                        onChange={() => setStyle(opt.id)}
                        className="peer sr-only"
                      />
                      <span
                        className={`block rounded-md px-3 py-1.5 text-sm font-medium transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-slate-100 dark:peer-focus-visible:ring-offset-slate-900 ${
                          active
                            ? "bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white"
                            : "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
                        }`}
                      >
                        {opt.label}
                      </span>
                    </label>
                  );
                })}
              </div>
            </fieldset>
          </div>

          <button
            type="button"
            onClick={() =>
              setOpen(allOpen ? [] : SECTIONS.map((s) => s.id))
            }
            className="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-slate-400 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-950"
          >
            {allOpen ? "Collapse all" : "Expand all"}
          </button>
        </div>

        <div className="mt-2">
          {SECTIONS.map((s) => {
            const isOpen = open.includes(s.id);
            const btnId = `${uid}-btn-${s.id}`;
            const panelId = `${uid}-panel-${s.id}`;
            return (
              <div key={s.id} className="py-3">
                <div className="flex items-center gap-4">
                  {renderLine("l")}
                  <button
                    id={btnId}
                    type="button"
                    aria-expanded={isOpen}
                    aria-controls={panelId}
                    onClick={() => toggle(s.id)}
                    className="group inline-flex shrink-0 items-center gap-2.5 rounded-full border border-slate-200 bg-white py-1.5 pl-2 pr-3.5 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:border-slate-300 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-slate-600 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
                  >
                    <span
                      className={`rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${s.tone}`}
                    >
                      {s.tag}
                    </span>
                    <span>{s.label}</span>
                    <motion.svg
                      viewBox="0 0 20 20"
                      aria-hidden
                      className="h-4 w-4 text-slate-400 dark:text-slate-500"
                      animate={{ rotate: isOpen ? 180 : 0 }}
                      transition={
                        reduced ? { duration: 0 } : { duration: 0.2, ease: "easeOut" }
                      }
                    >
                      <path
                        d="M5 7.5 10 12.5 15 7.5"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="1.75"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </motion.svg>
                  </button>
                  {renderLine("r")}
                </div>

                <AnimatePresence initial={false}>
                  {isOpen && (
                    <motion.div
                      key="panel"
                      id={panelId}
                      role="region"
                      aria-labelledby={btnId}
                      initial={{ height: 0, opacity: 0 }}
                      animate={{ height: "auto", opacity: 1 }}
                      exit={{ height: 0, opacity: 0 }}
                      transition={
                        reduced
                          ? { duration: 0 }
                          : { duration: 0.28, ease: [0.22, 1, 0.36, 1] }
                      }
                      className="overflow-hidden"
                    >
                      <ul className="grid gap-3 px-1 pb-1 pt-5 sm:grid-cols-2">
                        {s.items.map((item) => (
                          <li
                            key={item.title}
                            className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900/60"
                          >
                            <h3 className="text-sm font-semibold text-slate-900 dark:text-slate-100">
                              {item.title}
                            </h3>
                            <p className="mt-1.5 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                              {item.body}
                            </p>
                          </li>
                        ))}
                      </ul>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            );
          })}
        </div>
      </div>

      <style>{`
        @keyframes divlbl-shimmer {
          0% { background-position: 0% 50%; }
          100% { background-position: -200% 50%; }
        }
        .divlbl-shimmer {
          animation: divlbl-shimmer 3.6s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .divlbl-shimmer { animation: none !important; }
        }
      `}</style>
    </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 →