@framer/agent 0.0.0 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2526 @@
1
+ ---
2
+ name: framer-code-components
3
+ description: "Framer code component implementation guidance, platform constraints, layout annotations, property controls, and authoring best practices. Use only after loading the framer skill and the generated framer-project-<projectId> skill first in the same task; never load this skill directly as the entry point."
4
+ ---
5
+
6
+ # Framer Code Components
7
+
8
+ ## Best Practices
9
+
10
+ ### Component Structure
11
+
12
+ ```tsx
13
+ import { addPropertyControls, ControlType } from "framer";
14
+ import { motion } from "framer-motion"; // NOT from "framer"
15
+
16
+ interface MyComponentProps {
17
+ /* typed props */
18
+ }
19
+
20
+ /**
21
+ * @framerSupportedLayoutWidth any-prefer-fixed
22
+ * @framerSupportedLayoutHeight any-prefer-fixed
23
+ */
24
+ export default function MyComponent(props: MyComponentProps) {
25
+ // component
26
+ }
27
+
28
+ addPropertyControls(MyComponent, {
29
+ /* controls */
30
+ });
31
+ ```
32
+
33
+ ### Platform Constraints
34
+
35
+ These will cause errors if violated:
36
+
37
+ 1. **Single file, default export** - Use named `function` syntax (not arrow functions), no named exports
38
+ 2. **Imports** - Only `react`, `react-dom`, `framer`, `framer-motion`. Import `motion` from `"framer-motion"`, not `"framer"`
39
+ 3. **Position** - Use `position: relative` on the root element, never `fixed`
40
+ 4. **SSR** - Guard `window`/`document` access: `if (typeof window !== "undefined")`
41
+ 5. **Annotations** - Include `@framerSupportedLayoutWidth/Height` in a `/** */` block comment immediately above the component function
42
+ 6. **Types** - Provide a typed props interface (e.g. `MyComponentProps`). Avoid NodeJS types like `Timeout` — use `number` instead
43
+
44
+ ### Layout Annotations
45
+
46
+ | Content | Width | Height |
47
+ | ----------------- | ------------------ | ------------------ |
48
+ | No intrinsic size | `fixed` | `fixed` |
49
+ | Text/auto-sizing | `auto` | `auto` |
50
+ | Flexible | `any-prefer-fixed` | `any-prefer-fixed` |
51
+
52
+ Detect auto vs fixed sizing: check if `style.width` or `style.height` is `"100%"`.
53
+
54
+ ### Property Controls
55
+
56
+ To make components configurable in Framer's properties panel, add property controls:
57
+
58
+ - To make colors customizable, use `ControlType.Color`. Reuse the same prop for elements sharing a color.
59
+ - To make text styling customizable, use `ControlType.Font` with `controls: "extended"` and `defaultFontType: "sans-serif"`.
60
+ - For images, use `ControlType.ResponsiveImage`. Set defaults in the component body via destructuring (the control doesn't support `defaultValue`).
61
+ - Provide a `defaultValue` for every prop so components render correctly in the Framer canvas. Include at least one item in `ControlType.Array` controls.
62
+ - `ComponentName.defaultProps` is not supported in Framer — use `defaultValue` on the property control instead.
63
+ - Use `hidden` for conditional visibility: `hidden: (props) => !props.showFeature`
64
+ - Prefer sliders over steppers unless step values are large.
65
+ - Keep controls focused — make key elements configurable, hardcode the rest.
66
+ - Full property control reference is included below in this same skill.
67
+
68
+ ### Image Defaults (in component body)
69
+
70
+ ```tsx
71
+ const {
72
+ image = {
73
+ src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
74
+ alt: "Default",
75
+ },
76
+ } = props;
77
+ ```
78
+
79
+ ### Animation Performance
80
+
81
+ ```tsx
82
+ import { useIsStaticRenderer } from "framer";
83
+ import { useInView } from "framer-motion";
84
+
85
+ const isStatic = useIsStaticRenderer();
86
+ const ref = useRef(null);
87
+ const isInView = useInView(ref);
88
+
89
+ if (isStatic) return <StaticPreview />; // Show useful static state
90
+ // Pause animations when out of viewport
91
+ ```
92
+
93
+ - For very complex animations, consider WebGL instead of `framer-motion`.
94
+ - Static preview should include visual effects, not just text.
95
+ - Wrapping state updates in `startTransition()` prevents UI blocking and keeps interactions smooth.
96
+
97
+ ### Text
98
+
99
+ - For auto-sized components with text, apply `width: max-content` or `minWidth: max-content` to prevent text from collapsing.
100
+
101
+ ### Common Errors
102
+
103
+ - WebGL cross-origin: handle `SecurityError: Failed to execute 'texImage2D'` for cross-origin images.
104
+ - Inverted Y-axis: check if WebGL images render upside down and accommodate.
105
+
106
+ ### Accessibility
107
+
108
+ - `aria` roles on interactive elements
109
+ - Semantic HTML (`<nav>`, `<article>`, `<section>`)
110
+ - `alt=""` on decorative images
111
+ - 4.5:1 color contrast
112
+
113
+ ## Term Interpretation
114
+
115
+ - "responsive" → width/height 100%
116
+ - "modern" → 8px radius, 16px spacing, subtle shadows
117
+ - "minimal" → limited colors, whitespace
118
+ - "interactive" → hover/active states
119
+ - "accessible" → ARIA, semantic HTML
120
+ - "props"/"properties" → Framer property controls
121
+
122
+ # Framer Property Controls
123
+
124
+ Control types for property controls in Framer code components. Each control type specifies a different user interface for receiving input. All control types accept `title`, `description`, and `hidden` properties.
125
+
126
+ ## Table of Contents
127
+
128
+ - [Boolean](#boolean)
129
+ - [Number](#number)
130
+ - [String](#string)
131
+ - [Enum](#enum)
132
+ - [Color](#color)
133
+ - [ResponsiveImage](#responsiveimage)
134
+ - [File](#file)
135
+ - [Array](#array)
136
+ - [Slot](#slot)
137
+ - [EventHandler](#eventhandler)
138
+ - [Font](#font)
139
+ - [Transition](#transition)
140
+ - [BoxShadow](#boxshadow)
141
+ - [Link](#link)
142
+ - [Date](#date)
143
+ - [Object](#object)
144
+ - [Border](#border)
145
+ - [Cursor](#cursor)
146
+ - [Padding](#padding)
147
+ - [BorderRadius](#borderradius)
148
+ - [Gap](#gap)
149
+ - [TrackingId](#trackingid)
150
+ - [Deprecated Controls](#deprecated-controls)
151
+ - [TypeScript Interfaces](#typescript-interfaces)
152
+
153
+ ## Boolean
154
+
155
+ A control that displays an on/off checkbox. The associated property will be `true` or `false`, depending on the state of the checkbox.
156
+
157
+ **Properties:**
158
+
159
+ - `defaultValue`: Set to `true` by default
160
+ - `optional`: Whether the control value is optional
161
+ - `enabledTitle`: Customize the label when enabled _(deprecated)_
162
+ - `disabledTitle`: Customize the label when disabled _(deprecated)_
163
+
164
+ **Example:**
165
+
166
+ ```javascript
167
+ export function MyComponent(props) {
168
+ return <Frame size={"100%"}>{props.showText ? "Hello World" : null}</Frame>;
169
+ }
170
+
171
+ addPropertyControls(MyComponent, {
172
+ showText: {
173
+ type: ControlType.Boolean,
174
+ title: "Show Text",
175
+ defaultValue: true,
176
+ },
177
+ });
178
+ ```
179
+
180
+ ## Number
181
+
182
+ A control that accepts any numeric value. This will be provided directly as a property. Displays an input field with a range slider by default.
183
+
184
+ **Properties:**
185
+
186
+ - `defaultValue`: The default numeric value
187
+ - `min`: Minimum allowed value
188
+ - `max`: Maximum allowed value
189
+ - `unit`: Unit label (e.g., "deg", "px")
190
+ - `step`: Increment value for the slider
191
+ - `displayStepper`: Enable to show a stepper control instead
192
+ - `optional`: Whether the control value is optional
193
+
194
+ **Example:**
195
+
196
+ ```javascript
197
+ export function MyComponent(props) {
198
+ return (
199
+ <Frame rotateZ={props.rotation} size={"100%"}>
200
+ {props.rotation}
201
+ </Frame>
202
+ );
203
+ }
204
+
205
+ addPropertyControls(MyComponent, {
206
+ rotation: {
207
+ type: ControlType.Number,
208
+ defaultValue: 0,
209
+ min: 0,
210
+ max: 360,
211
+ unit: "deg",
212
+ step: 0.1,
213
+ displayStepper: true,
214
+ },
215
+ });
216
+ ```
217
+
218
+ ## String
219
+
220
+ A control that accepts plain text values. Displays an input field with an optional placeholder value.
221
+
222
+ **Properties:**
223
+
224
+ - `defaultValue`: The default text value
225
+ - `placeholder`: Placeholder text
226
+ - `obscured`: Set to true to use a password input field
227
+ - `displayTextArea`: Enable for a multi-line input area
228
+ - `preventLocalization`: Prevents automatic translation of the text
229
+ - `optional`: Whether the control value is optional
230
+
231
+ **Example:**
232
+
233
+ ```javascript
234
+ export function MyComponent(props) {
235
+ return (
236
+ <Frame>
237
+ {props.title} — {props.body}
238
+ </Frame>
239
+ );
240
+ }
241
+
242
+ addPropertyControls(MyComponent, {
243
+ title: {
244
+ type: ControlType.String,
245
+ defaultValue: "Framer",
246
+ placeholder: "Type something…",
247
+ },
248
+ body: {
249
+ type: ControlType.String,
250
+ defaultValue: "Lorem ipsum dolor sit amet.",
251
+ placeholder: "Type something…",
252
+ displayTextArea: true,
253
+ },
254
+ });
255
+ ```
256
+
257
+ ## Enum
258
+
259
+ A property control that represents a list of options. The selected option will be provided as a property. Displayed as a dropdown menu.
260
+
261
+ **Properties:**
262
+
263
+ - `defaultValue`: The default selected option (string, boolean, number, or null)
264
+ - `options`: Array of unique values (string, boolean, number, or null)
265
+ - `optionTitles`: Display names for the options
266
+ - `displaySegmentedControl`: Enable to display a segmented control instead
267
+ - `segmentedControlDirection`: Direction of the segmented control ('horizontal' or 'vertical')
268
+
269
+ **Example:**
270
+
271
+ ```javascript
272
+ export function MyComponent(props) {
273
+ const value = props.value || "a";
274
+ const colors = { a: "red", b: "green", c: "blue" };
275
+ return (
276
+ <Frame background={colors[value]} size={"100%"}>
277
+ {value}
278
+ </Frame>
279
+ );
280
+ }
281
+
282
+ addPropertyControls(MyComponent, {
283
+ value: {
284
+ type: ControlType.Enum,
285
+ defaultValue: "a",
286
+ options: ["a", "b", "c"],
287
+ optionTitles: ["Option A", "Option B", "Option C"],
288
+ },
289
+ });
290
+ ```
291
+
292
+ **Example with Segmented Control:**
293
+
294
+ ```javascript
295
+ addPropertyControls(MyComponent, {
296
+ alignment: {
297
+ type: ControlType.Enum,
298
+ defaultValue: "left",
299
+ options: ["left", "center", "right"],
300
+ optionTitles: ["Left", "Center", "Right"],
301
+ displaySegmentedControl: true,
302
+ segmentedControlDirection: "horizontal",
303
+ },
304
+ });
305
+ ```
306
+
307
+ ## Color
308
+
309
+ A control that represents a color value. The selected color is provided as a string in either HEX (`"#fff"`) or HSL (`hsla(203, 87%, 50%, 0.5)`) notation, depending on whether there is an alpha channel.
310
+
311
+ **Properties:**
312
+
313
+ - `defaultValue`: The default color value
314
+ - `optional`: Whether the color is optional
315
+
316
+ **Example:**
317
+
318
+ ```javascript
319
+ function MyComponent(props) {
320
+ return <Frame background={props.background} size={"100%"} />;
321
+ }
322
+
323
+ addPropertyControls(MyComponent, {
324
+ background: {
325
+ type: ControlType.Color,
326
+ defaultValue: "#fff",
327
+ },
328
+ });
329
+ ```
330
+
331
+ ## ResponsiveImage
332
+
333
+ A control that allows the user to pick an image resource. Displayed as an image picker with associated file picker.
334
+
335
+ **Returns an object with:**
336
+
337
+ - `src`: URL string of the full resolution image
338
+ - `srcSet`: Optional string with scaled image variants (for `<img srcSet>`)
339
+ - `alt`: Optional description of the image
340
+
341
+ **Example:**
342
+
343
+ ```javascript
344
+ function MyComponent(props) {
345
+ return (
346
+ <img
347
+ src={props.image?.src}
348
+ srcSet={props.image?.srcSet}
349
+ alt={props.image?.alt}
350
+ />
351
+ );
352
+ }
353
+
354
+ addPropertyControls(MyComponent, {
355
+ image: {
356
+ type: ControlType.ResponsiveImage,
357
+ },
358
+ });
359
+ ```
360
+
361
+ ## File
362
+
363
+ A control that allows the user to pick a file resource. The selected file will be provided as a fully qualified URL.
364
+
365
+ **Properties:**
366
+
367
+ - `allowedFileTypes`: Array specifying acceptable file types. Supported formats:
368
+ - Media types (`"image/png"`, `"audio/*"`, `"*/*"`)
369
+ - File extensions with dot (`".png"`, `".mov"`)
370
+ - File extensions without dot (`"png"`) for backward compatibility
371
+ - Wildcard (`"*"`) to allow everything
372
+
373
+ **Example:**
374
+
375
+ ```javascript
376
+ export function MyComponent(props) {
377
+ return (
378
+ <Frame size={"100%"}>
379
+ <video
380
+ style={{
381
+ objectFit: "contain",
382
+ width: props.width,
383
+ height: props.height,
384
+ }}
385
+ src={props.filepath}
386
+ controls
387
+ />
388
+ </Frame>
389
+ );
390
+ }
391
+
392
+ addPropertyControls(MyComponent, {
393
+ filepath: {
394
+ type: ControlType.File,
395
+ allowedFileTypes: ["mov", "mp4"],
396
+ },
397
+ });
398
+ ```
399
+
400
+ ## Array
401
+
402
+ A control that allows multiple values per `ControlType`, provided as an array via properties. Displays as an additional section in the properties panel.
403
+
404
+ **Properties:**
405
+
406
+ - `control`: The control type to repeat
407
+ - `minCount`: Minimum number of items
408
+ - `maxCount`: Maximum number of items
409
+ - `defaultValue`: Default array values
410
+
411
+ **Example with Objects:**
412
+
413
+ ```javascript
414
+ export function MyComponent(props) {
415
+ return (
416
+ <Stack size={"100%"}>
417
+ {props.items.map((item, index) => (
418
+ <div key={index}>{item.title}</div>
419
+ ))}
420
+ </Stack>
421
+ );
422
+ }
423
+
424
+ addPropertyControls(MyComponent, {
425
+ items: {
426
+ type: ControlType.Array,
427
+ control: {
428
+ type: ControlType.Object,
429
+ controls: {
430
+ title: { type: ControlType.String, defaultValue: "Item" },
431
+ image: { type: ControlType.ResponsiveImage },
432
+ },
433
+ },
434
+ defaultValue: [{ title: "First" }, { title: "Second" }],
435
+ maxCount: 10,
436
+ },
437
+ });
438
+ ```
439
+
440
+ ## Slot
441
+
442
+ A control that references one or more other components on the canvas, included in the component props as a React node. By default allows any number of components to be linked.
443
+
444
+ **Properties:**
445
+
446
+ - `maxCount`: Maximum number of components to be linked
447
+
448
+ **Example:**
449
+
450
+ ```javascript
451
+ export function MyComponent(props) {
452
+ return <Stack size={"100%"}>{props.children}</Stack>;
453
+ }
454
+
455
+ addPropertyControls(MyComponent, {
456
+ children: {
457
+ type: ControlType.Slot,
458
+ maxCount: 5,
459
+ },
460
+ });
461
+ ```
462
+
463
+ ## EventHandler
464
+
465
+ A control that exposes events in the prototyping panel within the Framer UI. When choosing an event, you can select from a list of actions to trigger.
466
+
467
+ **Example:**
468
+
469
+ ```javascript
470
+ export function MyComponent(props) {
471
+ return <Frame onTap={props.onTap} size={"100%"} />;
472
+ }
473
+
474
+ addPropertyControls(MyComponent, {
475
+ onTap: {
476
+ type: ControlType.EventHandler,
477
+ },
478
+ });
479
+ ```
480
+
481
+ ## Font
482
+
483
+ A control that allows for selecting a font to be used in the component.
484
+
485
+ **Properties:**
486
+
487
+ - `defaultValue`: Default font settings
488
+ - `controls`: Specifies control options ("basic" or "extended")
489
+ - `defaultFontType`: Default font type ("sans-serif", "serif", or "monospace")
490
+ - `displayTextAlignment`: Whether to display text alignment options
491
+ - `displayFontSize`: Whether to display font size options
492
+
493
+ **Default Value Options:**
494
+
495
+ - `textAlign`: "left", "right", "center", or "justify"
496
+ - `fontSize`: string or number (e.g., "16px", 16)
497
+ - `letterSpacing`: string or number (e.g., "-0.01em", 0.1)
498
+ - `lineHeight`: string or number (e.g., "1.5em", 1.5, "150%")
499
+ - `variant`: Font variant (only for "sans-serif" font type)
500
+
501
+ **Example:**
502
+
503
+ ```javascript
504
+ export function MyComponent(props) {
505
+ return <div style={props.customFont}>Hello World</div>;
506
+ }
507
+
508
+ addPropertyControls(MyComponent, {
509
+ customFont: {
510
+ type: ControlType.Font,
511
+ defaultValue: {
512
+ fontSize: "16px",
513
+ variant: "Medium",
514
+ letterSpacing: "-0.01em",
515
+ lineHeight: "1.2em",
516
+ textAlign: "left",
517
+ },
518
+ controls: "extended",
519
+ defaultFontType: "sans-serif",
520
+ },
521
+ });
522
+ ```
523
+
524
+ ## Transition
525
+
526
+ A control that allows for editing Framer Motion transition options within the Framer UI.
527
+
528
+ **Properties:**
529
+
530
+ - `defaultValue`: Default transition (null or Transition object)
531
+
532
+ **Example:**
533
+
534
+ ```javascript
535
+ export function MyComponent(props) {
536
+ return <Frame animate={{ scale: 2 }} transition={props.transition} />;
537
+ }
538
+
539
+ addPropertyControls(MyComponent, {
540
+ transition: {
541
+ type: ControlType.Transition,
542
+ },
543
+ });
544
+ ```
545
+
546
+ ## BoxShadow
547
+
548
+ A control that allows for exposing shadows. The value will be provided as a string with valid CSS box-shadow values.
549
+
550
+ **Properties:**
551
+
552
+ - `defaultValue`: Default shadow (string or BoxShadow array)
553
+
554
+ **Example:**
555
+
556
+ ```javascript
557
+ export function MyComponent(props) {
558
+ return <motion.div style={{ boxShadow: props.shadow }} />;
559
+ }
560
+
561
+ addPropertyControls(MyComponent, {
562
+ shadow: {
563
+ type: ControlType.BoxShadow,
564
+ },
565
+ });
566
+ ```
567
+
568
+ ## Link
569
+
570
+ A control that allows for exposing web links.
571
+
572
+ **Properties:**
573
+
574
+ - `defaultValue`: Default URL as string
575
+
576
+ **Example:**
577
+
578
+ ```javascript
579
+ export function MyComponent(props) {
580
+ return <a href={props.link}>My Link</a>;
581
+ }
582
+
583
+ addPropertyControls(MyComponent, {
584
+ link: {
585
+ type: ControlType.Link,
586
+ },
587
+ });
588
+ ```
589
+
590
+ ## Date
591
+
592
+ A control that allows for exposing dates. The value will be provided in toJSON() string format.
593
+
594
+ **Properties:**
595
+
596
+ - `displayTime`: Whether to include time selection
597
+ - `defaultValue`: Default date as ISO string
598
+ - `optional`: Whether the date is optional
599
+
600
+ **Example:**
601
+
602
+ ```javascript
603
+ export function MyComponent(props) {
604
+ const formattedDate = React.useMemo(() => {
605
+ return props.date ? new Date(props.date).toLocaleDateString() : "No date";
606
+ }, [props.date]);
607
+ return <div>{formattedDate}</div>;
608
+ }
609
+
610
+ addPropertyControls(MyComponent, {
611
+ date: {
612
+ type: ControlType.Date,
613
+ displayTime: true,
614
+ optional: true,
615
+ },
616
+ });
617
+ ```
618
+
619
+ ## Object
620
+
621
+ A control that allows for grouping multiple properties as an object.
622
+
623
+ **Properties:**
624
+
625
+ - `controls`: Object containing nested controls
626
+ - `defaultValue`: Default object values
627
+ - `buttonTitle`: Custom button title
628
+ - `optional`: Whether the object is optional
629
+ - `icon`: Icon to display ('object', 'effect', 'color', 'interaction', or 'boolean')
630
+
631
+ **Example:**
632
+
633
+ ```javascript
634
+ export function MyComponent(props) {
635
+ return (
636
+ <Frame opacity={props.style?.opacity} background={props.style?.tint} />
637
+ );
638
+ }
639
+
640
+ addPropertyControls(MyComponent, {
641
+ style: {
642
+ type: ControlType.Object,
643
+ optional: true,
644
+ icon: "effect",
645
+ controls: {
646
+ opacity: { type: ControlType.Number, defaultValue: 1, min: 0, max: 1 },
647
+ tint: { type: ControlType.Color, defaultValue: "#000" },
648
+ },
649
+ },
650
+ });
651
+ ```
652
+
653
+ ## Border
654
+
655
+ A control that represents a border.
656
+
657
+ **Properties:**
658
+
659
+ - `defaultValue`: Default border settings
660
+ - `optional`: Whether the border is optional
661
+
662
+ **Border Value Object:**
663
+
664
+ - `borderColor`: CSS color string
665
+ - `borderStyle`: "solid", "dashed", "dotted", or "double"
666
+ - `borderWidth`: Uniform width (number)
667
+ - `borderTopWidth`, `borderLeftWidth`, `borderRightWidth`, `borderBottomWidth`: Per-side widths
668
+
669
+ **Example:**
670
+
671
+ ```javascript
672
+ function MyComponent(props) {
673
+ return <div style={props.border} />;
674
+ }
675
+
676
+ addPropertyControls(MyComponent, {
677
+ border: {
678
+ type: ControlType.Border,
679
+ defaultValue: {
680
+ borderWidth: 1,
681
+ borderStyle: "solid",
682
+ borderColor: "rgba(0, 0, 0, 0.5)",
683
+ },
684
+ },
685
+ });
686
+ ```
687
+
688
+ ## Cursor
689
+
690
+ A control that allows specifying a web cursor that should be shown when mousing over the element assigned.
691
+
692
+ **Properties:**
693
+
694
+ - `defaultValue`: Default cursor value (CSS cursor string)
695
+
696
+ **Example:**
697
+
698
+ ```javascript
699
+ function MyComponent(props) {
700
+ return <div style={{ cursor: props.cursor }}>Hover me</div>;
701
+ }
702
+
703
+ addPropertyControls(MyComponent, {
704
+ cursor: {
705
+ type: ControlType.Cursor,
706
+ defaultValue: "pointer",
707
+ },
708
+ });
709
+ ```
710
+
711
+ ## Padding
712
+
713
+ A control that represents CSS padding.
714
+
715
+ **Properties:**
716
+
717
+ - `defaultValue`: Default padding value (e.g., "8px", "10px 20px", "10px 20px 30px 40px")
718
+
719
+ **Example:**
720
+
721
+ ```javascript
722
+ function MyComponent({ padding }) {
723
+ return <div style={{ padding }}>Content</div>;
724
+ }
725
+
726
+ addPropertyControls(MyComponent, {
727
+ padding: {
728
+ type: ControlType.Padding,
729
+ defaultValue: "8px",
730
+ },
731
+ });
732
+ ```
733
+
734
+ ## BorderRadius
735
+
736
+ A control that represents CSS border radius.
737
+
738
+ **Properties:**
739
+
740
+ - `defaultValue`: Default border radius value (e.g., "16px", "8px 16px")
741
+
742
+ **Example:**
743
+
744
+ ```javascript
745
+ function MyComponent({ borderRadius }) {
746
+ return <div style={{ borderRadius, background: "red" }} />;
747
+ }
748
+
749
+ addPropertyControls(MyComponent, {
750
+ borderRadius: {
751
+ type: ControlType.BorderRadius,
752
+ defaultValue: "16px",
753
+ title: "Radius",
754
+ },
755
+ });
756
+ ```
757
+
758
+ ## Gap
759
+
760
+ A control that represents CSS gap for grid/flex layouts.
761
+
762
+ **Properties:**
763
+
764
+ - `defaultValue`: Default gap value (e.g., "8px", "8px 16px")
765
+
766
+ **Example:**
767
+
768
+ ```javascript
769
+ function MyComponent({ gap, children }) {
770
+ return <div style={{ display: "flex", gap }}>{children}</div>;
771
+ }
772
+
773
+ addPropertyControls(MyComponent, {
774
+ gap: {
775
+ type: ControlType.Gap,
776
+ defaultValue: "8px",
777
+ },
778
+ children: {
779
+ type: ControlType.Slot,
780
+ },
781
+ });
782
+ ```
783
+
784
+ ## TrackingId
785
+
786
+ A control that represents an ID for tracking events.
787
+
788
+ **Format Requirements:**
789
+
790
+ - Lowercase letters (a-z) and numbers (0-9) only
791
+ - Hyphens (-) as separators (no leading/trailing or consecutive hyphens)
792
+ - Valid: "button-click", "form-submit", "video-play", "nav-item-1"
793
+ - Invalid: "Button-Click", "form--submit", "-button-click", "button_utils"
794
+
795
+ **Properties:**
796
+
797
+ - `defaultValue`: Default tracking ID string
798
+
799
+ **Example:**
800
+
801
+ ```javascript
802
+ function MyComponent(props) {
803
+ const handleClick = () => {
804
+ // Track the event using props.trackingId
805
+ analytics.track(props.trackingId);
806
+ };
807
+ return <button onClick={handleClick}>Click me</button>;
808
+ }
809
+
810
+ addPropertyControls(MyComponent, {
811
+ trackingId: {
812
+ type: ControlType.TrackingId,
813
+ defaultValue: "button-click",
814
+ },
815
+ });
816
+ ```
817
+
818
+ ## Deprecated Controls
819
+
820
+ ### ControlType.Image
821
+
822
+ **Deprecated.** Use `ControlType.ResponsiveImage` instead. The `src` field provides the image URL.
823
+
824
+ ### ControlType.ComponentInstance
825
+
826
+ **Deprecated.** Use `ControlType.Slot` instead. The new Slot type doesn't need to be nested within an array control for multiple items. By default, Slot allows infinite items; use `maxCount` to limit.
827
+
828
+ ### ControlType.SegmentedEnum
829
+
830
+ **Deprecated.** Use `ControlType.Enum` with `displaySegmentedControl: true` instead.
831
+
832
+ ### ControlType.FusedNumber
833
+
834
+ **Deprecated.** Use `ControlType.Padding` and `ControlType.BorderRadius` instead. These new controls provide a single value (e.g., "10px" or "10px 20px 30px 40px").
835
+
836
+ ---
837
+
838
+ # Framer Property Control Types
839
+
840
+ TypeScript interfaces for all Framer property control types. ## Base Control Description
841
+
842
+ All control descriptions extend this base interface:
843
+
844
+ ```typescript
845
+ export interface BaseControlDescription<P = any> {
846
+ title?: string;
847
+ description?: string;
848
+ hidden?: ((props: P, rootProps: any) => boolean) | boolean;
849
+ }
850
+
851
+ export interface WithOptional {
852
+ optional?: boolean;
853
+ }
854
+ ```
855
+
856
+ ## Control Type Interfaces
857
+
858
+ ### Boolean Control
859
+
860
+ ```typescript
861
+ export interface BooleanControlDescription<P = any>
862
+ extends BaseControlDescription<P>, WithOptional {
863
+ type: ControlType.Boolean;
864
+ defaultValue?: boolean;
865
+ /** @deprecated */
866
+ disabledTitle?: string;
867
+ /** @deprecated */
868
+ enabledTitle?: string;
869
+ }
870
+ ```
871
+
872
+ ### Number Control
873
+
874
+ ```typescript
875
+ export interface NumberControlDescription<P = any>
876
+ extends BaseControlDescription<P>, WithOptional {
877
+ type: ControlType.Number;
878
+ defaultValue?: number;
879
+ max?: number;
880
+ min?: number;
881
+ unit?: string;
882
+ step?: number;
883
+ displayStepper?: boolean;
884
+ }
885
+ ```
886
+
887
+ ### String Control
888
+
889
+ ```typescript
890
+ export interface StringControlDescription<P = any>
891
+ extends BaseControlDescription<P>, WithOptional {
892
+ type: ControlType.String;
893
+ defaultValue?: string;
894
+ placeholder?: string;
895
+ obscured?: boolean;
896
+ displayTextArea?: boolean;
897
+ preventLocalization?: boolean;
898
+ }
899
+ ```
900
+
901
+ ### Enum Control
902
+
903
+ ```typescript
904
+ export interface EnumControlDescription<
905
+ P = any,
906
+ > extends BaseControlDescription<P> {
907
+ type: ControlType.Enum;
908
+ defaultValue?: string | boolean | number | null;
909
+ options: (string | boolean | number | null)[];
910
+ optionTitles?: string[];
911
+ displaySegmentedControl?: boolean;
912
+ segmentedControlDirection?: "horizontal" | "vertical";
913
+ }
914
+ ```
915
+
916
+ ### Color Control
917
+
918
+ ```typescript
919
+ export interface ColorControlDescription<P = any>
920
+ extends BaseControlDescription<P>, WithOptional {
921
+ type: ControlType.Color;
922
+ defaultValue?: string;
923
+ }
924
+ ```
925
+
926
+ ### ResponsiveImage Control
927
+
928
+ ```typescript
929
+ export interface ResponsiveImageControlDescription<
930
+ P = any,
931
+ > extends BaseControlDescription<P> {
932
+ type: ControlType.ResponsiveImage;
933
+ }
934
+ ```
935
+
936
+ ### File Control
937
+
938
+ ```typescript
939
+ export type AllowedFileTypes = readonly string[];
940
+
941
+ export interface FileControlDescription<
942
+ P = any,
943
+ > extends BaseControlDescription<P> {
944
+ type: ControlType.File;
945
+ allowedFileTypes: AllowedFileTypes;
946
+ }
947
+ ```
948
+
949
+ ### Slot Control
950
+
951
+ ```typescript
952
+ export interface SlotControlDescription<
953
+ P = any,
954
+ > extends BaseControlDescription<P> {
955
+ type: ControlType.Slot;
956
+ maxCount?: number;
957
+ }
958
+ ```
959
+
960
+ ### Array Control
961
+
962
+ ```typescript
963
+ export interface ArrayControlDescription<
964
+ P = any,
965
+ > extends BaseControlDescription<P> {
966
+ type: ControlType.Array;
967
+ control: ArrayItemControlDescription<P>;
968
+ minCount?: number;
969
+ maxCount?: number;
970
+ defaultValue?: any[];
971
+ }
972
+ ```
973
+
974
+ ### Object Control
975
+
976
+ ```typescript
977
+ export type ObjectControlIcon =
978
+ | "object"
979
+ | "effect"
980
+ | "color"
981
+ | "interaction"
982
+ | "boolean";
983
+
984
+ export interface ObjectControlDescription<P = any>
985
+ extends BaseControlDescription<P>, WithOptional {
986
+ type: ControlType.Object;
987
+ controls: { [key: string]: ObjectPropertyControlDescription };
988
+ defaultValue?: { [key: string]: any };
989
+ buttonTitle?: string;
990
+ icon?: ObjectControlIcon;
991
+ }
992
+ ```
993
+
994
+ ### Event Handler Control
995
+
996
+ ```typescript
997
+ export interface EventHandlerControlDescription<
998
+ P = any,
999
+ > extends BaseControlDescription<P> {
1000
+ type: ControlType.EventHandler;
1001
+ }
1002
+ ```
1003
+
1004
+ ### Transition Control
1005
+
1006
+ ```typescript
1007
+ import type { Transition } from "framer-motion";
1008
+
1009
+ export interface TransitionControlDescription<
1010
+ P = any,
1011
+ > extends BaseControlDescription<P> {
1012
+ type: ControlType.Transition;
1013
+ defaultValue?: null | Transition;
1014
+ }
1015
+ ```
1016
+
1017
+ ### BoxShadow Control
1018
+
1019
+ ```typescript
1020
+ export interface BoxShadowControlDescription<
1021
+ P = any,
1022
+ > extends BaseControlDescription<P> {
1023
+ type: ControlType.BoxShadow;
1024
+ defaultValue?: string | readonly BoxShadow[];
1025
+ }
1026
+ ```
1027
+
1028
+ ### Link Control
1029
+
1030
+ ```typescript
1031
+ export interface LinkControlDescription<
1032
+ P = any,
1033
+ > extends BaseControlDescription<P> {
1034
+ type: ControlType.Link;
1035
+ defaultValue?: string;
1036
+ }
1037
+ ```
1038
+
1039
+ ### Date Control
1040
+
1041
+ ```typescript
1042
+ export interface DateControlDescription<P = any>
1043
+ extends BaseControlDescription<P>, WithOptional {
1044
+ type: ControlType.Date;
1045
+ displayTime?: boolean;
1046
+ defaultValue?: string;
1047
+ }
1048
+ ```
1049
+
1050
+ ### Border Control
1051
+
1052
+ ```typescript
1053
+ export type BorderStyle = "solid" | "dashed" | "dotted" | "double";
1054
+
1055
+ export interface Border {
1056
+ borderColor?: string;
1057
+ borderStyle?: BorderStyle;
1058
+ borderWidth?: number;
1059
+ borderTopWidth?: number;
1060
+ borderLeftWidth?: number;
1061
+ borderRightWidth?: number;
1062
+ borderBottomWidth?: number;
1063
+ }
1064
+
1065
+ export interface BorderControlDescription<P = any>
1066
+ extends BaseControlDescription<P>, WithOptional {
1067
+ type: ControlType.Border;
1068
+ defaultValue?: Border;
1069
+ }
1070
+ ```
1071
+
1072
+ ### Cursor Control
1073
+
1074
+ ```typescript
1075
+ export interface CursorControlDescription<
1076
+ P = any,
1077
+ > extends BaseControlDescription<P> {
1078
+ type: ControlType.Cursor;
1079
+ defaultValue?: string;
1080
+ }
1081
+ ```
1082
+
1083
+ ### Padding Control
1084
+
1085
+ ```typescript
1086
+ export interface PaddingControlDescription<
1087
+ P = any,
1088
+ > extends BaseControlDescription<P> {
1089
+ type: ControlType.Padding;
1090
+ defaultValue?: string;
1091
+ }
1092
+ ```
1093
+
1094
+ ### Border Radius Control
1095
+
1096
+ ```typescript
1097
+ export interface BorderRadiusControlDescription<
1098
+ P = any,
1099
+ > extends BaseControlDescription<P> {
1100
+ type: ControlType.BorderRadius;
1101
+ defaultValue?: string;
1102
+ }
1103
+ ```
1104
+
1105
+ ### Gap Control
1106
+
1107
+ ```typescript
1108
+ export interface GapControlDescription<
1109
+ P = any,
1110
+ > extends BaseControlDescription<P> {
1111
+ type: ControlType.Gap;
1112
+ defaultValue?: string;
1113
+ }
1114
+ ```
1115
+
1116
+ ### Tracking ID Control
1117
+
1118
+ ```typescript
1119
+ export interface TrackingIdControlDescription<
1120
+ P = any,
1121
+ > extends BaseControlDescription<P> {
1122
+ type: ControlType.TrackingId;
1123
+ defaultValue?: string;
1124
+ }
1125
+ ```
1126
+
1127
+ ### Font Control
1128
+
1129
+ ```typescript
1130
+ interface FontControlDescriptionBase<
1131
+ P = any,
1132
+ > extends BaseControlDescription<P> {
1133
+ type: ControlType.Font;
1134
+ controls?: "basic" | "extended";
1135
+ displayTextAlignment?: boolean;
1136
+ displayFontSize?: boolean;
1137
+ defaultValue?: FontControlDefaultValueBase;
1138
+ }
1139
+
1140
+ interface FontControlDescriptionSansSerif<
1141
+ P = any,
1142
+ > extends FontControlDescriptionBase<P> {
1143
+ defaultFontType?: "sans-serif";
1144
+ defaultValue?: FontControlDefaultValueWithVariant;
1145
+ }
1146
+
1147
+ interface FontControlDescriptionSerif<
1148
+ P = any,
1149
+ > extends FontControlDescriptionBase<P> {
1150
+ defaultFontType?: "serif";
1151
+ defaultValue?: FontControlDefaultValueBase;
1152
+ }
1153
+
1154
+ interface FontControlDescriptionMonospace<
1155
+ P = any,
1156
+ > extends FontControlDescriptionBase<P> {
1157
+ defaultFontType?: "monospace";
1158
+ defaultValue?: FontControlDefaultValueBase;
1159
+ }
1160
+
1161
+ export type FontControlDescription<P = any> =
1162
+ | FontControlDescriptionSansSerif<P>
1163
+ | FontControlDescriptionSerif<P>
1164
+ | FontControlDescriptionMonospace<P>;
1165
+
1166
+ interface FontControlDefaultValueBase {
1167
+ textAlign?: "left" | "right" | "center" | "justify";
1168
+ fontSize?: string | number;
1169
+ letterSpacing?: string | number;
1170
+ lineHeight?: string | number;
1171
+ }
1172
+
1173
+ interface FontControlDefaultValueWithVariant extends FontControlDefaultValueBase {
1174
+ variant?: FramerFontVariant;
1175
+ }
1176
+
1177
+ export const framerFontVariants = [
1178
+ "Regular",
1179
+ "Thin",
1180
+ "Extra Light",
1181
+ "Light",
1182
+ "Medium",
1183
+ "Semibold",
1184
+ "Bold",
1185
+ "Extra Bold",
1186
+ "Black",
1187
+ "Thin Italic",
1188
+ "Extra Light Italic",
1189
+ "Light Italic",
1190
+ "Italic",
1191
+ "Medium Italic",
1192
+ "Semibold Italic",
1193
+ "Bold Italic",
1194
+ "Extra Bold Italic",
1195
+ "Black Italic",
1196
+ "Regular Italic",
1197
+ "Variable",
1198
+ "Variable Italic",
1199
+ ] as const;
1200
+
1201
+ export type FramerFontVariant = (typeof framerFontVariants)[number];
1202
+ ```
1203
+
1204
+ ## Composite Types
1205
+
1206
+ ### All Control Types
1207
+
1208
+ ```typescript
1209
+ export type ControlDescription<P = any> =
1210
+ | NumberControlDescription<P>
1211
+ | EnumControlDescription<P>
1212
+ | BooleanControlDescription<P>
1213
+ | StringControlDescription<P>
1214
+ | ColorControlDescription<P>
1215
+ | ResponsiveImageControlDescription<P>
1216
+ | FileControlDescription<P>
1217
+ | SlotControlDescription<P>
1218
+ | ArrayControlDescription<P>
1219
+ | EventHandlerControlDescription<P>
1220
+ | TransitionControlDescription<P>
1221
+ | BoxShadowControlDescription<P>
1222
+ | LinkControlDescription<P>
1223
+ | DateControlDescription<P>
1224
+ | ObjectControlDescription<P>
1225
+ | FontControlDescription<P>
1226
+ | BorderControlDescription<P>
1227
+ | CursorControlDescription<P>
1228
+ | PaddingControlDescription<P>
1229
+ | BorderRadiusControlDescription<P>
1230
+ | GapControlDescription<P>
1231
+ | TrackingIdControlDescription<P>;
1232
+ ```
1233
+
1234
+ ### Property Controls
1235
+
1236
+ ```typescript
1237
+ export type PropertyControls<ComponentProps = any, ArrayTypes = any> = {
1238
+ [K in keyof ComponentProps]?: ControlDescription<Partial<ComponentProps>>;
1239
+ };
1240
+ ```
1241
+
1242
+ ## Associated Methods and Types
1243
+
1244
+ ### addPropertyControls
1245
+
1246
+ ```typescript
1247
+ export declare function addPropertyControls<Props = any>(
1248
+ component:
1249
+ | React.ComponentType<Props>
1250
+ | React.ForwardRefExoticComponent<Props>,
1251
+ propertyControls: PropertyControls<Props>,
1252
+ ): void;
1253
+ ```
1254
+
1255
+ ### addFonts
1256
+
1257
+ ```typescript
1258
+ export declare function addFonts(
1259
+ component: React.ComponentType<unknown>,
1260
+ fonts: any[],
1261
+ flags?: { supportsExplicitInterCodegen?: boolean },
1262
+ ): void;
1263
+ ```
1264
+
1265
+ ### Data API
1266
+
1267
+ ```typescript
1268
+ export declare const Data: {
1269
+ <T extends object = object>(initial?: Partial<T> | object): T;
1270
+ };
1271
+ ```
1272
+
1273
+ ### Renderer Detection APIs
1274
+
1275
+ ```typescript
1276
+ export declare type RenderTarget = RenderTargetName;
1277
+
1278
+ export declare const RenderTarget: {
1279
+ canvas: RenderTargetName;
1280
+ export: RenderTargetName;
1281
+ thumbnail: RenderTargetName;
1282
+ preview: RenderTargetName;
1283
+ current: () => RenderTargetName;
1284
+ hasRestrictions: () => boolean;
1285
+ };
1286
+
1287
+ /** Check if executed in a Framer Canvas or Export Canvas environment */
1288
+ export declare function isStaticRenderer(): boolean;
1289
+
1290
+ /** Hook to check if in a static renderer (Canvas or Export) */
1291
+ export declare function useIsStaticRenderer(): boolean;
1292
+
1293
+ /** Hook to observe data changes */
1294
+ export declare function useObserveData(): boolean;
1295
+ ```
1296
+
1297
+ ### Color Interface and Utilities
1298
+
1299
+ ```typescript
1300
+ export interface Color {
1301
+ r: number;
1302
+ g: number;
1303
+ b: number;
1304
+ h: number;
1305
+ s: number;
1306
+ l: number;
1307
+ a: number;
1308
+ roundA: number;
1309
+ format: ColorFormat;
1310
+ initialValue?: string;
1311
+ isValid?: boolean;
1312
+ mix: any;
1313
+ toValue: () => string;
1314
+ }
1315
+
1316
+ export enum ColorFormat {
1317
+ RGB = "rgb",
1318
+ HSL = "hsl",
1319
+ HSV = "hsv",
1320
+ HEX = "hex",
1321
+ NAME = "name",
1322
+ }
1323
+ ```
1324
+
1325
+ ---
1326
+
1327
+ # Property Control Guide
1328
+
1329
+ Font styling patterns, variant mapping, and recommended default values for Framer property controls. ## Styling of text elements
1330
+
1331
+ When using Control Properties on text elements, do not introduce any new control properties for styles which can be applied with `ControlType.Font` and `FontControlDescription` respectively. Specifically:
1332
+ - `FontControlDescription.defaultValue.fontSize` for `font-size`
1333
+ - `FontControlDescription.defaultValue.textAlignment` for `text-alignment`
1334
+ - `FontControlDescription.defaultValue.letterSpacing` for `letter-spacing`
1335
+ - `FontControlDescription.defaultValue.lineHeight` for `line-height`
1336
+ - `FontControlDescription.defaultValue.variant` for `font-weight` and/or `font-style`
1337
+ - `FontControlDescription.defaultValue.variant` can be set only if `FontControlDescription.defaultFontType` is set to `"sans-serif"`
1338
+
1339
+ Remarks:
1340
+ - `FontControlDescription.defaultValue.fontFamily` is not a valid default value. Font family cannot be set via `defaultValue` — it can only be selected by the user through the font control UI.
1341
+ - Set `FontControlDescription.controls` to `"extended"` to expose full typography options (size, weight, spacing, alignment).
1342
+
1343
+ When you need to use font weight you should use `FontControlDescription.defaultValue.variant`.
1344
+ The variant encapsulates both the font weight and style together. Refer to the following object to determine the correct variant for a given font weight:
1345
+
1346
+ ```ts
1347
+ interface ResolvedFontVariant {
1348
+ fontStyle: "normal" | "italic"
1349
+ weight: number
1350
+ }
1351
+
1352
+ const variantNameToFontWeight: Record<FramerFontVariant, ResolvedFontVariant> = {
1353
+ Regular: { fontStyle: "normal", fontWeight: 400 },
1354
+ Thin: { fontStyle: "normal", fontWeight: 100 },
1355
+ "Extra Light": { fontStyle: "normal", fontWeight: 200 },
1356
+ Light: { fontStyle: "normal", fontWeight: 300 },
1357
+ Medium: { fontStyle: "normal", fontWeight: 500 },
1358
+ Semibold: { fontStyle: "normal", fontWeight: 600 },
1359
+ Bold: { fontStyle: "normal", fontWeight: 700 },
1360
+ "Extra Bold": { fontStyle: "normal", fontWeight: 800 },
1361
+ Black: { fontStyle: "normal", fontWeight: 900 },
1362
+ "Thin Italic": { fontStyle: "italic", fontWeight: 100 },
1363
+ "Extra Light Italic": { fontStyle: "italic", fontWeight: 200 },
1364
+ "Light Italic": { fontStyle: "italic", fontWeight: 300 },
1365
+ Italic: { fontStyle: "italic", fontWeight: 400 },
1366
+ "Medium Italic": { fontStyle: "italic", fontWeight: 500 },
1367
+ "Semibold Italic": { fontStyle: "italic", fontWeight: 600 },
1368
+ "Bold Italic": { fontStyle: "italic", fontWeight: 700 },
1369
+ "Extra Bold Italic": { fontStyle: "italic", fontWeight: 800 },
1370
+ "Black Italic": { fontStyle: "italic", fontWeight: 900 },
1371
+ "Regular Italic": { fontStyle: "italic", fontWeight: 400 },
1372
+ }
1373
+ ```
1374
+
1375
+ Example of a simple text component in Framer which demonstrates how to use Property Control of type `ControlType.Font`.
1376
+
1377
+ ```tsx
1378
+ import { addPropertyControls, ControlType } from "framer"
1379
+
1380
+ /**
1381
+ * @framerSupportedLayoutWidth auto
1382
+ * @framerSupportedLayoutHeight auto
1383
+ */
1384
+ export default function SimpleText(props) {
1385
+ const { label, heading } = props
1386
+ return (
1387
+ <span
1388
+ style={{
1389
+ fontSize: heading.fontSize,
1390
+ textAlign: heading.textAlign,
1391
+ fontWeight: heading.fontWeight,
1392
+ fontFamily: heading.fontFamily,
1393
+ lineHeight: heading.lineHeight,
1394
+ letterSpacing: heading.letterSpacing,
1395
+ fontStyle: heading.fontStyle,
1396
+ }}
1397
+ >
1398
+ {label}
1399
+ </span>
1400
+ )
1401
+ }
1402
+
1403
+ addPropertyControls(SimpleText, {
1404
+ heading: {
1405
+ type: ControlType.Font,
1406
+ title: "Heading 2 Font",
1407
+ defaultValue: {
1408
+ textAlign: "right",
1409
+ fontSize: 40,
1410
+ variant: "Extra Bold",
1411
+ letterSpacing: "-0.03em",
1412
+ lineHeight: "1em",
1413
+ },
1414
+ controls: "extended",
1415
+ defaultFontType: "sans-serif",
1416
+ },
1417
+ label: {
1418
+ title: "Label",
1419
+ type: ControlType.String,
1420
+ defaultValue: "Hello",
1421
+ },
1422
+ })
1423
+ ```
1424
+
1425
+ ## Default Control Values
1426
+
1427
+ Recommended default values for common control types.
1428
+
1429
+ ### Colors
1430
+
1431
+ Recommended color defaults:
1432
+
1433
+ ```typescript
1434
+ const colors: Record<string, ColorControlDescription> = {
1435
+ /** Use for main container backgrounds, cards, and primary surfaces */
1436
+ background: {
1437
+ type: ControlType.Color,
1438
+ defaultValue: "#FFFFFF", // White: backgrounds
1439
+ },
1440
+ /** Use for secondary backgrounds, input fields, and subtle visual elements */
1441
+ subtleBackground: {
1442
+ type: ControlType.Color,
1443
+ defaultValue: "#F5F5F5", // Very light gray: subtle backgrounds, placeholders
1444
+ },
1445
+ /** Use for borders, dividers, and visual separators */
1446
+ darkBackground: {
1447
+ type: ControlType.Color,
1448
+ defaultValue: "#EEEEEE", // Light gray: borders, separators
1449
+ },
1450
+ /** Use for secondary text, icons, and less prominent UI elements */
1451
+ tertiary: {
1452
+ type: ControlType.Color,
1453
+ defaultValue: "#CCCCCC", // Medium gray: text, icons
1454
+ },
1455
+ /** Use for primary text, icons, and key UI elements that need emphasis */
1456
+ primary: {
1457
+ type: ControlType.Color,
1458
+ defaultValue: "#000000", // Black: text, icons
1459
+ },
1460
+ }
1461
+ ```
1462
+
1463
+ ### Images
1464
+
1465
+ To provide a default image, set it via destructuring in the component body (`ControlType.ResponsiveImage` does not support `defaultValue`):
1466
+ ```tsx
1467
+ const { image = { src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg", alt: "Gradient 1 - Blue" } } = props
1468
+ ```
1469
+ When applying image properties to elements, use spreads like `{...image}`.
1470
+
1471
+ Recommended image sources (gradient series — use in sequence when multiple are needed):
1472
+
1473
+ ```typescript
1474
+ const images = {
1475
+ /** Use for professional or corporate contexts, informational content, or quinary image slot */
1476
+ image1: {
1477
+ src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
1478
+ alt: "Gradient 1 - Blue"
1479
+ },
1480
+ /** Use for creative or innovative contexts, feature highlights, or quaternary image slot */
1481
+ image2: {
1482
+ src: "https://framerusercontent.com/images/aNsAT3jCvt4zglbWCUoFe33Q.jpg",
1483
+ alt: "Gradient 2 - Purple"
1484
+ },
1485
+ /** Use for energetic contexts, call-to-action backgrounds, or tertiary image slot */
1486
+ image3: {
1487
+ src: "https://framerusercontent.com/images/BYnxEV1zjYb9bhWh1IwBZ1ZoS60.jpg",
1488
+ alt: "Gradient 3 - Orange"
1489
+ },
1490
+ /** Use for warm-toned contexts, product showcases, or secondary image slot */
1491
+ image4: {
1492
+ src: "https://framerusercontent.com/images/2uTNEj5aTl2K3NJaEFWMbnrA.jpg",
1493
+ alt: "Gradient 4 - Yellow"
1494
+ },
1495
+ /** Use for nature-themed components, environmental contexts, or primary image slot */
1496
+ image5: {
1497
+ src: "https://framerusercontent.com/images/f9RiWoNpmlCMqVRIHz8l8wYfeI.jpg",
1498
+ alt: "Gradient 5 - Green"
1499
+ }
1500
+ }
1501
+ ```
1502
+
1503
+ ### Typography
1504
+
1505
+ Use these exact font definitions for all text elements
1506
+
1507
+ ```typescript
1508
+ const typography: Record<string, FontControlDescription> = {
1509
+ /** Use for main page titles and primary headlines */
1510
+ heading1: {
1511
+ type: ControlType.Font,
1512
+ title: "Heading 1 Font",
1513
+ defaultValue: {
1514
+ fontSize: "40px",
1515
+ variant: "Bold",
1516
+ letterSpacing: "-0.04em",
1517
+ lineHeight: "1em",
1518
+ },
1519
+ controls: "extended",
1520
+ defaultFontType: "sans-serif",
1521
+ },
1522
+ /** Use for section titles and secondary headlines */
1523
+ heading2: {
1524
+ type: ControlType.Font,
1525
+ title: "Heading 2 Font",
1526
+ defaultValue: {
1527
+ fontSize: "32px",
1528
+ variant: "Semibold",
1529
+ letterSpacing: "-0.03em",
1530
+ lineHeight: "1em",
1531
+ },
1532
+ controls: "extended",
1533
+ defaultFontType: "sans-serif",
1534
+ },
1535
+ /** Use for subsection titles and feature headings */
1536
+ heading3: {
1537
+ type: ControlType.Font,
1538
+ title: "Heading 3 Font",
1539
+ defaultValue: {
1540
+ fontSize: "22px",
1541
+ variant: "Semibold",
1542
+ letterSpacing: "-0.01em",
1543
+ lineHeight: "1.2em",
1544
+ },
1545
+ controls: "extended",
1546
+ defaultFontType: "sans-serif",
1547
+ },
1548
+ /** Use for card titles, list headings, and UI element headers */
1549
+ heading4: {
1550
+ type: ControlType.Font,
1551
+ title: "Heading 4 Font",
1552
+ defaultValue: {
1553
+ fontSize: "15px",
1554
+ variant: "Medium",
1555
+ letterSpacing: "-0.01em",
1556
+ lineHeight: "1em",
1557
+ },
1558
+ controls: "extended",
1559
+ defaultFontType: "sans-serif",
1560
+ },
1561
+ /** Use for body text, descriptions, and general content */
1562
+ paragraph: {
1563
+ type: ControlType.Font,
1564
+ title: "Paragraph Font",
1565
+ defaultValue: {
1566
+ fontSize: "15px",
1567
+ variant: "Medium",
1568
+ letterSpacing: "-0.01em",
1569
+ lineHeight: "1.3em",
1570
+ },
1571
+ controls: "extended",
1572
+ defaultFontType: "sans-serif",
1573
+ },
1574
+ /** Use for buttons, links, and interactive text elements */
1575
+ buttonText: {
1576
+ type: ControlType.Font,
1577
+ title: "Button Text Font",
1578
+ defaultValue: {
1579
+ variant: "Semibold",
1580
+ fontSize: "14px",
1581
+ letterSpacing: "-0.01em",
1582
+ lineHeight: "1em",
1583
+ },
1584
+ controls: "extended",
1585
+ defaultFontType: "sans-serif",
1586
+ },
1587
+ }
1588
+ ```
1589
+
1590
+ ### File Types
1591
+
1592
+ `ControlType.File` does not support `defaultValue` in its property control. Set default values through component parameter destructuring instead.
1593
+
1594
+ ```typescript
1595
+ const fileTypes: Record<string, FileControlDescription> = {
1596
+ /** Use for image upload fields, gallery components, and avatar selectors */
1597
+ images: {
1598
+ type: ControlType.File,
1599
+ allowedFileTypes: ["jpg", "jpeg", "png", "gif", "webp", "svg"],
1600
+ },
1601
+ /** Use for video players, media galleries, and promotional content */
1602
+ videos: {
1603
+ type: ControlType.File,
1604
+ allowedFileTypes: ["mp4", "webm", "mov"],
1605
+ },
1606
+ /** Use for document viewers, file download components, and resource sections */
1607
+ documents: {
1608
+ type: ControlType.File,
1609
+ allowedFileTypes: ["pdf", "doc", "docx", "txt"],
1610
+ },
1611
+ /** Use for audio players, podcast components, and music interfaces */
1612
+ audio: {
1613
+ type: ControlType.File,
1614
+ allowedFileTypes: ["mp3", "wav", "ogg"],
1615
+ },
1616
+ }
1617
+ ```
1618
+
1619
+ Use the following values for each file type as default values:
1620
+
1621
+ ```typescript
1622
+ const defaultValues: Record<keyof typeof fileTypes, string> = {
1623
+ images: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
1624
+ videos: "https://framerusercontent.com/assets/MLWPbW1dUQawJLhhun3dBwpgJak.mp4",
1625
+ audio: "https://framerusercontent.com/assets/8w3IUatLX9a5JVJ6XPCVuHi94.mp3",
1626
+ }
1627
+ ```
1628
+
1629
+ Recommended pattern for file control defaults:
1630
+
1631
+ ```tsx
1632
+ function MyComponent(props) {
1633
+ // CORRECT: Set file defaults through parameter destructuring
1634
+ const {
1635
+ imageFile = "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
1636
+ videoFile = "https://framerusercontent.com/assets/MLWPbW1dUQawJLhhun3dBwpgJak.mp4",
1637
+ audioFile = "https://framerusercontent.com/assets/8w3IUatLX9a5JVJ6XPCVuHi94.mp3",
1638
+ } = props
1639
+
1640
+ return (
1641
+ <div>
1642
+ <img src={imageFile} />
1643
+ <video src={videoFile} />
1644
+ <audio src={audioFile} />
1645
+ </div>
1646
+ )
1647
+ }
1648
+
1649
+ addPropertyControls(MyComponent, {
1650
+ imageFile: {
1651
+ type: ControlType.File,
1652
+ allowedFileTypes: ["jpg", "jpeg", "png", "gif", "webp", "svg"],
1653
+ },
1654
+ // Additional file controls...
1655
+ })
1656
+ ```
1657
+
1658
+ # Framer Code Component Examples
1659
+
1660
+ Reference implementations demonstrating best practices for code components.
1661
+
1662
+ ## Cookie Banner
1663
+
1664
+ Location-aware cookie consent with timezone detection.
1665
+
1666
+ ```tsx
1667
+ // Cookie banner with opt-in for Europe, opt-out elsewhere, based on time zone
1668
+ import {
1669
+ useEffect,
1670
+ useState,
1671
+ startTransition,
1672
+ type CSSProperties,
1673
+ } from "react";
1674
+ import { addPropertyControls, ControlType, RenderTarget } from "framer";
1675
+
1676
+ interface CookiebannerProps {
1677
+ message: string;
1678
+ acceptLabel: string;
1679
+ declineLabel: string;
1680
+ backgroundColor: string;
1681
+ textColor: string;
1682
+ buttonColor: string;
1683
+ buttonTextColor: string;
1684
+ font: any;
1685
+ borderRadius: number;
1686
+ buttonFont: any;
1687
+ style?: CSSProperties;
1688
+ }
1689
+
1690
+ /**
1691
+ * Cookies
1692
+ *
1693
+ * @framerIntrinsicWidth 400
1694
+ * @framerIntrinsicHeight 100
1695
+ *
1696
+ * @framerSupportedLayoutWidth any-prefer-fixed
1697
+ * @framerSupportedLayoutHeight any-prefer-fixed
1698
+ */
1699
+ export default function Cookiebanner(props: CookiebannerProps) {
1700
+ const {
1701
+ message,
1702
+ acceptLabel,
1703
+ declineLabel,
1704
+ backgroundColor,
1705
+ textColor,
1706
+ buttonColor,
1707
+ buttonTextColor,
1708
+ font,
1709
+ borderRadius,
1710
+ } = props;
1711
+
1712
+ // Guess if user is in Europe based on timezone offset
1713
+ const [show, setShow] = useState(true);
1714
+ const [isEurope, setIsEurope] = useState(false);
1715
+ useEffect(() => {
1716
+ if (typeof window !== "undefined") {
1717
+ const offset = new Date().getTimezoneOffset();
1718
+ // Europe: UTC+0 to UTC+3 (offset -0 to -180)
1719
+ startTransition(() => setIsEurope(offset <= 0 && offset >= -180));
1720
+ }
1721
+ }, []);
1722
+
1723
+ // Hide on accept/decline
1724
+ function handleAccept() {
1725
+ startTransition(() => setShow(false));
1726
+ }
1727
+ function handleDecline() {
1728
+ startTransition(() => setShow(false));
1729
+ }
1730
+
1731
+ if (!show || RenderTarget.current() === RenderTarget.thumbnail) return null;
1732
+
1733
+ const buttonBaseStyles = {
1734
+ borderRadius: 10,
1735
+ flex: 1,
1736
+ border: `1px solid ${buttonColor}`,
1737
+ padding: "8px 18px",
1738
+ cursor: "pointer",
1739
+ ...props.buttonFont,
1740
+ };
1741
+
1742
+ const isFixedWidth = props?.style && props.style.width === "100%";
1743
+
1744
+ return (
1745
+ <div
1746
+ style={{
1747
+ ...props.style,
1748
+ overflow: "hidden",
1749
+ position: "relative",
1750
+ ...(isFixedWidth ? { ...props?.style } : { minWidth: "max-content" }),
1751
+ background: backgroundColor,
1752
+ color: textColor,
1753
+ borderRadius,
1754
+ display: "flex",
1755
+ flexDirection: "column",
1756
+ justifyContent: "space-between",
1757
+ padding: 20,
1758
+ boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
1759
+ gap: 20,
1760
+
1761
+ ...props.font,
1762
+ }}
1763
+ >
1764
+ <span style={{ flex: 1 }}>{message}</span>
1765
+ <div style={{ width: "100%", display: "flex", gap: 10 }}>
1766
+ <button
1767
+ style={{
1768
+ ...buttonBaseStyles,
1769
+ background: "transparent",
1770
+ color: buttonColor,
1771
+ }}
1772
+ onClick={handleDecline}
1773
+ >
1774
+ {declineLabel}
1775
+ </button>
1776
+ <button
1777
+ style={{
1778
+ ...buttonBaseStyles,
1779
+ background: buttonColor,
1780
+ color: buttonTextColor,
1781
+ }}
1782
+ onClick={handleAccept}
1783
+ >
1784
+ {acceptLabel}
1785
+ </button>
1786
+ </div>
1787
+ </div>
1788
+ );
1789
+ }
1790
+
1791
+ addPropertyControls(Cookiebanner, {
1792
+ message: {
1793
+ type: ControlType.String,
1794
+ title: "Message",
1795
+ defaultValue: "We use cookies to improve your website experience.",
1796
+ displayTextArea: true,
1797
+ },
1798
+ acceptLabel: {
1799
+ type: ControlType.String,
1800
+ title: "Accept Label",
1801
+ defaultValue: "Accept",
1802
+ },
1803
+ declineLabel: {
1804
+ type: ControlType.String,
1805
+ title: "Decline Label",
1806
+ defaultValue: "Decline",
1807
+ },
1808
+ backgroundColor: {
1809
+ type: ControlType.Color,
1810
+ title: "Background",
1811
+ defaultValue: "#fff",
1812
+ },
1813
+ textColor: {
1814
+ type: ControlType.Color,
1815
+ title: "Text Color",
1816
+ defaultValue: "#222",
1817
+ },
1818
+ buttonColor: {
1819
+ type: ControlType.Color,
1820
+ title: "Button Color",
1821
+ defaultValue: "#111",
1822
+ },
1823
+ buttonTextColor: {
1824
+ type: ControlType.Color,
1825
+ title: "Button Text",
1826
+ defaultValue: "#fff",
1827
+ },
1828
+ font: {
1829
+ type: ControlType.Font,
1830
+ title: "Font",
1831
+ controls: "extended",
1832
+ defaultFontType: "sans-serif",
1833
+ defaultValue: {
1834
+ variant: "Medium",
1835
+ fontSize: "14px",
1836
+ letterSpacing: "-0.01em",
1837
+ lineHeight: "1em",
1838
+ },
1839
+ },
1840
+ buttonFont: {
1841
+ type: ControlType.Font,
1842
+ title: "Font",
1843
+ controls: "extended",
1844
+ defaultFontType: "sans-serif",
1845
+ defaultValue: {
1846
+ variant: "Medium",
1847
+ fontSize: "14px",
1848
+ letterSpacing: "-0.01em",
1849
+ lineHeight: "1em",
1850
+ },
1851
+ },
1852
+ borderRadius: {
1853
+ type: ControlType.Number,
1854
+ title: "Radius",
1855
+ defaultValue: 8,
1856
+ min: 0,
1857
+ max: 32,
1858
+ },
1859
+ });
1860
+ ```
1861
+
1862
+ ## Tweemoji
1863
+
1864
+ Convert emoji to Twitter's Twemoji SVGs.
1865
+
1866
+ ````tsx
1867
+ import { useMemo, useEffect, useState, type CSSProperties } from "react";
1868
+ import { addPropertyControls, ControlType, withCSS } from "framer";
1869
+ import twemojiParser from "https://jspm.dev/twemoji-parser@14.0.0";
1870
+
1871
+ const fireSrc =
1872
+ "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f525.svg";
1873
+
1874
+ interface TwemojiProps {
1875
+ /** Emoji to convert such as 🍐, 🐙 or 🐸 */
1876
+ search?: string;
1877
+ isSelection?: boolean;
1878
+ [prop: string]: any;
1879
+ }
1880
+
1881
+ const baseURL = "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/";
1882
+
1883
+ /**
1884
+ * TWEMOJI
1885
+ *
1886
+ * Convert any emoji into a Twemoji from Twitter. Choose a preset or type in your emoji and the Twemoji will automatically appear on the canvas.
1887
+ *
1888
+ * ```jsx
1889
+ * <Twemoji search="🍐" />
1890
+ * ```
1891
+ *
1892
+ * @framerIntrinsicWidth 100
1893
+ * @framerIntrinsicHeight 100
1894
+ *
1895
+ * @framerSupportedLayoutWidth fixed
1896
+ * @framerSupportedLayoutHeight fixed
1897
+ */
1898
+ export default function Twemoji(props: TwemojiProps) {
1899
+ const { search, isSelection, selection, style, alt = "" } = props;
1900
+
1901
+ const emoji = useMemo(() => {
1902
+ if (isSelection) return selection;
1903
+ if (!search) return "⭐️";
1904
+ return search;
1905
+ }, [search, isSelection, selection]);
1906
+
1907
+ const src = useMemo(() => {
1908
+ const parsedTwemoji = twemojiParser.parse(emoji, {
1909
+ buildUrl: (icon) => `${baseURL}${icon}.svg`,
1910
+ });
1911
+ return parsedTwemoji[0].url;
1912
+ }, [emoji]);
1913
+
1914
+ return (
1915
+ <div style={containerStyle}>
1916
+ <img src={src} style={containerStyle} alt={alt} />
1917
+ </div>
1918
+ );
1919
+ }
1920
+
1921
+ addPropertyControls<TwemojiProps>(Twemoji, {
1922
+ isSelection: {
1923
+ type: ControlType.Boolean,
1924
+ title: "Select",
1925
+ enabledTitle: "Preset",
1926
+ disabledTitle: "Search",
1927
+ },
1928
+ selection: {
1929
+ type: ControlType.Enum,
1930
+ title: " ",
1931
+ options: ["🔥", "💖", "😆", "👍", "👎"],
1932
+ defaultValue: "🔥",
1933
+ displaySegmentedControl: true,
1934
+ hidden: ({ isSelection }) => !isSelection,
1935
+ },
1936
+ search: {
1937
+ type: ControlType.String,
1938
+ title: " ",
1939
+ placeholder: "Paste Emoji…",
1940
+ defaultValue: "⭐️",
1941
+ hidden: ({ isSelection }) => isSelection,
1942
+ },
1943
+ });
1944
+
1945
+ const containerStyle: CSSProperties = {
1946
+ height: "100%",
1947
+ width: "100%",
1948
+ objectFit: "contain",
1949
+ textAlign: "center",
1950
+ overflow: "hidden",
1951
+ backgroundColor: "transparent",
1952
+ };
1953
+ ````
1954
+
1955
+ ## Image Compare
1956
+
1957
+ Before/after image comparison slider.
1958
+
1959
+ ```tsx
1960
+ import {
1961
+ addPropertyControls,
1962
+ ControlType,
1963
+ RenderTarget,
1964
+ useIsStaticRenderer,
1965
+ } from "framer";
1966
+ import {
1967
+ useCallback,
1968
+ useEffect,
1969
+ useRef,
1970
+ useState,
1971
+ startTransition,
1972
+ type CSSProperties,
1973
+ } from "react";
1974
+
1975
+ interface Image {
1976
+ src: string;
1977
+ alt: string;
1978
+ }
1979
+
1980
+ interface ImageCompareProps {
1981
+ beforeImage: Image;
1982
+ afterImage: Image;
1983
+ orientation: "horizontal" | "vertical";
1984
+ initialPosition: number;
1985
+ dividerColor: string;
1986
+ dividerWidth: number;
1987
+ dividerShadow: boolean;
1988
+ showHandle: boolean;
1989
+ handleColor: string;
1990
+ handleSize: number;
1991
+ style?: CSSProperties;
1992
+ }
1993
+
1994
+ /**
1995
+ * Image Comparison Slider
1996
+ *
1997
+ * A component that allows users to compare two images by dragging a divider.
1998
+ *
1999
+ * @framerIntrinsicWidth 500
2000
+ * @framerIntrinsicHeight 300
2001
+ *
2002
+ * @framerSupportedLayoutWidth fixed
2003
+ * @framerSupportedLayoutHeight fixed
2004
+ */
2005
+ export default function ImageCompare(props: ImageCompareProps) {
2006
+ const {
2007
+ beforeImage = {
2008
+ src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
2009
+ alt: "Before image",
2010
+ },
2011
+ afterImage = {
2012
+ src: "https://framerusercontent.com/images/aNsAT3jCvt4zglbWCUoFe33Q.jpg",
2013
+ alt: "After image",
2014
+ },
2015
+ orientation = "horizontal",
2016
+ initialPosition = 50,
2017
+ dividerColor = "#FFFFFF",
2018
+ dividerWidth = 2,
2019
+ dividerShadow = true,
2020
+ showHandle = false,
2021
+ handleColor = "#FFFFFF",
2022
+ handleSize = 40,
2023
+ } = props;
2024
+
2025
+ const isHorizontal = orientation === "horizontal";
2026
+ const containerRef = useRef<HTMLDivElement>(null);
2027
+ const [position, setPosition] = useState(initialPosition);
2028
+ const [isDragging, setIsDragging] = useState(false);
2029
+ const isStatic = useIsStaticRenderer();
2030
+
2031
+ const updatePositionFromEvent = useCallback(
2032
+ (e) => {
2033
+ if (!containerRef.current) return;
2034
+
2035
+ const rect = containerRef.current.getBoundingClientRect();
2036
+
2037
+ if (isHorizontal) {
2038
+ const x = e.clientX - rect.left;
2039
+ const newPosition = Math.max(0, Math.min(100, (x / rect.width) * 100));
2040
+ startTransition(() => setPosition(newPosition));
2041
+ } else {
2042
+ const y = e.clientY - rect.top;
2043
+ const newPosition = Math.max(0, Math.min(100, (y / rect.height) * 100));
2044
+ startTransition(() => setPosition(newPosition));
2045
+ }
2046
+ },
2047
+ [isHorizontal],
2048
+ );
2049
+
2050
+ const handleClick = useCallback(
2051
+ (e) => {
2052
+ // Only handle as a click if we're not dragging
2053
+ if (!isDragging) {
2054
+ updatePositionFromEvent(e);
2055
+ }
2056
+ },
2057
+ [isDragging, updatePositionFromEvent],
2058
+ );
2059
+
2060
+ const handleDoubleClick = () => {
2061
+ startTransition(() => setPosition(initialPosition));
2062
+ };
2063
+
2064
+ const handleMouseDown = (e) => {
2065
+ e.preventDefault();
2066
+ startTransition(() => setIsDragging(true));
2067
+ };
2068
+
2069
+ const handleMouseMove = useCallback(
2070
+ (e) => {
2071
+ if (!isDragging || !containerRef.current) return;
2072
+ updatePositionFromEvent(e);
2073
+ },
2074
+ [isDragging, updatePositionFromEvent],
2075
+ );
2076
+
2077
+ const handleMouseUp = useCallback(() => {
2078
+ startTransition(() => setIsDragging(false));
2079
+ }, []);
2080
+
2081
+ // Add global event listeners for drag
2082
+ useEffect(() => {
2083
+ if (isStatic) return;
2084
+
2085
+ const handleGlobalMouseMove = (e) => handleMouseMove(e);
2086
+ const handleGlobalMouseUp = () => handleMouseUp();
2087
+
2088
+ if (isDragging) {
2089
+ window.addEventListener("mousemove", handleGlobalMouseMove);
2090
+ window.addEventListener("mouseup", handleGlobalMouseUp);
2091
+ }
2092
+
2093
+ return () => {
2094
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
2095
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
2096
+ };
2097
+ }, [isDragging, handleMouseMove, handleMouseUp, isStatic]);
2098
+
2099
+ return (
2100
+ <div
2101
+ ref={containerRef}
2102
+ style={{
2103
+ position: "relative",
2104
+ width: "100%",
2105
+ height: "100%",
2106
+ overflow: "hidden",
2107
+ cursor: isDragging
2108
+ ? isHorizontal
2109
+ ? "ew-resize"
2110
+ : "ns-resize"
2111
+ : "pointer",
2112
+ userSelect: "none",
2113
+ }}
2114
+ onClick={isStatic ? undefined : handleClick}
2115
+ onMouseMove={isStatic ? undefined : handleMouseMove}
2116
+ onMouseDown={isStatic ? undefined : handleMouseDown}
2117
+ onMouseUp={isStatic ? undefined : handleMouseUp}
2118
+ onDoubleClick={handleDoubleClick}
2119
+ onKeyDown={(e) => {
2120
+ if (e.key === " " || e.key === "Enter") {
2121
+ handleClick(e);
2122
+ }
2123
+ }}
2124
+ tabIndex={0}
2125
+ role="slider"
2126
+ aria-valuenow={position}
2127
+ aria-valuemin={0}
2128
+ aria-valuemax={100}
2129
+ aria-orientation={orientation}
2130
+ >
2131
+ {/* After Image (Full) */}
2132
+ <div
2133
+ style={{
2134
+ position: "absolute",
2135
+ top: 0,
2136
+ left: 0,
2137
+ width: "100%",
2138
+ height: "100%",
2139
+ backgroundImage: `url(${afterImage.src})`,
2140
+ backgroundSize: "cover",
2141
+ backgroundPosition: "center",
2142
+ }}
2143
+ aria-label={afterImage.alt}
2144
+ role="img"
2145
+ />
2146
+
2147
+ {/* Before Image (Clipped) */}
2148
+ <div
2149
+ style={{
2150
+ position: "absolute",
2151
+ top: 0,
2152
+ left: 0,
2153
+ width: "100%",
2154
+ height: "100%",
2155
+ backgroundImage: `url(${beforeImage.src})`,
2156
+ backgroundSize: "cover",
2157
+ backgroundPosition: "center",
2158
+ clipPath: isHorizontal
2159
+ ? `inset(0 ${100 - position}% 0 0)`
2160
+ : `inset(0 0 ${100 - position}% 0)`,
2161
+ }}
2162
+ aria-label={beforeImage.alt}
2163
+ role="img"
2164
+ />
2165
+
2166
+ {/* Divider */}
2167
+ <div
2168
+ style={{
2169
+ position: "absolute",
2170
+ top: isHorizontal ? 0 : `${position}%`,
2171
+ left: isHorizontal ? `${position}%` : 0,
2172
+ width: isHorizontal ? `${dividerWidth}px` : "100%",
2173
+ height: isHorizontal ? "100%" : `${dividerWidth}px`,
2174
+ backgroundColor: dividerColor,
2175
+ boxShadow: dividerShadow ? "0 0 5px rgba(0, 0, 0, 0.7)" : "none",
2176
+ transform: isHorizontal
2177
+ ? `translateX(-${dividerWidth / 2}px)`
2178
+ : `translateY(-${dividerWidth / 2}px)`,
2179
+ cursor: isHorizontal ? "ew-resize" : "ns-resize",
2180
+ zIndex: 2,
2181
+ }}
2182
+ onMouseDown={isStatic ? undefined : handleMouseDown}
2183
+ />
2184
+
2185
+ {/* Handle */}
2186
+ {showHandle && (
2187
+ <div
2188
+ style={{
2189
+ position: "absolute",
2190
+ top: isHorizontal
2191
+ ? `calc(50% - ${handleSize / 2}px)`
2192
+ : `${position}%`,
2193
+ left: isHorizontal
2194
+ ? `${position}%`
2195
+ : `calc(50% - ${handleSize / 2}px)`,
2196
+ width: `${handleSize}px`,
2197
+ height: `${handleSize}px`,
2198
+ borderRadius: "50%",
2199
+ backgroundColor: handleColor,
2200
+ border: `2px solid ${handleColor}`,
2201
+ boxShadow: "0 0 5px rgba(0, 0, 0, 0.5)",
2202
+ transform: isHorizontal
2203
+ ? `translateX(-${handleSize / 2}px)`
2204
+ : `translateY(-${handleSize / 2}px)`,
2205
+ cursor: isHorizontal ? "ew-resize" : "ns-resize",
2206
+ zIndex: 3,
2207
+ display: "flex",
2208
+ justifyContent: "center",
2209
+ alignItems: "center",
2210
+ }}
2211
+ onMouseDown={isStatic ? undefined : handleMouseDown}
2212
+ >
2213
+ <div
2214
+ style={{
2215
+ display: "flex",
2216
+ justifyContent: "center",
2217
+ alignItems: "center",
2218
+ width: "100%",
2219
+ height: "100%",
2220
+ transform: isHorizontal ? "rotate(90deg)" : "rotate(0deg)",
2221
+ }}
2222
+ >
2223
+ <svg
2224
+ viewBox="0 0 24 24"
2225
+ width={handleSize * 0.5}
2226
+ height={handleSize * 0.5}
2227
+ strokeWidth="2"
2228
+ stroke="#000"
2229
+ fill="none"
2230
+ aria-label="Drag handle"
2231
+ >
2232
+ <title>Drag handle</title>
2233
+ <path d="M13 5l6 6m-6 6l6-6m-6 0l-6 6m6-6l-6-6" />
2234
+ </svg>
2235
+ </div>
2236
+ </div>
2237
+ )}
2238
+ </div>
2239
+ );
2240
+ }
2241
+
2242
+ addPropertyControls(ImageCompare, {
2243
+ beforeImage: {
2244
+ type: ControlType.ResponsiveImage,
2245
+ title: "Before Image",
2246
+ },
2247
+ afterImage: {
2248
+ type: ControlType.ResponsiveImage,
2249
+ title: "After Image",
2250
+ },
2251
+ orientation: {
2252
+ type: ControlType.Enum,
2253
+ title: "Orientation",
2254
+ options: ["horizontal", "vertical"],
2255
+ optionTitles: ["Horizontal", "Vertical"],
2256
+ defaultValue: "horizontal",
2257
+ displaySegmentedControl: true,
2258
+ },
2259
+ initialPosition: {
2260
+ type: ControlType.Number,
2261
+ title: "Initial Position",
2262
+ defaultValue: 50,
2263
+ min: 0,
2264
+ max: 100,
2265
+ step: 1,
2266
+ unit: "%",
2267
+ },
2268
+ dividerColor: {
2269
+ type: ControlType.Color,
2270
+ title: "Divider Color",
2271
+ defaultValue: "#FFFFFF",
2272
+ },
2273
+ dividerWidth: {
2274
+ type: ControlType.Number,
2275
+ title: "Divider Width",
2276
+ defaultValue: 2,
2277
+ min: 1,
2278
+ max: 20,
2279
+ step: 1,
2280
+ unit: "px",
2281
+ },
2282
+ dividerShadow: {
2283
+ type: ControlType.Boolean,
2284
+ title: "Divider Shadow",
2285
+ defaultValue: true,
2286
+ enabledTitle: "On",
2287
+ disabledTitle: "Off",
2288
+ },
2289
+ showHandle: {
2290
+ type: ControlType.Boolean,
2291
+ title: "Show Handle",
2292
+ defaultValue: false,
2293
+ enabledTitle: "Show",
2294
+ disabledTitle: "Hide",
2295
+ },
2296
+ handleColor: {
2297
+ type: ControlType.Color,
2298
+ title: "Handle Color",
2299
+ defaultValue: "#FFFFFF",
2300
+ hidden: ({ showHandle }) => !showHandle,
2301
+ },
2302
+ handleSize: {
2303
+ type: ControlType.Number,
2304
+ title: "Handle Size",
2305
+ defaultValue: 40,
2306
+ min: 20,
2307
+ max: 80,
2308
+ step: 1,
2309
+ unit: "px",
2310
+ hidden: ({ showHandle }) => !showHandle,
2311
+ },
2312
+ });
2313
+ ```
2314
+
2315
+ ## Notes (Sticky Note)
2316
+
2317
+ Colorful sticky note with font options.
2318
+
2319
+ ```tsx
2320
+ import { type MouseEventHandler, type CSSProperties, useMemo } from "react";
2321
+ import { addPropertyControls, ControlType, RenderTarget, Color } from "framer";
2322
+
2323
+ const colors = {
2324
+ blue: "#0099FF",
2325
+ darkBlue: "#0066FF",
2326
+ purple: "#8855FF",
2327
+ red: "#FF5588",
2328
+ green: "#22CC66",
2329
+ yellow: "#FFBB00",
2330
+ };
2331
+
2332
+ interface NotesProps {
2333
+ note: string;
2334
+ shadow: boolean;
2335
+ color: string;
2336
+ preview: boolean;
2337
+ alignment: "left" | "center" | "right";
2338
+ smallFont: boolean;
2339
+ onClick?: MouseEventHandler<HTMLDivElement>;
2340
+ onMouseEnter?: MouseEventHandler<HTMLDivElement>;
2341
+ onMouseLeave?: MouseEventHandler<HTMLDivElement>;
2342
+ onMouseDown?: MouseEventHandler<HTMLDivElement>;
2343
+ onMouseUp?: MouseEventHandler<HTMLDivElement>;
2344
+ useScriptFont: boolean;
2345
+ font: CSSProperties;
2346
+ }
2347
+
2348
+ /**
2349
+ * STICKY
2350
+ *
2351
+ * @framerIntrinsicWidth 150
2352
+ * @framerIntrinsicHeight 150
2353
+ *
2354
+ * @framerSupportedLayoutWidth any-prefer-fixed
2355
+ * @framerSupportedLayoutHeight any-prefer-fixed
2356
+ */
2357
+ export default function Notes(props: NotesProps) {
2358
+ const {
2359
+ note = "",
2360
+ shadow,
2361
+ color,
2362
+ preview,
2363
+ alignment,
2364
+ smallFont,
2365
+ onClick,
2366
+ onMouseEnter,
2367
+ onMouseLeave,
2368
+ onMouseDown,
2369
+ onMouseUp,
2370
+ useScriptFont,
2371
+ font,
2372
+ } = props;
2373
+
2374
+ const [baseColorString, backgroundColorString] = useMemo(() => {
2375
+ const baseColor = Color(colors[color]);
2376
+ const hslColor = Color.toHsl(baseColor);
2377
+ hslColor.l = 0.95;
2378
+
2379
+ const baseColorString = Color(colors[color]).toValue();
2380
+ const backgroundColorString = Color(hslColor).toValue();
2381
+
2382
+ return [baseColorString, backgroundColorString];
2383
+ }, [color]);
2384
+
2385
+ const centerAligned = alignment === "center";
2386
+ const hasContent = note.length > 0;
2387
+
2388
+ return (
2389
+ <div
2390
+ style={{
2391
+ flex: 1,
2392
+ width: "100%",
2393
+ height: "100%",
2394
+ display: "flex",
2395
+ alignItems: centerAligned ? "center" : "flex-start",
2396
+ backgroundColor: backgroundColorString,
2397
+ overflow: "hidden",
2398
+ paddingLeft: smallFont ? 15 : 18,
2399
+ paddingTop: useScriptFont ? 12 : 14,
2400
+ paddingBottom: useScriptFont ? 12 : 14,
2401
+ paddingRight: smallFont ? 15 : 18,
2402
+ borderRadius: 8,
2403
+ visibility:
2404
+ RenderTarget.current() === RenderTarget.preview && !preview
2405
+ ? "hidden"
2406
+ : "visible",
2407
+ ...(useScriptFont ? { fontFamily: "Nanum Pen Script" } : font),
2408
+ //@ts-ignore
2409
+ fontDisplay: "fallback",
2410
+ boxShadow: shadow ? "0 4px 10px rgba(0,0,0,0.08)" : "none",
2411
+ }}
2412
+ {...{ onClick, onMouseEnter, onMouseLeave, onMouseDown, onMouseUp }}
2413
+ >
2414
+ {useScriptFont && (
2415
+ <link
2416
+ href="https://fonts.googleapis.com/css?family=Nanum+Pen+Script&display=swap"
2417
+ rel="stylesheet"
2418
+ />
2419
+ )}
2420
+ <p
2421
+ style={{
2422
+ width: "max-content",
2423
+ wordBreak: "break-word",
2424
+ overflowWrap: "break-word",
2425
+ overflow: "hidden",
2426
+ whiteSpace: "pre-wrap",
2427
+ margin: 0,
2428
+ fontSize: smallFont
2429
+ ? useScriptFont
2430
+ ? 18
2431
+ : 12
2432
+ : useScriptFont
2433
+ ? 32
2434
+ : 24,
2435
+
2436
+ lineHeight: smallFont
2437
+ ? useScriptFont
2438
+ ? 1.15
2439
+ : 1.4
2440
+ : useScriptFont
2441
+ ? 1.08
2442
+ : 1.3,
2443
+ textAlign: alignment,
2444
+ color: baseColorString,
2445
+ display: "-webkit-box",
2446
+ opacity: hasContent ? 1 : 0.5,
2447
+ WebkitBoxOrient: "vertical",
2448
+ }}
2449
+ >
2450
+ {hasContent ? note : "Write something..."}
2451
+ </p>
2452
+ </div>
2453
+ );
2454
+ }
2455
+
2456
+ addPropertyControls(Notes, {
2457
+ note: {
2458
+ type: ControlType.String,
2459
+ displayTextArea: true,
2460
+ placeholder: `Write something… \n\n\n`,
2461
+ },
2462
+ color: {
2463
+ type: ControlType.Enum,
2464
+ defaultValue: "blue",
2465
+ options: Object.keys(colors),
2466
+ optionTitles: Object.keys(colors).map((c) =>
2467
+ c.replace(/^\w/, (c) => c.toUpperCase()),
2468
+ ),
2469
+ },
2470
+
2471
+ alignment: {
2472
+ title: "Text Align",
2473
+ type: ControlType.Enum,
2474
+ displaySegmentedControl: true,
2475
+ optionTitles: ["Left", "Center", "Right"],
2476
+ options: ["left", "center", "right"],
2477
+ },
2478
+ useScriptFont: {
2479
+ type: ControlType.Boolean,
2480
+ disabledTitle: "Custom",
2481
+ enabledTitle: "Script",
2482
+ title: "Font",
2483
+ defaultTitle: true,
2484
+ },
2485
+ font: {
2486
+ type: ControlType.Font,
2487
+ defaultFontType: "sans-serif",
2488
+ controls: "extended",
2489
+ hidden: ({ useScriptFont }) => useScriptFont,
2490
+ },
2491
+ smallFont: {
2492
+ type: ControlType.Boolean,
2493
+ disabledTitle: "Big",
2494
+ enabledTitle: "Small",
2495
+ title: "Text Size",
2496
+ defaultValue: true,
2497
+ },
2498
+ preview: {
2499
+ type: ControlType.Boolean,
2500
+ defaultValue: true,
2501
+ title: "In Preview",
2502
+ enabledTitle: "Show",
2503
+ disabledTitle: "Hide",
2504
+ },
2505
+ shadow: {
2506
+ type: ControlType.Boolean,
2507
+ defaultValue: false,
2508
+ title: "Shadow",
2509
+ enabledTitle: "Show",
2510
+ disabledTitle: "Hide",
2511
+ },
2512
+ });
2513
+
2514
+ Notes.displayName = "Sticky Note";
2515
+ ```
2516
+
2517
+ ## Key Patterns Demonstrated
2518
+
2519
+ 1. **SSR Safety**: `if (typeof window !== "undefined")` guards
2520
+ 2. **State Transitions**: `setState` wrapped in `startTransition()` for smooth interactions
2521
+ 3. **Static Renderer**: `useIsStaticRenderer()` to skip animations on canvas
2522
+ 4. **Image Defaults**: Set in destructuring, not in property controls
2523
+ 5. **Font Controls**: `controls: "extended"` with `defaultFontType: "sans-serif"` for full typography customization
2524
+ 6. **Conditional Controls**: `hidden: (props) => !props.showFeature`
2525
+ 7. **Accessibility**: `role`, `aria-*`, semantic HTML, keyboard support
2526
+ 8. **Color Utilities**: Using `Color` from framer for color manipulation