Retour au catalogue

Trust Animated Shield

Grande icone shield SVG qui se dessine au scroll via stroke animation. Les certifications apparaissent ensuite en orbite autour du shield avec stagger.

trustcomplex Both Responsive a11y
corporateelegantminimalsaasecommerceuniversalcentered
Theme
"use client";

import { useRef } from "react";
import { motion, useInView } from "framer-motion";

interface Certification {
  id: string;
  label: string;
  description: string;
}

interface TrustAnimatedShieldProps {
  title?: string;
  subtitle?: string;
  certifications?: Certification[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

function ShieldSVG({ isInView }: { isInView: boolean }) {
  return (
    <svg
      viewBox="0 0 100 120"
      fill="none"
      style={{ width: 120, height: 144, margin: "0 auto", display: "block" }}
    >
      <motion.path
        d="M50 4 L92 24 V60 C92 88 50 116 50 116 C50 116 8 88 8 60 V24 L50 4Z"
        stroke="var(--color-accent)"
        strokeWidth="2.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        fill="none"
        initial={{ pathLength: 0, opacity: 0 }}
        animate={isInView ? { pathLength: 1, opacity: 1 } : {}}
        transition={{ duration: 1.8, ease: "easeInOut" }}
      />
      <motion.path
        d="M50 4 L92 24 V60 C92 88 50 116 50 116 C50 116 8 88 8 60 V24 L50 4Z"
        fill="var(--color-accent)"
        initial={{ opacity: 0 }}
        animate={isInView ? { opacity: 0.08 } : {}}
        transition={{ duration: 0.6, delay: 1.6 }}
      />
      <motion.path
        d="M35 58 L46 69 L67 48"
        stroke="var(--color-accent)"
        strokeWidth="3.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        fill="none"
        initial={{ pathLength: 0, opacity: 0 }}
        animate={isInView ? { pathLength: 1, opacity: 1 } : {}}
        transition={{ duration: 0.6, delay: 1.8, ease: EASE }}
      />
    </svg>
  );
}

export default function TrustAnimatedShield({
  title = "Securite et conformite",
  subtitle = "Vos donnees sont protegees par les standards les plus stricts.",
  certifications = [],
}: TrustAnimatedShieldProps) {
  const ref = useRef<HTMLDivElement>(null);
  const isInView = useInView(ref, { once: true, margin: "-80px" });

  const radius = 180;
  const count = certifications.length;

  return (
    <section style={{ padding: "var(--section-padding-y) 0", background: "var(--color-background)" }}>
      <div ref={ref} style={{ maxWidth: "var(--container-max-width)", margin: "0 auto", padding: "0 var(--container-padding-x)", textAlign: "center" }}>
        <motion.div initial={{ opacity: 0, y: 16 }} animate={isInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, ease: EASE }}>
          <h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.75rem, 3.5vw, 3rem)", fontWeight: 700, lineHeight: 1.1, letterSpacing: "-0.02em", color: "var(--color-foreground)", marginBottom: "0.5rem" }}>
            {title}
          </h2>
          <p style={{ fontSize: "1.0625rem", color: "var(--color-foreground-muted)", marginBottom: "3rem", maxWidth: "480px", margin: "0 auto 3rem" }}>
            {subtitle}
          </p>
        </motion.div>

        <div style={{ position: "relative", width: radius * 2 + 140, height: radius * 2 + 140, margin: "0 auto" }}>
          {/* Shield center */}
          <div style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }}>
            <ShieldSVG isInView={isInView} />
          </div>

          {/* Orbit ring */}
          <motion.div
            aria-hidden
            initial={{ opacity: 0, scale: 0.8 }}
            animate={isInView ? { opacity: 1, scale: 1 } : {}}
            transition={{ duration: 0.8, delay: 2, ease: EASE }}
            style={{
              position: "absolute",
              top: "50%",
              left: "50%",
              transform: "translate(-50%, -50%)",
              width: radius * 2,
              height: radius * 2,
              borderRadius: "50%",
              border: "1px dashed var(--color-border)",
              pointerEvents: "none",
            }}
          />

          {/* Certification badges orbiting */}
          {certifications.map((cert, i) => {
            const angle = (i / count) * Math.PI * 2 - Math.PI / 2;
            const x = Math.cos(angle) * radius;
            const y = Math.sin(angle) * radius;

            return (
              <motion.div
                key={cert.id}
                initial={{ opacity: 0, scale: 0 }}
                animate={isInView ? { opacity: 1, scale: 1 } : {}}
                transition={{ duration: 0.5, delay: 2.2 + i * 0.15, ease: EASE }}
                style={{
                  position: "absolute",
                  top: "50%",
                  left: "50%",
                  transform: `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`,
                  background: "var(--color-background-card)",
                  border: "1px solid var(--color-border)",
                  borderRadius: "var(--radius-lg)",
                  padding: "0.75rem 1.25rem",
                  textAlign: "center",
                  minWidth: 130,
                  boxShadow: "0 4px 24px color-mix(in srgb, var(--color-accent) 8%, transparent)",
                }}
              >
                <div style={{ fontSize: "0.875rem", fontWeight: 600, color: "var(--color-foreground)", marginBottom: 2 }}>
                  {cert.label}
                </div>
                <div style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
                  {cert.description}
                </div>
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

Trust Animated Shield — React Trust Section — Incubator