Open-source project
kitze/react-genie avatar
kitze/react-genie

react-genie: scroll-triggered reveal animations for React

A set of React components for animating elements as they scroll into the viewport

740 stars22 forksTypeScriptMIT

At a glance

What is it?
react-genie is a small set of React components that animate elements as they scroll into the viewport, built on Intersection Observer and styled-components class names. The interesting design choice is a mode prop that lets a reveal either wrap its children in an extra div or clone them and pass the class through.
Who is it for?
Adopt react-genie if you want scroll reveal animations with a small, readable surface and no animation framework behind it, since the whole library is a reveal component, a global styles component and a set of class names. Keep away if you need scroll-driven timelines, pinning or scrubbed animation, because this is a one-shot reveal rather than a scroll-linked animation system.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One component, one global styles block, and a set of class names

react-genie is a set of React components for animating elements as they scroll into the viewport. The description is the whole scope, and the implementation is small enough to read in a sitting.

There are two things you render. The first is RevealGlobalStyles, which you place anywhere in your app and which adds the class names for the animations:

jsx
import {RevealGlobalStyles} from 'react-genie'

const App = () => {
  return (
    <div>
      <RevealGlobalStyles />
      <div> rest of the app </div>
    </div>
  );
};

The second is the Reveal component, which wraps the content you want animated:

jsx
import {Reveal} from 'react-genie';

<Reveal>
  <h1>The default animation is fade in</h1>
</Reveal>

So the model is: one global stylesheet of animation classes, and a component that decides when to apply one. The global component has to exist for the class names to do anything, which is the usual trade with styled-components style class injection, and it is a deliberate one. The library names the classes once, in one place, instead of generating them per component instance.

The project is MIT licensed, written in TypeScript, the default branch is master, the last push was on 2026-10-09, and the repository has no GitHub releases. The version in the package manifest is 0.2.2, and the homepage of the project is a CodeSandbox example rather than a documentation site.

The props, and what each one actually does

The props list is short and worth reading in full, because each entry is a decision you make per element.

The animation prop takes a string, which is an animation class name. The delay prop is a number, the animation delay. The mode prop is a RevealMode, either clone or wrap, and the default is wrap. The debugName prop, if you specify it, gets you console logs. The style prop takes a CSSObject for extra styles. And onShowDone is a callback that fires after the show.

That last one is the prop that makes the library composable. Without a completion callback, a reveal animation is fire and forget, and anything that depends on the animation having finished, such as a sequence of panels or a measurement after layout, has no hook. onShowDone gives you one.

The debugName prop is a smaller version of the same idea. Rather than a global debug flag, you name the individual element you are investigating and get logs for that one, which is a nicer way to work than turning on logging everywhere and reading through the noise.

The style prop deserves a note because it is where the library meets the styled-components world. The style prop takes a CSSObject, which is the styled-components type, so the extra styles you pass are in the same shape as the rest of a styled-components codebase rather than in a separate idiom.

Preset animations, or your own class name

The default animation on a Reveal with no props is a fade in, which is what the quick start example shows. To pick something else you pass the animation prop with one of the exported Animation constants:

jsx
import {Reveal, Animation} from 'react-genie';

<Reveal animation={Animation.SlideInLeft}>
  <h1>This title will slide in from the left</h1>
</Reveal>

The hint in the README is the useful part. You can also pass your own class name here if you have custom animations. That is a small decision with a large effect on how locked in you are. The Animation constants are convenience, and the animation prop takes any class name, so if the preset set does not have what you want, you write the keyframes yourself and pass the class name.

The class names come from the RevealGlobalStyles component, which is where the animations are defined, so a custom animation that follows the same naming will be picked up by the same mechanism.

The dependency list in the README is what makes this work. It names styled-components, layout-styled-components, react-animations and react-intersection-observer. The styled-components pair handles the CSS objects and the global style injection, react-animations provides the animation definitions, and react-intersection-observer is the mechanism that knows when an element has entered the viewport.

Here is the discrepancy to be aware of. The package manifest declares only react-intersection-observer at version 8.24.2 or newer in its dependencies, while the README lists four packages. So depending on your setup, you may find that the styled-components and react-animations packages are not installed for you and you need to add them yourself. That is the sort of thing worth checking on a first install rather than debugging later.

Wrap versus clone, and why the extra div matters

The mode prop is the design decision that makes this library worth an article, and the default choice is the one that surprises people.

The default mode is wrap, so the children element is wrapped in an additional div. The alternative is clone, or RevealMode.Clone:

jsx
import {Reveal, RevealMode} from 'react-genie';

<Reveal mode={RevealMode.Clone}>
  <h1>This h1 will be cloned instead of wrapped</h1>
</Reveal>

The README states the requirement for clone to work, and it is the important constraint: your element needs to accept className, style, and the other props the clone passes through. A custom component that ignores className will not be animated in clone mode, and it will work fine in wrap mode because the wrapper div is what receives the class.

So the trade is between correctness and DOM cleanliness. Wrap always works, because the animation is applied to a div the library controls. Clone keeps your DOM exactly as you wrote it, which matters for flexbox and grid layouts where an extra div changes the layout, for table rows where a div is invalid, and for anything where you care about the rendered markup.

