Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 02:17:10Get Lifetime →
loopkit
freeNew118 lines · framer-motion

Feature Steps Connector

A line that draws itself from step one to the last. A 'how it works' row of numbered steps joined by a connecting line that draws itself in on mount — horizontal on desktop, a vertical timeline on small screens.

Use it for
A short process explanation (3-5 steps) right after a hero or feature section, where a drawn connector reads as more intentional than a bare numbered list.
Themes
Light + dark
License
Commercial use
Live

How it runs

Four steps, no detours

  1. 1

    Connect

    Point it at the repo, the tracker and the docs.

  2. 2

    Configure

    Pick what runs on its own and what waits for a yes.

  3. 3

    Run

    Plans, tool calls and cost show up in one timeline.

  4. 4

    Review

    Approve the handful of decisions that actually need you.

tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";

function useReducedMotionSafe() {
  const prefersReduced = useReducedMotion();
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
  return mounted ? !!prefersReduced : false;
}

export interface ConnectorStep {
  title: string;
  description: string;
}

export interface FeatureStepsConnectorProps {
  eyebrow?: string;
  title?: string;
  steps?: ConnectorStep[];
  className?: string;
}

const DEFAULT_STEPS: ConnectorStep[] = [
  { title: "Connect", description: "Point it at the repo, the tracker and the docs." },
  { title: "Configure", description: "Pick what runs on its own and what waits for a yes." },
  { title: "Run", description: "Plans, tool calls and cost show up in one timeline." },
  { title: "Review", description: "Approve the handful of decisions that actually need you." },
];

function Dot({ i, active }: { i: number; active: boolean }) {
  return (
    <span
      className={cn(
        "relative z-10 grid size-10 shrink-0 place-items-center rounded-full border-2 font-mono text-[12px] font-bold transition-colors duration-300",
        active ? "border-signal bg-signal text-signal-foreground" : "border-border bg-card text-muted-foreground",
      )}
    >
      {i + 1}
    </span>
  );
}

export function FeatureStepsConnector({ eyebrow = "How it runs", title = "Four steps, no detours", steps = DEFAULT_STEPS, className }: FeatureStepsConnectorProps) {
  const reduce = useReducedMotionSafe();
  const n = steps.length;

  return (
    <section className={cn("w-full bg-background px-4 py-20 sm:px-6 lg:px-10", className)}>
      <div className="mx-auto max-w-3xl text-center">
        <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">{eyebrow}</p>
        <h2 className="mt-3 font-display text-[clamp(2rem,5vw,3.5rem)] font-extrabold leading-[0.95] tracking-[-0.03em] text-foreground">{title}</h2>
      </div>

      {/* Desktop: horizontal connector */}
      <div className="mx-auto mt-16 hidden max-w-5xl lg:block">
        <div className="relative grid grid-cols-4 gap-6">
          <div className="absolute left-0 right-0 top-5 h-[2px] -translate-y-1/2 bg-border" aria-hidden />
          <motion.div
            className="absolute left-0 top-5 h-[2px] origin-left -translate-y-1/2 bg-signal"
            style={{ right: `${100 / n / 2}%` }}
            initial={reduce ? false : { scaleX: 0 }}
            animate={{ scaleX: 1 }}
            transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
            aria-hidden
          />
          {steps.map((s, i) => (
            <motion.div
              key={s.title}
              initial={reduce ? false : { opacity: 0, y: 14 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ duration: 0.45, delay: i * 0.15 }}
              className="flex flex-col items-center text-center"
            >
              <Dot i={i} active />
              <h3 className="mt-4 font-display text-base font-bold tracking-tight text-foreground">{s.title}</h3>
              <p className="mt-1.5 max-w-[14rem] text-sm leading-relaxed text-muted-foreground">{s.description}</p>
            </motion.div>
          ))}
        </div>
      </div>

      {/* Mobile / tablet: vertical connector */}
      <ol className="mx-auto mt-12 max-w-md space-y-0 lg:hidden">
        {steps.map((s, i) => (
          <motion.li
            key={s.title}
            initial={reduce ? false : { opacity: 0, x: -10 }}
            animate={{ opacity: 1, x: 0 }}
            transition={{ duration: 0.4, delay: i * 0.1 }}
            className="relative flex gap-4 pb-8 last:pb-0"
          >
            {i < n - 1 && (
              <motion.span
                className="absolute left-5 top-10 w-[2px] origin-top bg-signal"
                style={{ height: "calc(100% - 1rem)" }}
                initial={reduce ? false : { scaleY: 0 }}
                animate={{ scaleY: 1 }}
                transition={{ duration: 0.6, delay: i * 0.1 + 0.15 }}
                aria-hidden
              />
            )}
            <Dot i={i} active />
            <div className="pt-1.5">
              <h3 className="font-display text-base font-bold tracking-tight text-foreground">{s.title}</h3>
              <p className="mt-1 text-sm leading-relaxed text-muted-foreground">{s.description}</p>
            </div>
          </motion.li>
        ))}
      </ol>
    </section>
  );
}

export default FeatureStepsConnector;

How it behaves

  • →On desktop the connecting line between step dots scales in from the left as soon as the section mounts, so it reads cleanly even with no scrolling.
  • →On small screens the same steps stack vertically with a connecting line that draws top-to-bottom per step, staggered slightly after each dot.
  • →Step dots and text spring in with a small per-step delay for a sense of sequence.
  • →All entrance and line-drawing animation is skipped under prefers-reduced-motion; steps appear in their final state immediately.

Accessibility

  • ✓Steps are a real ordered list (`<ol>`) on mobile and a labelled grid on desktop, so the sequence is preserved for screen readers either way.
  • ✓Connecting lines are `aria-hidden`; numbering and heading text carry the actual meaning.
  • ✓Honours prefers-reduced-motion.