Building A Customizable Framer Component

Oct 14, 2024
6 minute read

Note: When I left Twitter to become an indie developer I committed to helping others with my work as much as I could. My goal has always been to make the lives of developers a little easier, so they can turn their ideas into, apps, products, and businesses. Every week this month I will be open sourcing something I've built for Plinky, my very own personal Open Source October. My career rests on the foundation that others have created, and I want to continue paying it forward by lowering the barrier for others to make what matters to them. Now, onto the post.

If you came here for the Buttondown Framer Component I've open sourced, you can skip the post below and grab the code on Buttondown’s official repo. But if you happen to be interested in the technical details of how to build a customizable Framer Component, keep reading for a walkthrough.


When I launched Plinky’s Links You’ll Love newsletter, I knew I needed a beautiful landing page to set the right expectation for quality. The problem? I'm sick and tired of writing HTML, CSS, and JavaScript, with zero interest in building a landing page from scratch.

A frowny penguin

I’d heard a lot about Framer from my designer friends, and how easy Framer made it to build beautiful websites. The canvas felt familiar, like a more interactive version of Figma. Within half an hour of playing with Framer, I had a slick landing page with fancy animations that took only minutes to create. Despite a few missing features and minor quirks, I fell in love immediately.

Code Components

What really sold me on Framer wasn’t just the features it has, but how it handles what it doesn’t have. Framer provides common elements for website layouts, like Stacks, Images, and Text, but sometimes you need something custom. That’s where Code Components come in.

At its simplest, a Code Component is a React component, which means you can create just about anything with a bit of JavaScript.1 There are also many pre-made components available, and best of all, they're fully customizable.

The magic of Framer’s Code Components lies in Property Controls, which are essentially React props. By adding Property Controls, Code Components become as customizable as any other Framer element. I’ll explain how this works in just a moment.

Custom Framer Component

Framer x Buttondown

I use Buttondown, a popular indie email service, to deliver my newsletter. Buttondown is run by Justin Duke, a prolific writer who's very helpful and shares a lot of valuable lessons — really an all-around great guy. While the default look of Buttondown’s email form is basic, I wanted something more polished for my website. This meant writing a lot of CSS, or… I could create a Code Component. 🤔

I dove into the Framer docs, and once I had an understanding for how they worked, I fed that documentation to Claude to outsource the work of writing a template for my Code Component. The real fun began when I realized I could open source this component for other Framer users, and leaned into my love for building reusable abstractions.

Building A Code Component

The structure of a Framer Code Component is similar to any other React component. You import React, write functions, and use features like State and Effects. Most of the time you’re just writing standard React so I'll skip over some details, but you can view the complete code for this component here.

import React, { useState, useEffect } from "react"
import { addPropertyControls, ControlType } from "framer"

export default function ButtondownForm(props) {
  const {
    newsletterConfiguration,
    text,
    style,
    textFieldFont,
    buttonFont,
    backgroundColor,
    submitButtonColors,
    margin,
    cornerRadius,
    borderColor,
    borderWidth,
    ...rest
  } = props
  
  const [email, setEmail] = useState("")
  const [formState, setFormState] = useState("idle") // idle, invalid, submitting, success

  // A *lot* more code here...

  useEffect(() => {
      if (formState === "success") {
          const timer = setTimeout(() => {
              setFormState("idle")
              setEmail("")
          }, 2000)
          return () => clearTimeout(timer)
      }
  }, [formState])

  const handleSubmit = (e) => {
    // It's me, I'm handling submit!
  }

  const getButtonContent = () => {
    // It's me, I'm getting the button content!
  }

  const getButtonStyle = () => {
    // It's me, I'm getting the button style!
  }

  return (
    <>
      <style>{`
        // Your neat CSS bits
      `}</style>
      <form
          className="buttondown-form"
          action={newsletterConfiguration.embedURL}
          method="post"
          target={
              newsletterConfiguration.onSubmit &&
              newsletterConfiguration.onSubmit.trim() !== ""
                  ? "popupwindow"
                  : undefined
          }
          onSubmit={handleSubmit}
          style={{
              ...newsletterConfiguration,
              ...formStyle,
              ...style,
              backgroundColor,
              borderRadius: cornerRadius.form,
              borderColor,
              borderWidth,
              borderStyle: borderWidth > 0 ? "solid" : "none",
          }}
      >
        <input
            type="email"
            name="email"
            id="bd-email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder={text.placeholder}
            style={{
                ...inputStyle,
                ...textFieldFont,
                borderRadius: cornerRadius.input,
            }}
            required
        />
        <button
            type="submit"
            style={getButtonStyle()}
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
            disabled={formState === "submitting"}
        >
            {getButtonContent()}
        </button>
        <label htmlFor="bd-email" style={srOnlyStyle}>
            Enter your email
        </label>
      </form>
    </>
  )
}

