Props and styling

There are two ways to customize an exported component:

  1. Use a Framer property or variable. This is the recommended approach because the value is part of the component's public contract and survives future exports more reliably.
  2. Add a CSS override in your app. This is useful when you need a quick visual adjustment, but it depends on the component's rendered markup and generated CSS.

Start with Framer properties and variables whenever you can. Use CSS inspection for the cases that are not exposed as public controls.

Pass component properties

The generated .tsx facade is the source of truth for the props an export exposes. Open that file to see the exact prop names, accepted values, and variant names for your component.

For example, an exported button might expose text, a link, a fill color, a border, a radius, and shadows:

import Button from "./framer/components/button-primary-button";

export function ContactButton() {
  return (
    <Button
      text="Get in touch"
      link="/contact"
      newTab={false}
      fill="#ff6747"
      radius="12px"
      buttonBorder={{
        borderColor: "#ff8f7b",
        borderStyle: "solid",
        borderWidth: 1,
      }}
      className="contact-button"
    />
  );
}

The names are based on the public properties in Framer, not on the private names inside the generated runtime. If a prop is not present in the generated facade, it was not exposed by that component's public controls. TypeScript will then help you catch a misspelled or unsupported prop.

Prefer Framer variables for reusable values

If a value should be configurable—such as a button fill, text color, border, radius, or shadow—create a variable in Framer and connect the component property to it. This makes the value part of the component's design contract instead of a selector that your app has to guess.

In Framer, open the component's variables menu and create a variable with a meaningful name:

Framer's variable menu for adding a component variable

Then select the variable and give it a type and default value. The component variables panel shows the properties that can be configured and the value assigned to each one:

A component variables panel with text, border, fill, radius, and shadow properties

For the variable to be passable from React, it needs to be connected to a public component property. A color variable that only exists inside a private layer is not automatically a React prop. After connecting it to a public property, export the component again and check the generated .tsx file.

For example, if the generated facade contains fill and textColor, you can pass those values from your app:

import Button from "./framer/components/button-primary-button";

const brandFill = "#ff6747";
const brandText = "#ffffff";

export function BrandedButton() {
  return (
    <Button
      fill={brandFill}
      textColor={brandText}
      text="Start building"
    />
  );
}

This approach is especially useful when several instances should share one design value. You can change the value in one place, pass a theme token from your application, or expose light and dark values without depending on a generated class name.

Common generated value shapes

The exact names depend on the component, but the generated facade commonly uses shapes like these:

type FramerBorderValue = {
  borderColor?: string;
  borderStyle?: string;
  borderWidth?: number;
  borderTopWidth?: number;
  borderRightWidth?: number;
  borderBottomWidth?: number;
  borderLeftWidth?: number;
};

type FramerBoxShadowValue = {
  type?: string;
  x?: number;
  y?: number;
  blur?: number;
  spread?: number;
  color?: string;
  inset?: boolean;
  diffusion?: number;
  focus?: number;
};

For example:

<Button
  buttonBorder={{
    borderColor: "rgba(255, 255, 255, 0.2)",
    borderStyle: "solid",
    borderWidth: 1,
  }}
  buttonShadows={[
    {
      type: "box",
      x: 0,
      y: 8,
      blur: 24,
      spread: 0,
      color: "rgba(0, 0, 0, 0.2)",
      inset: false,
    },
  ]}
  radius="999px"
/>

Use the generated facade rather than copying these example names blindly. A component may call the same kinds of controls fill, background, buttonColor, or something else.

Use className and style

Use className for reusable application rules. Use style for a value that belongs to one instance:

import Button from "./framer/components/button-primary-button";

export function MarketingButton() {
  return (
    <Button
      className="marketing-button"
      style={{ marginTop: 16, maxWidth: 280, width: "100%" }}
      text="Get started"
    />
  );
}
.marketing-button {
  border-radius: 999px;
}

