Animation » Example Animations » 17. Page: Indicators

17. Page: Indicators

(You can also tap the indicators to directly go to a page.)

Code component

We use React’s useState() hook to keep track of the current page.

const pages = [1, 2, 3, 4, 5]
const indicatorSize = 10
const indicatorPadding = 10
const indicatorWidth = pages.length * indicatorSize
const indicatorPaddingTotal = (pages.length - 1) * indicatorPadding
const indicatorWidthTotal = indicatorWidth + indicatorPaddingTotal
const indicatorAlpha = 0.3

export default function CC_17_Page_Indicators(props) {
    const [current, setCurrent] = useState(0)

    return (
        <div
            style={{
                width: 400,
                height: 400,
                ...props.style,
                display: "flex",
                placeItems: "center",
                placeContent: "center",
            }}
        >
            <Page
                width={150}
                height={150}
                radius={30}
                currentPage={current}
                onChangePage={(current, previous) => setCurrent(current)}
            >
                {pages.map((index) => {
                    return (
                        <div
                            style={{
                                width: 150,
                                height: 150,
                                borderRadius: 30,
                                backgroundColor: "#fff",
                            }}
                        />
                    )
                })}
            </Page>

            {pages.map((index) => {
                return (
                    <motion.div
                        style={{
                            width: indicatorSize,
                            height: indicatorSize,
                            borderRadius: "50%",
                            backgroundColor: "#fff",
                            position: "absolute",
                            top: "calc(50% + 100px)",
                            left: `calc(50% + ${index - 1} * ${
                                indicatorSize + indicatorPadding
                            }px)`,
                            x: -indicatorWidthTotal / 2,
                        }}
                        animate={{
                            opacity: current === index - 1 ? 1 : indicatorAlpha,
                        }}
                        onTap={() => setCurrent(index - 1)}
                    />
                )
            })}
        </div>
    )
}

Pro tip: When using the <Page> component in a React project (like in the CodeSandbox above), set the box-sizing of all elements to border-box. Like this, for instance:

* {
    box-sizing: border-box;
}

<Page>
box-sizing, CSS universal selector (*)

Code overrides

Here I used Framer’s createStore() to communicate between the overrides.

The Indicators() override is attached to the parent frame of the indicators. It maps through its children (the five indicators) and clones them, giving them each an animate to the correct opacity and an onTap() event so that you can tap them to change the page.

const useStore = createStore({ currentIndicator: 0 })

export function Indicators(Component): ComponentType<any> {
    return (props) => {
        const { children, ...rest } = props

        const [store, setStore] = useStore()

        return (
            <Component
                {...rest}
                children={children.map((indicator, index) => {
                    let opacity = 0.3
                    if (index === store.currentIndicator) {
                        opacity = 1
                    }

                    return cloneElement(indicator, {
                        animate: { opacity: opacity },
                        onTap: () => setStore({ currentIndicator: index }),
                    })
                })}
            />
        )
    }
}

export function Page(Component): ComponentType {
    return (props) => {
        const [store, setStore] = useStore()

        return (
            <Component
                {...props}
                currentPage={store.currentIndicator}
                onChangePage={(current, previous) =>
                    setStore({ currentIndicator: current })
                }
            />
        )
    }
}


Join the Framer book mailing list    ( ± 6 emails/year )

GDPR

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing per their Privacy Policy and Terms.



15 comments on “17. Page: Indicators”

  • caihehuang says:

    Please take a look, https://framer.com/share/g9SyCPIVasosUGPblSXC/vS9mxUz1B
    Try to show the button when page component is tapped and remain hidden when page is swiped.

    How do you tell between swipe and tap gesture?

  • regimantas.vegele says:

    Hi, I seem to be having a few issues trying to follow the Override example. I’ll attach the image. With I could attach a few of them 🙂 Thanks for your help!

    – The map error : “Property ‘map’ does not exist on type ‘ReactNode’. Property ‘map’ does not exist on type ‘string’.(2339)”

    – The cloneElement: “‘cloneElement’ cannot be used as a value because it was imported using ‘import type’.(1361)”

    • Tes Mat says:

      The map error: That’s just TypeScript (probably) not realizing that children is an array and that we want to use the map() function on it. The override will work anyway, but if you want, you can silence this error by putting a // @ts-ignore above it (see the example project).

      The cloneElement error: Like it says, you imported cloneElement as a TypeScript type object, probably together with ComponentType like this:

      import type { ComponentType, cloneElement } from "react"

      You should import it like a normal object (separately), like this:

      import type { ComponentType } from "react"
      import { cloneElement } from "react"
      import { useMotionValue, useTransform } from "framer-motion"
      import { createStore } from "https://framer.com/m/framer/store.js@^1.0.0"
  • kenneth.spry1 says:

    Hey there. Is there a way to turn off page animations?

    • Tes Mat says:

      Not when swiping, but you can set animateCurrentPageUpdate to false and page with buttons.

      Here’s an example:

      import { Override, Data, useObserveData } from "framer"
      
      const appState = Data({ index: 0 })
      
      export function Page(props): Override {
          useObserveData()
          return {
              animateCurrentPageUpdate: false,
              currentPage: appState.index,
              // dragEnabled: false,
          }
      }
      
      export function Forward(props): Override {
          useObserveData()
          return {
              onTap() {
                  appState.index = appState.index + 1
              },
          }
      }
      
      export function Back(props): Override {
          useObserveData()
          return {
              onTap() {
                  appState.index = appState.index - 1
              },
          }
      }

      Example project: https://framer.com/projects/new?duplicate=vMpLuGiPe16YO7tYtFWh

      Changing the currentPage like that only seems to work well with legacy overrides (didn’t work with createStore()).

      You can also make it impossible to swipe by setting dragEnabled to false.

  • arron says:

    His Tes,
    How would one go about adding a Tap/Click event to the Small Page Indicators to change the corresponding page? // Appreciate the knowledge …

    • Tes Mat says:

      That isn’t too hard. The indicators need an event that changes the current indicator (the current state, or currentIndicator in the data store) and we also need to pass that value to the Page component’s currentPage property, so that it reacts to changes.

      It actually makes sense to add this, so I updated the example.

  • folksblue says:

    I mean, the override type indicator works on the framer canvas. But it doesn’t work on framer web.

Leave a Reply