That extra div in wrap mode is a real cost in modern layout. A Reveal inside a flex row becomes a flex child, and the flex properties you set on the original element apply to the wrapper instead. Clone avoids that, at the cost of requiring your component to forward its props.

There is no middle option in the API as documented, so you choose per element.

The example project, the build tooling and the age of the stack

The repository has an example/ directory that is a real, self-contained project rather than a snippet. It has its own package.json, a tsconfig, a yarn.lock, an index.html, an index.tsx and a Home.tsx, and the package manifest has an example script that changes into the example directory and runs yarn start.

The build tooling is the older tsdx setup, with tsdx watch for start, tsdx build for build and tsdx test with a jsdom environment for test. The devDependencies pin tsdx at 0.9.3, TypeScript at 3.6.3, React and React DOM at 16.10.1 and Prettier at 1.18.2. The package is built to a dist directory with a CommonJS main at dist/index.js, an ES module at dist/react-genie.esm.js and typings at dist/index.d.ts.

Those version numbers tell you something important. TypeScript 3.6 and tsdx 0.9 are from 2019, and React 16 predates the concurrent rendering work. The peer dependency is react at 16 or newer, so the library claims compatibility with modern React, but the toolchain it was built with is not the toolchain you would start a project with today.

The husky configuration runs pretty-quick on staged files before a commit, and the Prettier settings are explicit, a print width of 80, semicolons, single quotes and es5 trailing commas. The project also ships a .npmrc and a .yarnrc.yml, and the top level of the tree is a src/ directory alongside the example and the config files.

The library is used on sizzy.co, which is the author's own product, and there is a CodeSandbox example and a live demo.

Where react-genie is the wrong choice

The first wrong case is scroll-driven animation rather than scroll-triggered animation. This library reveals an element once, when it enters the viewport. If you want the animation to scrub with the scroll position, to pin a section while it scrolls, or to have a timeline that advances as the user scrolls, this is the wrong tool and the difference in effort is large. Those effects need a scroll-linked animation system, not a one-shot class toggle.

The second is a project on the modern React stack with a modern build. The tsdx 0.9 and TypeScript 3.6 toolchain is old enough that you will probably end up rebuilding the package rather than consuming it, and the styled-components dependency arrangement is a version question rather than a solved problem. If you are starting fresh with Vite and React 19, wiring this into your project is more work than writing the twenty lines of Intersection Observer logic yourself.

The third is a project that does not want styled-components. The README lists styled-components and layout-styled-components among the dependencies, and the style prop is a CSSObject. If your codebase has standardised on a different styling approach, or on plain CSS modules, you are pulling in a styling library to use a reveal animation.

The fourth is a bundle-size-sensitive site. The library is small, but the class definitions and the animation library behind it are not nothing, and the class names are injected globally by RevealGlobalStyles, so they land in your CSS whether or not the page uses them.

The alternative worth weighing is the platform itself. The Intersection Observer API is in every browser you care about, and a custom hook that observes an element and toggles a class is small, gives you complete control over the trigger options, and adds no dependencies. What react-genie gives you in exchange is the preset animation set, the clone and wrap distinction, the onShowDone callback and the global class name registry, and for many projects that is worth exactly what it costs.

Editorial conclusion

Adopt react-genie if you want scroll reveal animations with a small, readable surface and no animation framework behind it, since the whole library is a reveal component, a global styles component and a set of class names. Keep away if you need scroll-driven timelines, pinning or scrubbed animation, because this is a one-shot reveal rather than a scroll-linked animation system. Before you install it, check the dependency situation, because the README lists styled-components, layout-styled-components, react-animations and react-intersection-observer while the package manifest declares only react-intersection-observer as a runtime dependency, and note that the version is 0.2.2 with no GitHub releases and a peer dependency on React 16 or newer.

Frequently asked questions

How do I add a scroll reveal animation with react-genie?

Render the RevealGlobalStyles component once anywhere in your app so the animation class names are added, then wrap the content you want animated in a Reveal component. The default animation is a fade in, and you pass the animation prop with an Animation constant such as Animation.SlideInLeft to pick another.

What is the difference between wrap and clone mode in react-genie?

The default mode is wrap, so the children are wrapped in an additional div that receives the animation class. Clone passes the class through to your own element instead, keeping the DOM as you wrote it, but it only works if your element accepts className and style, since the props are cloned onto it.

What dependencies does react-genie need?

The README lists styled-components, layout-styled-components, react-animations and react-intersection-observer. The package manifest, however, declares only react-intersection-observer as a runtime dependency, so you may need to add the others yourself when installing.

Can I use my own CSS animation with react-genie?

Yes. The animation prop takes an animation class name, and the README notes you can pass your own class name if you have custom animations, with the classes picked up by the same mechanism that provides the built-in ones.

Official sources

  1. Issues
  2. kitze/react-genie on GitHub
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kitze-react-genie.svg)](https://hysenlabs.com/projects/kitze-react-genie)

Was this useful?