Framer Motion in a single HTML file

Framer Motion is a JavaScript animation library, now published as Motion. Two script tags are enough to run it in one HTML file, with or without React.

Framer Motion here means the JavaScript animation library, not the Framer website builder. It is now published as Motion, and it runs in a plain HTML file: add one script tag, read animate from the global Motion object, and call it on an element.

Try the smallest version first. Press the buttons.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Motion animate()</title>
<style>
  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .track { height: 72px; border-radius: 12px; background: #fff; box-shadow: inset 0 0 0 1px #e1e4ea; display: flex; align-items: center; padding: 0 8px; }
  #box { width: 56px; height: 56px; border-radius: 12px; background: #2563eb; }
  .row { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
  button { font: inherit; padding: 9px 14px; border: 0; border-radius: 8px; background: #1d2330; color: #fff; cursor: pointer; }
  #out { margin-top: 12px; font-size: 14px; color: #4b5563; min-height: 20px; }
</style>
</head>
<body>
<div class="track" id="track"><div id="box"></div></div>
<div class="row">
  <button id="slide">Slide</button>
  <button id="pop">Pop</button>
</div>
<div id="out">Press a button.</div>

<!-- Motion, pinned to one version. The script puts everything on window.Motion -->
<script src="https://cdn.jsdelivr.net/npm/motion@13.4.6/dist/motion.js"></script>
<script>
  const { animate } = Motion;
  const box = document.getElementById('box');
  const track = document.getElementById('track');
  const out = document.getElementById('out');
  let right = false;

  document.getElementById('slide').addEventListener('click', () => {
    right = !right;
    const distance = track.clientWidth - box.offsetWidth - 16;
    // animate(element, values, options): x is a transform shorthand
    animate(box, { x: right ? distance : 0 }, { type: 'spring', bounce: 0.3 })
      .finished.then(() => { out.textContent = 'Slide finished'; });
  });

  document.getElementById('pop').addEventListener('click', () => {
    // an array is a list of keyframes: 1 -> 1.4 -> 1
    animate(box, { scale: [1, 1.4, 1] }, { duration: 0.4 })
      .finished.then(() => { out.textContent = 'Pop finished'; });
  });
</script>
</body>
</html>
One script tag and one function call. Slide uses a spring, Pop uses keyframes. Edit the code and the example reruns.

The same file works with no build step, no install and no React. That is the route this guide starts with. The React component API comes after it.

What the name means today

The library began as Framer Motion for React. Its authors then moved it into its own project, called Motion, and added APIs that work without React. Framer's dictionary says Motion was previously called Framer Motion and is an animation library for React and JavaScript.

For you, that gives two names for the same family of code. Searching for "framer motion react" still finds the React side. The vanilla functions such as animate are the newer part.

The same library from an app with a build step, and from one HTML file with script tags.
The same library from an app with a build step, and from one HTML file with script tags.

The smallest example, line by line

The example above has three moving parts.

<script src="https://cdn.jsdelivr.net/npm/motion@13.4.6/dist/motion.js"></script>
<script>
  const { animate } = Motion;
  animate(box, { x: 200 }, { duration: 0.5 });
</script>
The three arguments of animate: what moves, the values to reach, and how to get there.
The three arguments of animate: what moves, the values to reach, and how to get there.
  1. The element. A DOM element, or a CSS selector.
  2. The values. An object of targets. x and scale are transform shortcuts, and a list such as [1, 1.4, 1] plays as keyframes.
  3. The options. Durations are in seconds. type: 'spring' swaps the timed curve for a spring.

animate returns controls. The finished property is a promise, so .finished.then(...) runs code when the animation ends. The controls also have pause(), play(), stop() and cancel().

Pin the version

Every script address in these examples contains a version number. Keep it. A link that says latest can change under a page that worked yesterday, and Motion's own quick start recommends replacing latest with a specific version.

The examples use version 13.4.6, the newest on npm when this guide was written on 1 October 2026. Change the number only after you have tried the page with the new one.

The React motion component in one file

In a React project you write motion.div and give it animation props. In a bundled app that looks like this.

import { motion } from "motion/react"

<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} />

A plain script has no bundler, and import works only in a module script. A bare name such as motion/react also needs an import map. The browser build avoids both by putting everything on window.Motion.

It needs React in place first. JSX is a syntax extension that must be transformed into plain JavaScript, so the example below calls React.createElement directly.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>framer-motion in one HTML file</title>
<style>
  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  button { font: inherit; padding: 10px 16px; border: 0; border-radius: 10px; background: #1d2330; color: #fff; cursor: pointer; }
  .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
  ul { list-style: none; margin: 0; padding: 0; }
  li { background: #fff; border-radius: 10px; padding: 11px 14px; margin-bottom: 8px; box-shadow: 0 1px 0 #e1e4ea, 0 4px 12px rgba(0,0,0,.06); display: flex; justify-content: space-between; align-items: center; }
  li button { padding: 4px 10px; background: #e5e7eb; color: #1d2330; font-size: 13px; }
</style>
</head>
<body>
<div id="root"></div>

<!-- Order matters: React, ReactDOM, then framer-motion, then your code -->
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/framer-motion@13.4.6/dist/framer-motion.js"></script>
<script>
  const { motion, AnimatePresence } = Motion;   // the browser build exposes window.Motion
  const h = React.createElement;                // no JSX without a compiler
  let nextId = 3;

  function App() {
    const [items, setItems] = React.useState([{ id: 1, text: 'First note' }, { id: 2, text: 'Second note' }]);
    const add = () => setItems((l) => [...l, { id: nextId, text: 'Note ' + nextId++ }]);
    const remove = (id) => setItems((l) => l.filter((x) => x.id !== id));

    return h('div', null,
      h('div', { className: 'row' },
        h(motion.button, { onClick: add, whileHover: { scale: 1.06 }, whileTap: { scale: 0.94 } }, 'Add note'),
        h('span', null, items.length + ' notes')),
      h('ul', null,
        // AnimatePresence plays "exit" before a child leaves; each child needs a stable key
        h(AnimatePresence, null,
          items.map((it) =>
            h(motion.li, {
              key: it.id,
              initial: { opacity: 0, y: -12 },
              animate: { opacity: 1, y: 0 },
              exit: { opacity: 0, x: 60 },
              transition: { duration: 0.25 },
            }, it.text, h('button', { onClick: () => remove(it.id) }, 'Remove'))))));
  }

  ReactDOM.createRoot(document.getElementById('root')).render(h(App));
</script>
</body>
</html>
React 18, ReactDOM and framer-motion from script tags. Add a note, then remove one and watch it slide out.

Three details make this work:

  • Load order. React, ReactDOM, framer-motion, then your own script.
  • One alias. const h = React.createElement keeps the calls short.
  • Exit animations. AnimatePresence detects a direct child leaving and plays its exit first. Each child needs a stable key, so avoid array positions.
React before framer-motion works. The reverse order stops with React is not defined.
React before framer-motion works. The reverse order stops with React is not defined.

React 18 is used because React 19 no longer ships the script-tag build that this approach loads. React's upgrade guide points to ES module CDNs for that case, which is a different setup from this one.

A finished page: cards that reveal as you scroll

The last example combines three functions: inView runs code when an element enters the viewport, stagger spaces the starts apart, and animate does the movement.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Staggered reveal with Motion</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  header { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 14px; }
  h1 { font-size: 18px; margin: 0; }
  button { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #1d2330; color: #fff; cursor: pointer; }
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
  .card { background: #fff; border-radius: 12px; padding: 14px; height: 110px; box-shadow: 0 1px 0 #e1e4ea, 0 6px 16px rgba(0,0,0,.07); }
  .card b { display: block; margin-bottom: 4px; }
  .card span { font-size: 13px; color: #6b7280; }
  /* hide cards only if Motion really loaded, so a blocked CDN never leaves a blank page */
  .motion .card { opacity: 0; }
</style>
</head>
<body>
<header><h1>Reveal on scroll</h1><button id="replay">Replay</button></header>
<div class="grid" id="grid"></div>

<script src="https://cdn.jsdelivr.net/npm/motion@13.4.6/dist/motion.js"></script>
<script>
  const grid = document.getElementById('grid');
  const names = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel', 'India', 'Juliet', 'Kilo', 'Lima'];
  grid.innerHTML = names.map((n, i) => '<div class="card"><b>' + n + '</b><span>Card ' + (i + 1) + '</span></div>').join('');
  const cards = [...document.querySelectorAll('.card')];

  if (window.Motion) {
    const { animate, stagger, inView } = Motion;
    document.documentElement.classList.add('motion');
    // honour the visitor's "reduce motion" setting: skip the movement, keep the fade
    const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

    const show = () => reduce ? { opacity: 1 } : { opacity: 1, y: [24, 0] };

    // inView runs the callback when a card enters the viewport
    inView('.card', (card) => {
      animate(card, show(), { duration: 0.4, delay: (cards.indexOf(card) % 3) * 0.08 });
    }, { margin: '0px 0px -40px 0px' });

    document.getElementById('replay').addEventListener('click', () => {
      window.scrollTo(0, 0);
      animate(cards, { opacity: 0 }, { duration: 0 });
      // stagger(0.08) gives each card in the list a delay 0.08 s longer than the one before
      animate(cards.slice(0, 6), show(), { duration: 0.4, delay: stagger(0.08) });
    });
  }
</script>
</body>
</html>
Scroll inside the example. Each card fades and rises as it comes into view. Replay runs the first screen again.
  • Hide only when Motion loaded. The script adds a class after the library is present, and the CSS hides cards only under that class. A blocked CDN then leaves a readable page instead of a blank one.
  • Reduced motion. The code asks matchMedia whether the visitor chose reduced motion. When it does, cards fade in without the upward move.
  • Stagger. stagger(0.08) gives each element in a list a start 0.08 seconds later than the one before.

In React, the same wish is a MotionConfig setting named reducedMotion, with the values user, always and never. With user, Motion turns off transform animations and keeps changes such as opacity.

For the CSS-only version of this idea, see scroll reveal and CSS transitions. If you only need hover and fade effects, they are enough, and no library is needed.

When it does not work

What you see Cause Fix
Motion is not defined The script did not load: blocked CDN, wrong address, or your code runs first Check the address, and put your script after the library tag
React is not defined framer-motion loaded before React Order the tags: React, ReactDOM, framer-motion
An error about an import statement import is only allowed in a module script, and a bare name needs an import map Use the global Motion object, or add type="module" and a full address
JSX tags break the script JSX has to be transformed into plain JavaScript first Use React.createElement, or run the file through a compiler
The exit animation never plays The child has no key, or is not a direct child of AnimatePresence Give each child a stable key
Nothing is visible on a slow network Content starts hidden and waits for the library Hide it only after the library is present
Movement is missing for some visitors Their device asks for reduced motion Keep a fade, as the last example does

An animation is hard to judge from a screenshot, and a GIF loses the spring. Sending the page lets people press, hover and scroll it themselves.

The page also needs its viewport meta tag to look right on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can try the animation themselves. Scripts from jsDelivr and unpkg load. If you change the code later, the same link shows the new version.

Questions people ask

Is Framer Motion the same thing as Framer?

No. Framer Motion is an animation library for code, now published as Motion at motion.dev. Framer is a visual tool for designing and publishing sites. Framer's own page on the topic says the site builder has Motion-powered animation tools built in, so you do not install anything there.

What happened to the framer-motion package?

The project became independent and is now called Motion. The npm package for the library is motion, and React code imports from motion/react. The framer-motion package is still on npm; on 1 October 2026 both listed version 13.4.6 as the latest.

Can I use Framer Motion without npm or a bundler?

Yes. Load the browser build with a script tag from a CDN and read the functions from the global Motion object. For the React component API, load React and ReactDOM first, then the framer-motion browser build, and write elements with React.createElement instead of JSX.

Why does the example use React 18 and not React 19?

React stopped producing UMD builds, the kind a plain script tag loads, with React 19. React's own guide recommends an ES module CDN for React 19. A single file that uses ordinary script tags is simplest with React 18.

Do I need React to animate a page with Motion?

No. The animate function works on ordinary HTML elements and takes a selector or an element. It is the shorter route when a page has no React in it.

Keep reading