style and className are applied to the exported component's outer element. They do not guarantee that every nested text layer or visual effect will inherit the value. Framer's generated styles may set a more specific value on an inner element.

When a CSS override is appropriate

CSS is a practical fallback when a visual detail was not exposed as a public Framer property, or when you need to adjust one instance without changing the original Framer component.

Always scope the rule to a class on the component:

import TextCycle from "./framer/components/text-cycle";

export function ColoredHeadline() {
  return (
    <TextCycle
      className="red-text-cycle"
      text1="Design"
      text2="Build"
      text3="Create"
    />
  );
}

Then inspect the rendered component in your browser's developer tools. Find the actual nested element that draws the text. It may be a p, h1, h2, h3, h4, span, or another element depending on how the component was made in Framer.

If the selected element uses Framer's text color variable, the override can look like this:

.red-text-cycle p {
  --framer-text-color: red !important;
}

If inspection shows that the text is an h4, target that element instead:

.red-text-cycle h4 {
  --framer-text-color: red !important;
}

Where does --framer-text-color come from?

It is not a Reframe prop and it is not a universal CSS variable that you should invent for every component. Framer's generated rich-text and text styles often use --framer-text-color to feed the visible color into the nested text element. You know to use it because it appears in the element's generated rule or computed styles when you inspect that element.

The same inspection method works for other visuals:

.custom-card .target-element {
  background: #171717 !important;
  border-color: #ff6747 !important;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
}

For a generated custom property, copy the exact property name shown by developer tools and override it only within your scoped class:

.custom-card .target-element {
  --the-generated-property-you-inspected: #ff6747 !important;
}

Generated selectors, class names, and custom properties can change when the Framer component is edited or re-exported. This makes inspection-based CSS subjective and potentially fragile. It is useful, but it is not the most stable customization API.

A simple inspection checklist

  1. Add a unique className to the exported component.
  2. Open the page in the browser and inspect the visible element you want to change.
  3. Identify the actual nested tag or selector that owns the color, fill, border, or shadow.
  4. Check the Styles and Computed panels for the winning declaration or custom property.
  5. Recreate that selector under your component class and add !important only when the generated rule wins without it.
  6. Re-test after updating the component with the CLI, because generated markup may have changed.

Responsive variants

Use .Responsive when the export contains Framer breakpoint variants:

import TextCycle from "./framer/components/text-cycle";

export function ResponsiveHeadline() {
  return (
    <TextCycle.Responsive
      variants={{
        base: "Text 1",
        md: "Text 2",
        lg: "Text 3",
      }}
      className="headline"
    />
  );
}

.Responsive chooses the configured variant for the current viewport. The breakpoint names and variant values come from the export. A component with hand-made variants that are not tied to Framer's native breakpoints will not become responsive simply because .Responsive is used.

Keep custom CSS outside framer

Put application styles in your normal stylesheet, such as src/app.css or src/styles.css:

import Button from "./framer/components/button-primary-button";
import "./app.css";

export default function Page() {
  return <Button className="page-button" text="Continue" />;
}
.page-button {
  margin-inline: auto;
}

Do not edit generated runtime files or generated component styles for product-specific rules. A later pnpx @reframejs/cli add <component-id> can update files owned by the export. Your own stylesheet remains stable and keeps the boundary clear.

Layout around a component

Use a wrapper you own for page-level layout. This avoids coupling your route to generated layout classes:

export function FeatureSection() {
  return (
    <section className="feature-section">
      <div className="feature-copy">
        <h2>Build from a real Framer component</h2>
        <p>Keep the exported behavior while composing it with your application.</p>
      </div>
      <Button text="Explore" variant="Primary Button" />
    </section>
  );
}
.feature-section {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2rem;
  align-items: center;
}

@media (max-width: 640px) {
  .feature-section {
    grid-template-columns: 1fr;
  }
}

The generated component remains responsible for its Framer behavior. Your wrapper remains responsible for where the component sits in your application.