Skip to content

Advanced

Motion

Every animation in appshell-react runs on plain CSS transitions by default — no animation library required. One provider at the root upgrades reveals, drawers, and footers to real springs, through a public adapter contract you can also implement yourself.

CSS by default

Without any setup, the shell animates with CSS transitions alone. Internally every animated element is rendered through a motion adapter, and the built-in CSS adapter is deliberately boring: its AnimatePresence passes children straight through, and its motion elements strip the animation props (initial, animate, exit, transition, layoutId, …) and render the plain HTML tag. No animation runtime ships in your bundle, and header reveals and footer hide/show still transition smoothly via CSS.

Upgrading to springs

Wrap your app once — at the root, above the AppShell — in MotionProvider with the Framer Motion adapter, imported from the appshell-react/motion-framer subpath:

app.tsx
import { AppShell, MotionProvider } from "appshell-react";
import { framerMotionAdapter } from "appshell-react/motion-framer";
import type { ReactNode } from "react";

export default function App({ children }: { children: ReactNode }) {
  return (
    <MotionProvider adapter={framerMotionAdapter}>
      <AppShell safeArea>{children}</AppShell>
    </MotionProvider>
  );
}

Header reveals, sidebar drawers, and footer hide/show now run as spring-based enter/exit animations, and details like the tab bar’s active indicator glide between tabs with a shared-layout spring. Every shell component below the provider picks it up through context — which is why one wrapper at the root is the whole setup.

framer-motion is an optional peer dependency: the core never imports it, only the motion-framer subpath does. Install it yourself (pnpm add framer-motion) when you use the adapter — skip both and it never touches your bundle.

MotionProvider

PropTypeDefaultDescription
adapter*MotionAdapter—The animation engine to use — framerMotionAdapter, or your own object satisfying the contract below.
children*ReactNode—Your app — typically the AppShell and everything inside it.

The MotionAdapter contract

The adapter type is public, so Framer Motion is a choice, not a requirement. An adapter is an AnimatePresence component plus a map of motion-wrapped HTML elements:

MotionAdapter (shape)
import type { ComponentType, ForwardRefExoticComponent } from "react";

/** Public — import type { MotionAdapter } from "appshell-react". */
interface MotionAdapter {
  AnimatePresence: ComponentType<any>;
  motion: {
    div: ForwardRefExoticComponent<any>;
    footer: ForwardRefExoticComponent<any>;
    nav: ForwardRefExoticComponent<any>;
    header: ForwardRefExoticComponent<any>;
    section: ForwardRefExoticComponent<any>;
    main: ForwardRefExoticComponent<any>;
    span: ForwardRefExoticComponent<any>;
    button: ForwardRefExoticComponent<any>;
    h1: ForwardRefExoticComponent<any>;
    p: ForwardRefExoticComponent<any>;
  };
}

Shell components hand these elements framer-motion-shaped props — initial/animate/exit variants, a transition, sometimes a layoutId — so any engine that can interpret (or deliberately ignore) that vocabulary can plug in. This skeleton is a working starting point; it behaves like a reduced-motion adapter until you wire the props into your engine:

my-adapter.tsx
import React, { forwardRef, type ReactNode } from "react";
import type { MotionAdapter } from "appshell-react";

/** Render children straight through — exiting elements simply unmount. */
function InstantPresence({ children }: { children?: ReactNode }) {
  return <>{children}</>;
}

/**
 * One motion element. This skeleton drops the animation props and renders
 * the plain tag — swap that for calls into your animation engine, which
 * receives framer-motion-shaped props (initial / animate / exit /
 * transition / layoutId) from the shell components.
 */
function createMotionElement(tag: string) {
  return forwardRef<HTMLElement, Record<string, unknown>>(
    function MotionElement(props, ref) {
      const {
        initial: _initial,
        animate: _animate,
        exit: _exit,
        transition: _transition,
        layoutId: _layoutId,
        whileHover: _whileHover,
        whileTap: _whileTap,
        ...rest
      } = props;
      return React.createElement(tag, { ...rest, ref });
    }
  );
}

export const myAdapter: MotionAdapter = {
  AnimatePresence: InstantPresence,
  motion: {
    div: createMotionElement("div"),
    footer: createMotionElement("footer"),
    nav: createMotionElement("nav"),
    header: createMotionElement("header"),
    section: createMotionElement("section"),
    main: createMotionElement("main"),
    span: createMotionElement("span"),
    button: createMotionElement("button"),
    h1: createMotionElement("h1"),
    p: createMotionElement("p"),
  },
};