This component has various customizable properties, like newsletterConfiguration, formStyle, text, making it adaptable to different design needs. But the real power of a Framer Code Component comes from the addPropertyControls function.

By adding Property Controls, we can make this form as customizable as any other standard Framer element. This is what allows us to transform the Buttondown form I built for my website, and turn it into a form that anyone can personalize for their website.

addPropertyControls(ButtondownForm, {
    newsletterConfiguration: {
        type: ControlType.Object,
        title: "Newsletter",
        controls: {
            embedURL: {
                type: ControlType.String,
                title: "Embed URL",
            },
            onSubmit: {
                type: ControlType.String,
                title: "onSubmit",
                displayTextArea: true,
            },
        },
    },
    textFieldFont: {
        type: ControlType.Object,
        title: "Input Font",
        controls: {
            fontFamily: { type: ControlType.String, title: "Family" },
            fontSize: { type: ControlType.String, title: "Size" },
            fontWeight: {
                type: ControlType.Enum,
                title: "Weight",
                options: [
                    "Normal",
                    "Bold",
                    "100",
                    "200",
                    "300",
                    "400",
                    "500",
                    "600",
                    "700",
                    "800",
                    "900",
                ],
            },
        },
    },
    backgroundColor: { type: ControlType.Color, title: "Background Color" },
    text: {
        type: ControlType.Object,
        title: "Text",
        controls: {
            placeholder: {
                type: ControlType.String,
                title: "Placeholder",
            },
            submitButton: {
                type: ControlType.String,
                title: "Button Text",
            },
        },
    },
    borderWidth: {
        type: ControlType.Number,
        title: "Border Width",
        min: 0,
        max: 10,
        step: 1,
    },
})

The addPropertyControls function allows us to define these customization options using Framer primitives like ControlType.String, ControlType.Number, ControlType.Color, and ControlType.Object. These controls turn into beautiful configuration panels in Framer, where users can tweak settings to match their design preferences.

Custom Framer Component

Now, we have a fully customizable Framer Component, ready for any Buttondown newsletter, whether your favorite color is red, blue, green, or even my favorite color in Plinky, pink.


Speaking of newsletters, if you enjoy thought-provoking and enjoyable articles delivered straight to your inbox, I’d love for you to see the form I made for my Buttondown newsletter, and perhaps even sign up..

And if you have any thoughts or suggestions, I'm always available on Threads. I welcome any and all polite discussions.


  1. Although I started off this post by saying I didn’t want to write JavaScript (or TypeScript), I find writing this kind of code is a lot more enjoyable than laying out a responsive website that works across half a dozen browsers.

Joe Fabisevich is an indie developer creating software at Red Panda Club Inc. while writing about design, development, and building a company here at build.ms. Before all that he was working as an iOS developer on societal health issues @Twitter.

Like my writing? You can keep up with it in your favorite RSS reader, or get posts emailed in newsletter form. I promise to never spam you or send you anything other than my posts, it's just a way for you to read my writing wherever's most comfortable for you.

If you'd like to know more, wanna talk, or need some advice, feel free to sign up for office hours at no charge, I'm very friendly. 🙂