Search code examples
reactjssvgtailwind-cssframer-motionsvg-animate

Framer-Motion: How do I animate an SVG's viewbox size?


I have an SVG of a logo that, when on scroll, switches from the full name to just the initials. It sits in a container div with a colored background that should also change size accordingly. So far, I've been able to animate the extra characters out and move the initials to the correct space, but I'm having issues wrapping my mind on how to update/animate the container div's size.

I have it currently working using GSAP, but we're switching over to Framer Motion. It's a bit hard to explain, so here's the current working version: https://pixelbakery.com/about (I hope links are allowed)

Note: we use Tailwind as our styling framework.

My code as of now:

  1. You can ignore this part- it's how I calculate scroll position. I'm adding it just in case you want additional context to some of the props:
  const [isHamActive, setHamToggle] = useState(false)
  const [windowHeight, setwindowHeight] = useState(0)
  const [showNavBar, setShowNavBar] = useState(true)
  const [scrollPosition, setScrollPosition] = useState(0)

  function handleShowNavBar() {
    if (scrollPosition + 1 >= windowHeight / 3) setShowNavBar(false)
    else setShowNavBar(true)
  }
  // Create a window resize event listener
  useEffect(() => {
    setwindowHeight(window.innerHeight)
    const handleWindowResize = () => {
      setwindowHeight(window.innerHeight)
      handleShowNavBar()
    }
    window.addEventListener('resize', handleWindowResize)
    return () => {
      window.removeEventListener('resize', handleWindowResize)
      handleShowNavBar()
    }
  }, [])

  // Create a scroll event listener, and call handleShowNavBar
  useEffect(() => {
    setScrollPosition(window.scrollY + 1)
    handleShowNavBar()
    const handleScroll = () => {
      const position = window.scrollY + 1
      setScrollPosition(position)
      handleShowNavBar()
    }
    window.addEventListener('scroll', handleScroll, { passive: true })

    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [scrollPosition])

  1. The parent component/ full navbar
import { motion, Variants } from 'framer-motion'
import Nav_Logo from './Nav_Logo'
export default function Navbar() {
  const navItem: Variants = {
    offscreen: (delay) => ({
      y: -300,
      transition: {
        ease: 'easeOut',
        duration: 1,
        delay: delay,
      },
    }),
    onscreen: (delay) => ({
      y: 0,
      transition: {
        ease: 'easeOut',
        duration: 1,
        delay: delay,
      },
    }),
  }

 return (
    <>
      <motion.div initial='offscreen' className={'z-40 '}>
        <motion.div
          initial={'offscreen'}
          animate={'onscreen'}
          variants={navItem}
          custom={0.3}
          className='navItem origin-top-left ml-8 mt-8 fixed top-0 left-0 z-40 '
        >
          <div className='bg-cream rounded-md origin-top-left hidden xl:block  '>
            <Link
              hrefLang={'en-US'}
              href={'/'}
              className=' pointer-events-auto block relative min-w-full z-40 px-4 pt-3 my-0 w-full'
            >
              <Nav_Logo showNavBar={showNavBar} />
            </Link>
          </div>
        </motion.div>
      </motion.div>
    </>
  )
}
  1. Here's the SVG logo component
import { Variants, motion } from "framer-motion";

const fadeAway: Variants = {
  hide: (delay) => ({
    opacity: 0,
    transition: {
      ease: "easeOut",
      duration: 1,
      delay: delay,
    },
  }),
  show: (delay) => ({
    opacity: 1,
    transition: {
      ease: "easeOut",
      duration: 1,
      delay: delay,
    },
  }),
};

const Nav_Logo = ({ showNavBar }) => {
  return (
    <motion.svg
      xmlns='http://www.w3.org/2000/svg'
      version='1.1'
      id='Logo_Wordmark'
      viewBox='0 0 494 138'
      width={"100%"}
      preserveAspectRatio='xMaxYMin meet'
    >
      {/* P */}
      <motion.path d='...' />
      {/* IXEL */}
      <motion.g
        variants={fadeAway}
        animate={showNavBar ? "show" : "hide"}
      >  ... </motion.g>
      {/* B */}
      <motion.path
        animate={showNavBar ? {{x: 0}} : {{x: -140}}
        d='...'
      />
      {/* AKERY */}
      <motion.g
        variants={fadeAway}
        animate={showNavBar ? "show" : "hide"} 
      >
       ...
      </motion.g>
      {/* D */}
      <motion.path
        animate={showNavBar ? {{scale: 1}} : {{scale: 1.4, y:-10}}
        ...
      />
      {/* ESIGN */}
      <motion.g
        variants={fadeAway}
        animate={showNavBar ? "show" : "hide"}
      >
        ...
      </motion.g>
      {/* S */}
      <motion.path
        animate={showNavBar ? {{scale: 1}} : {{scale: 1.4, y:-10}}}
        p='...'
      />
      {/* TUDIO */}
      <motion.g
        variants={fadeAway}
        animate={showNavBar ? "show" : "hide"}
      >
        ...
      </motion.g>
    </motion.svg>
  )
}

export default Nav_Logo;

Is changing the viewbox size even the best approach?


Solution

  • I don't know if this is useful, but it was fun to make. You cannot animate the viewBox of an SVG, but if you remove it all values in CSS will refer to the context document. Here I toggle the style small on the SVG.

    document.forms.f1.addEventListener('submit', e => {
      e.preventDefault();
      document.querySelector('svg').classList.toggle('small');
    });
    svg {
      display: block;
      width: 20em;
      height: 7em;
      transition: all 1s;
    }
    
    svg text {
      dominant-baseline: middle;
      text-anchor: middle;
      font-size: 200%;
      font-family: sans-serif;
      font-weight: bold;
      fill: #ff5e64;
      transition: font-size .1s .3s;
    }
    
    svg text:nth-child(even) {
      font-size: 270%;
    }
    
    tspan {
      transition: fill-opacity .3s, font-size .1s .3s;
    }
    
    .small {
      width: 6em;
    }
    
    .small .tail {
      fill-opacity: 0;
      font-size: 0;
    }
    
    .small text:nth-child(odd) {
      font-size: 270%;
    }
    <svg>
      <rect width="100%" height="100%" rx="5%" fill="#efe8f2" />
      <text x="50%" y="40%">
        <tspan class="head">p</tspan><tspan class="tail">ixel&nbsp;</tspan><tspan class="head">b</tspan><tspan class="tail">akery</tspan>
      </text>
      <text x="50%" y="70%" >
        <tspan class="head">d</tspan><tspan class="tail">esign&nbsp;</tspan><tspan class="head">s</tspan><tspan class="tail">tudio</tspan>
      </text>
    </svg>
    
    <form name="f1">
      <button>toggle</button>
    </form>