Skip to content

Quickstart Guide: Display Frames

This guide shows you how to add frames rendering to your next.js + tailwind app using frames.js.

Steps

Create a new repo

Create a new Next.js app

npx create-next-app@latest my-project --ts --eslint --tailwind --app
cd my-project

Add @frames.js/render to your project

npm
npm install @frames.js/render

Add proxies for routing frame requests via your backend for privacy + preventing CORS issues

./app/frames/route.tsx
export { GET, POST } from "@frames.js/render/next";

Set up the useFrame() hook

See the useFrame() hook documentation for more information.

"use client";

import {
  type FarcasterSigner,
  signFrameAction,
} from "@frames.js/render/farcaster";
import { useFrame } from "@frames.js/render/use-frame";
import { fallbackFrameContext } from "@frames.js/render";
 
export default function App() {
  // @TODO: replace with your farcaster signer

  const farcasterSigner: FarcasterSigner = {
    fid: 1,
    status: "approved",
    publicKey:
      "0x00000000000000000000000000000000000000000000000000000000000000000",
    privateKey:
      "0x00000000000000000000000000000000000000000000000000000000000000000",
  };
 
  const frameState = useFrame({
    // replace with frame URL
    homeframeUrl: "https://framesjs.org",
    // corresponds to the name of the route for POST and GET in step 2
    frameActionProxy: "/frames",
    frameGetProxy: "/frames",
    connectedAddress: undefined,
    frameContext: fallbackFrameContext,
    // map to your identity if you have one
    signerState: {
      hasSigner: farcasterSigner.status === "approved",
      signer: farcasterSigner,
      isLoadingSigner: false,
      async onSignerlessFramePress() {
        // Only run if `hasSigner` is set to `false`
        // This is a good place to throw an error or prompt the user to login
        console.log(
          "A frame button was pressed without a signer. Perhaps you want to prompt a login"
        );
      },
      signFrameAction,
      async logout() {
        // here you can add your logout logic
        console.log("logout");
      },
    },
  });
 
  // @TODO here will go the renderer
  return null;
}

Add the renderer to your page

"use client";
import {
  type FarcasterSigner,
  signFrameAction,
} from "@frames.js/render/farcaster";
import { useFrame } from "@frames.js/render/use-frame";
import { fallbackFrameContext } from "@frames.js/render";

import {
  FrameUI,
  type FrameUIComponents,
  type FrameUITheme,
} from "@frames.js/render/ui";
 
/**
 * StylingProps is a type that defines the props that can be passed to the components to style them.
 */
type StylingProps = {
  className?: string;
  style?: React.CSSProperties;
};
 
/**
 * You can override components to change their internal logic or structure if you want.
 * By default it is not necessary to do that since the default structure is already there
 * so you can just pass an empty object and use theme to style the components.
 *
 * You can also style components here and completely ignore theme if you wish.
 */
const components: FrameUIComponents<StylingProps> = {};
 
/**
 * By default there are no styles so it is up to you to style the components as you wish.
 */
const theme: FrameUITheme<StylingProps> = {
  ButtonsContainer: {
    className: "flex gap-[8px] px-2 pb-2 bg-white",
  },
  Button: {
    className:
      "border text-sm text-gray-700 rounded flex-1 bg-white border-gray-300 p-2",
  },
  Root: {
    className:
      "flex flex-col w-full gap-2 border rounded-lg overflow-hidden bg-white relative",
  },
  Error: {
    className:
      "flex text-red-500 text-sm p-2 bg-white border border-red-500 rounded-md shadow-md aspect-square justify-center items-center",
  },
  LoadingScreen: {
    className: "absolute top-0 left-0 right-0 bottom-0 bg-gray-300 z-10",
  },
  Image: {
    className: "w-full object-cover max-h-full",
  },
  ImageContainer: {
    className:
      "relative w-full h-full border-b border-gray-300 overflow-hidden",
    style: {
      aspectRatio: "var(--frame-image-aspect-ratio)", // fixed loading skeleton size
    },
  },
  TextInput: {
    className: "p-[6px] border rounded border-gray-300 box-border w-full",
  },
  TextInputContainer: {
    className: "w-full px-2",
  },
};
 
export default function App() {
  // @TODO: replace with your farcaster signer
  const farcasterSigner: FarcasterSigner = {
    fid: 1,
    status: "approved",
    publicKey:
      "0x00000000000000000000000000000000000000000000000000000000000000000",
    privateKey:
      "0x00000000000000000000000000000000000000000000000000000000000000000",
  };
 
  const frameState = useFrame({
    // replace with frame URL
    homeframeUrl: "https://framesjs.org",
    // corresponds to the name of the route for POST and GET in step 2
    frameActionProxy: "/frames",
    frameGetProxy: "/frames",
    connectedAddress: undefined,
    frameContext: fallbackFrameContext,
    // map to your identity if you have one
    signerState: {
      hasSigner: farcasterSigner.status === "approved",
      signer: farcasterSigner,
      isLoadingSigner: false,
      async onSignerlessFramePress() {
        // Only run if `hasSigner` is set to `false`
        // This is a good place to throw an error or prompt the user to login
        console.log(
          "A frame button was pressed without a signer. Perhaps you want to prompt a login"
        );
      },
      signFrameAction,
      async logout() {
        // here you can add your logout logic
        console.log("logout");
      },
    },
  });
 

  return (
    <FrameUI frameState={frameState} components={components} theme={theme} />
  );
}

Allow images from any domain

next.config.js
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**",
      },
    ],
  },
};

Run

npm
npm run dev

Done! 🎉

Using Next.js Image Optimization for image proxying

You can use next/image to proxy images via your backend to preserve user privacy and prevent CORS issues.

import { type FrameUIComponents } from "@frames.js/render/ui";
import Image from "next/image";
 
/**
 * StylingProps is a type that defines the props that can be passed to the components to style them.
 */
type StylingProps = {
  className?: string;
  style?: React.CSSProperties;
};
 
/**
 * You can override components to change their internal logic or structure if you want.
 * By default it is not necessary to do that since the default structure is already there
 * so you can just pass an empty object and use theme to style the components.
 *
 * You can also style components here and completely ignore theme if you wish.
 */
const components: FrameUIComponents<StylingProps> = {
  Image: (props, stylingProps) => {
    if (props.status === "frame-loading") {
      return <></>;
    }
 
    // Here you can add your own logic to sanitize and validate the image URL
    let sanitizedSrc = props.src;
 
    // Don't allow data URLs that are not images -- we don't want to allow arbitrary data to be loaded
    if (props.src.startsWith("data:") && !props.src.startsWith("data:image")) {
      sanitizedSrc = "";
    }
 
    // Don't allow SVG data URLs -- could contain malicious code
    if (props.src.startsWith("data:image/svg")) {
      sanitizedSrc = "";
    }
 
    // Can set the dimensions based on the aspect ratio of the image
    // const aspectRatio = props.aspectRatio; // "1:1" or "1.91:1"
 
    return (
      <Image
        {...stylingProps}
        src={sanitizedSrc}
        onLoad={props.onImageLoadEnd}
        onError={props.onImageLoadEnd}
        alt="Frame image"
        sizes="100vw"
        height={0}
        width={0}
      />
    );
  },
};