On this page9 sections
In a Framer site converted to React, each component's variants become values of a variant prop with the names the designer gave them, such as "Primary" or "Dark / Small". Hover and press states become gesture states named after their variant ("Primary-hover"), switched on per variant in an enabledGestures table. The visual differences between states live in the component's CSS module and in per-variant overrides in the component, and motion animates between them with the transition the designer set. To change a state, edit its class or its override; to add a variant, follow the same pattern.
This post reads a converted component piece by piece. Deframer is an independent tool and is not affiliated with, endorsed or sponsored by Framer, and it converts only sites you own or have permission to convert. The code below is simplified to show the shape; your components will be longer and named after your design.
What is a variant in Framer, and what does it become?
In Framer, a component can have several variants: a button might have Primary and Secondary, each with a hover state. A page can also behave like a component with breakpoint variants: Desktop, Tablet, Phone. Designers switch variants with a property on the instance, interactions or breakpoints.
In the converted project:
In Framer | In the React code |
|---|---|
Component | A React component in |
Variant | A value of the |
Default variant | The fallback when no |
Hover, press | Gesture states: |
Interaction "change to variant" | A |
Transition | A motion transition object, springs with stiffness, damping and mass |
Per-variant styles | A class per variant in the CSS module, and per-variant overrides |
Framer itself refers to variants by internal IDs in published code. The conversion replaces those IDs with the names from the design, so variant === "Desktop" reads as it should. Framer's internal attribute names are also neutralised, for example data-framer-name becomes data-name.
Reading a converted component
A component with variants is built around a small state hook from the kit in src/lib/deframer. Simplified, the top of a button component looks like this:
const cycleOrder = ["Primary", "Secondary"];
const enabledGestures = {
Primary: { hover: true, pressed: true },
Secondary: { hover: true },
};
const transition1 = { damping: 60, delay: 0, mass: 1, stiffness: 400, type: "spring" } as const;cycleOrderlists the component's variants by name. It's also the set of valid values for thevariantprop: any other value falls back to the default variant.enabledGesturessays which variants react to hover or press. Here, Secondary has no pressed state.transition1is a transition from the design, written out with its spring values.
Inside the component, the kit's useVariantState hook works out what to show:
baseVariantis the variant in effect: thevariantprop if it's valid, otherwise the default, or whateversetVariantlast set.gestureVariantis"<baseVariant>-hover","-pressed"and so on while a gesture is active, with error and loading states taking priority over press, and press over hover.setVariantchanges the base variant, which is how a "change to variant" interaction from Framer works in code.
The root element is a motion element, such as <motion.button> or <motion.div>, which receives the variant as its animation label, so motion animates between states with the component's transition.
Where the visual differences live
There are two places, and it's worth knowing both before editing.
1. The CSS module. Each component has a .module.css beside it with classes named after its layers and variants. The kit puts the current variant's class and the active gesture's name on the root, so many differences between states are plain CSS:
/* button.module.css, simplified */
.root { background: var(--token-brand); }
.secondary { background: transparent; }
.root.hover { background: var(--token-brand-dark); }You can inspect these in the browser's developer tools like any CSS. Colours refer to the design tokens in src/styles/tokens.ts.
2. Per-variant overrides in the component. Some differences aren't CSS: different text, a different icon, a layer that only exists in one variant, or a different motion value. These are written as overrides keyed by variant name, applied on top of the default:
{...addPropertyOverrides(
{
Secondary: { "data-border": true },
"Primary-hover": { animate: { scale: 1.02 } },
},
baseVariant,
gestureVariant,
)}When both the base variant and a gesture state have an override, both apply, gesture last.
How do I change a hover state?
- Open the component's file and find its gesture in
enabledGesturesto confirm the variant has a hover state. - Check the CSS module for a class combining the variant and
hover. If the change is a colour, border, shadow or opacity, edit it there. - If the hover changes text, an icon or a motion value, find the
"<Variant>-hover"key in the component's overrides and edit it there. - Check the transition. If the hover should feel faster or softer, change the transition's spring values, or its duration for a tween. How to edit a spring explains the values.
To remove a hover state entirely, set that variant's hover to false in enabledGestures, or remove the key.
How do I add a variant?
Say the button needs an "Outline" variant:
- Add
"Outline"tocycleOrder. - Add a class for it in the CSS module, using tokens for colours.
- Add any non-CSS differences as an
Outlinekey in the overrides. - If it needs hover, add
Outline: { hover: true }toenabledGesturesand a hover class or override. - Add
"Outline"to thevariantprop's type if the component's props list variants as a union. - Use it:
<Button variant="Outline" />.
Then check every place the component is used still looks right; variants share the default's structure.
Breakpoint variants on pages
Pages and some components switch variants by viewport width: Desktop, Tablet, Phone. In the converted project these are separate layouts selected by CSS media queries, with hidden classes for layers that exist only at some widths, so the right layout shows before any JavaScript runs. That's why an edit to one breakpoint's layout often needs repeating in the others. How Framer breakpoints become responsive layouts covers this in detail.
Code components with their own variants
Components written in code in Framer, rather than drawn, keep their own logic. They land in src/code as the site's source, with whatever props and state the author wrote. The conventions above apply to drawn components; code components follow their author's. See Framer code components and overrides.
Can I simplify this into plain React state?
You can. For a small component, rewriting useVariantState into a useState and a few CSS classes is reasonable, and the result may be shorter. Do it component by component with visual regression tests in place, and keep the kit for components you don't rewrite. The kit is part of your project, so you may also read and change useVariantState itself; just remember every component with variants uses it.
FAQ
Do variant names survive the conversion?
Yes. Variants keep the names from the design, including spaces and slashes such as "Dark / Small". Internal IDs another file still passes are accepted as aliases.
Where is the hover animation's timing?
In the component's transition objects, written out with the design's values. Springs keep stiffness, damping and mass; tweens keep duration and easing.
Why are there two places for styles?
Because not everything that changes between variants is CSS. Colours and spacing are classes in the CSS module; text, icons and motion values are overrides in the component.
Do hover states work on touch devices?
They behave as motion's hover gestures do: they respond to a pointer that can hover, and touch devices typically skip them. Press states respond to taps.
Does this work the same in Astro, TanStack Start, React Router and Vite?
Yes. Components, variants, styles and the kit are the same React and TypeScript code in all five frameworks; only routing and the server differ. See Framer to React.
