A wrapper that knows when it is on screen: it sets a data attribute for CSS, offers a render function and callbacks, and can run a simple entrance.
effect
fade
effect
slide-up
effect
slide-down
effect
scale
effect
blur
import { InView, type InViewEffect } from "@/components/ballmac/in-view"
const effects: InViewEffect[] = ["fade", "slide-up", "slide-down", "scale", "blur"]
export default function InViewDemo() {
return (
<div className="grid w-full max-w-md grid-cols-2 gap-3 sm:grid-cols-3">
{effects.map((effect, i) => (
<InView key={effect} effect={effect} delay={i * 0.1} amount={0.1} className="rounded-xl border bg-card p-4 text-center">
<p className="font-mono text-xs text-muted-foreground">effect</p>
<p className="text-sm font-semibold">{effect}</p>
</InView>
))}
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/in-viewInstall the dependencies.
$ pnpm add motion@^12Copy the source into your project.
components/ballmac/in-view.tsx// Ballmac UI: In View. https://ui.ballmac.com/components/in-view "use client" import * as React from "react" import { motion, useInView, useReducedMotion } from "motion/react" import { cn } from "@/lib/utils" type InViewEffect = "none" | "fade" | "slide-up" | "slide-down" | "scale" | "blur" const FROM: Record<Exclude<InViewEffect, "none">, Record<string, number | string>> = { fade: { opacity: 0 }, "slide-up": { opacity: 0, y: 24 }, "slide-down": { opacity: 0, y: -24 }, scale: { opacity: 0, scale: 0.92 }, blur: { opacity: 0, filter: "blur(10px)" }, } type InViewProps = Omit<React.ComponentProps<"div">, "children" | "ref"> & { /** Content, or a function that receives whether the element is in view. */ children: React.ReactNode | ((inView: boolean) => React.ReactNode) /** Built-in entrance. "none" only reports the state and leaves styling to you. */ effect?: InViewEffect /** Fraction of the element that must be visible: 0 to 1, or "some" / "all". */ amount?: number | "some" | "all" /** Shrinks or grows the viewport used for the check, like CSS margins: "0px 0px -20% 0px". */ margin?: string /** Stay revealed after the first time. */ once?: boolean /** Seconds before the entrance starts. */ delay?: number /** Seconds the entrance takes. */ duration?: number /** Called whenever the element enters or leaves the view. */ onInViewChange?: (inView: boolean) => void /** Element to render. */ as?: "div" | "section" | "article" | "li" | "span" } /** Tells you when an element is on screen. Sets `data-in-view` for CSS, and can run a simple entrance. */ function InView({ children, effect = "fade", amount = 0.25, margin, once = true, delay = 0, duration = 0.55, onInViewChange, as = "div", className, ...props }: InViewProps) { const ref = React.useRef<HTMLElement>(null) const reduce = useReducedMotion() const inView = useInView(ref, { once, amount, margin: margin as `${number}px` | undefined }) const last = React.useRef<boolean | null>(null) const callback = React.useRef(onInViewChange) React.useEffect(() => { callback.current = onInViewChange }) React.useEffect(() => { if (last.current !== inView) { if (last.current !== null || inView) callback.current?.(inView) last.current = inView } }, [inView]) const Tag = motion[as] as typeof motion.div const animated = effect !== "none" && !reduce return ( <Tag ref={ref as React.Ref<HTMLDivElement>} data-slot="in-view" data-in-view={inView} initial={false} animate={animated ? (inView ? { opacity: 1, x: 0, y: 0, scale: 1, filter: "blur(0px)" } : FROM[effect as Exclude<InViewEffect, "none">]) : undefined} transition={{ duration, delay: inView ? delay : 0, ease: [0.22, 1, 0.36, 1] }} className={cn(className)} {...(props as object)} > {typeof children === "function" ? children(inView) : children} </Tag> ) } export { InView, type InViewProps, type InViewEffect }Update the import paths to match your project setup.
Usage
import { InView } from "@/components/ballmac/in-view"The full example is in the Code tab above.
Examples
Render function and callback
Off screen
Style it with data-in-view, or read the state in a function child.
"use client"
import * as React from "react"
import { InView } from "@/components/ballmac/in-view"
export default function InViewState() {
const [events, setEvents] = React.useState(0)
return (
<div className="grid w-full max-w-sm gap-3">
<InView
effect="none"
once={false}
amount={0.5}
onInViewChange={() => setEvents((n) => n + 1)}
className="rounded-xl border bg-card p-4 transition-colors duration-300 data-[in-view=true]:border-chart-2 data-[in-view=true]:bg-chart-2/10"
>
{(inView) => (
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold">{inView ? "On screen" : "Off screen"}</p>
<p className="text-xs text-muted-foreground">Style it with data-in-view, or read the state in a function child.</p>
</div>
<span className="rounded-full border px-2 py-0.5 font-mono text-xs tabular-nums">{events} changes</span>
</div>
)}
</InView>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
children*Content, or a function that receives whether the element is in view. | React.ReactNode | ((inView: boolean) => React.ReactNode) | — |
effectBuilt-in entrance. "none" only reports the state and leaves styling to you. | InViewEffect | "fade" |
amountFraction of the element that must be visible: 0 to 1, or "some" / "all". | number | "some" | "all" | 0.25 |
marginShrinks or grows the viewport used for the check, like CSS margins: "0px 0px -20% 0px". | string | — |
onceStay revealed after the first time. | boolean | true |
delaySeconds before the entrance starts. | number | 0 |
durationSeconds the entrance takes. | number | 0.55 |
onInViewChangeCalled whenever the element enters or leaves the view. | (inView: boolean) => void | — |
asElement to render. | "div" | "section" | "article" | "li" | "span" | "div" |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Screen readers | Content is always in the DOM; only presentation changes |
| Reduced motion | Content is shown with no entrance |
Use with AI
<InView effect='fade|slide-up|slide-down|scale|blur|none' amount margin once onInViewChange>{content or (inView)=>content}</InView>. data-in-view is true or false for Tailwind's data-[in-view=true] variants. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI In View (@ballmac/in-view) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Starting a video, counter or animation only when seen
- Custom reveal styling driven by data-in-view
Not for
- Staggered groups (blur-fade or stagger-list)
Registry JSON: https://ui.ballmac.com/r/in-view.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
- Registry
- shadcn/utils
Pairs well with
Blur Fade
A wrapper that fades content in while it unblurs and slides a few pixels, once it scrolls into view, with a group that staggers its children.
Number Ticker
Counts up to a number when it scrolls into view, with locale-aware formatting for currency, percentages and decimals.
Stagger List
A list or grid whose items arrive one after another from any direction, then glide to their new places when filtered, sorted, added or removed.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.