framer-code-components-overrides by fredm00n/framerlabs
npx skills add https://github.com/fredm00n/framerlabs --skill framer-code-components-overrides代码组件:添加到画布的自定义 React 组件。支持 addPropertyControls。
代码覆盖:包装现有画布元素的高阶组件。不支持 addPropertyControls。
始终至少包含:
/**
* @framerDisableUnlink
* @framerIntrinsicWidth 100
* @framerIntrinsicHeight 100
*/
完整集合:
@framerDisableUnlink — 防止修改时取消链接@framerIntrinsicWidth / @framerIntrinsicHeight — 默认尺寸@framerSupportedLayoutWidth / — 、、、广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
@framerSupportedLayoutHeightanyautofixedany-prefer-fixedimport type { ComponentType } from "react"
import { useState, useEffect } from "react"
/**
* @framerDisableUnlink
*/
export function withFeatureName(Component): ComponentType {
return (props) => {
// 状态和逻辑写在这里
return <Component {...props} />
}
}
命名:始终使用 withFeatureName 前缀。
import { motion } from "framer-motion"
import { addPropertyControls, ControlType } from "framer"
/**
* @framerDisableUnlink
* @framerIntrinsicWidth 300
* @framerIntrinsicHeight 200
*/
export default function MyComponent(props) {
const { style } = props
return <motion.div style={{ ...style }}>{/* 内容 */}</motion.div>
}
MyComponent.defaultProps = {
// 始终定义默认值
}
addPropertyControls(MyComponent, {
// 控件定义在这里
})
切勿单独访问字体属性。始终展开整个字体对象。
// ❌ 错误 - 无法工作
style={{
fontFamily: props.font.fontFamily,
fontSize: props.font.fontSize,
}}
// ✅ 正确 - 展开整个对象
style={{
...props.font,
}}
字体控件定义:
font: {
type: ControlType.Font,
controls: "extended",
defaultValue: {
fontFamily: "Inter",
fontWeight: 500,
fontSize: 16,
lineHeight: "1.5em",
},
}
Framer 中的所有 React 状态更新都必须包装在 startTransition() 中:
import { startTransition } from "react"
// ❌ 错误 - 可能导致 Framer 渲染管道问题
setCount(count + 1)
// ✅ 正确 - 始终包装状态更新
startTransition(() => {
setCount(count + 1)
})
这是 Framer 特有的要求,可防止并发渲染的性能问题。
Framer 在服务器端预渲染。SSR 期间浏览器 API 不可用。
两阶段渲染模式:
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (!isClient) {
return <Component {...props} /> // SSR 安全的回退
}
// 仅客户端的逻辑写在这里
切勿在渲染时直接访问:
window、document、navigatorlocalStorage、sessionStoragewindow.innerWidth、window.innerHeightimport { RenderTarget } from "framer"
const isOnCanvas = RenderTarget.current() === RenderTarget.canvas
// 仅在编辑器中显示调试
{isOnCanvas && <DebugOverlay />}
用于:
完整控件类型和模式请参阅 references/property-controls.md。
实现方法请参阅 references/patterns.md:共享状态、键盘检测、仅显示一次逻辑、滚动效果、磁性悬停、动画触发器。
无法从 props 读取变体名称(可能被哈希处理)。在内部管理:
export function withVariantControl(Component): ComponentType {
return (props) => {
const [currentVariant, setCurrentVariant] = useState("variant-1")
// 更改变体的逻辑
setCurrentVariant("variant-2")
return <Component {...props} variant={currentVariant} />
}
}
Framer 的滚动检测使用基于视口的 IntersectionObserver。对容器应用 overflow: scroll 会破坏此检测。
对于滚动触发的动画,请使用:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasEntered) {
setHasEntered(true)
}
})
},
{ threshold: 0.1 }
)
着色器实现模式(包括透明度处理)请参阅 references/webgl-shaders.md。
标准导入(推荐):
import { Component } from "package-name"
当 Framer 缓存卡住时,通过 CDN 强制使用特定版本:
import { Component } from "https://esm.sh/package-name@1.2.3?external=react,react-dom"
对于 React 组件,始终包含 ?external=react,react-dom。
Chrome/Firefox 不原生支持 HLS 流。普通的 <video src="...m3u8"> 要么会失败,要么会永久播放最低质量的版本。Safari 原生处理 HLS。
修复方法: 通过动态导入使用 HLS.js,并提供静默回退:
let HlsModule = null
let hlsImportAttempted = false
async function loadHls() {
if (hlsImportAttempted) return HlsModule
hlsImportAttempted = true
try {
const mod = await import("https://esm.sh/hls.js@1?external=react,react-dom")
HlsModule = mod.default || mod
} catch {
HlsModule = null // 回退到原生视频
}
return HlsModule
}
function attachHls(videoEl, src) {
if (typeof window === "undefined") return null // SSR 防护
const Hls = HlsModule
if (src.includes(".m3u8") && Hls?.isSupported()) {
const hls = new Hls({ startLevel: -1, capLevelToPlayerSize: true })
hls.loadSource(src)
hls.attachMedia(videoEl)
hls.on(Hls.Events.MANIFEST_PARSED, () => videoEl.play().catch(() => {}))
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
data.type === Hls.ErrorTypes.NETWORK_ERROR
? hls.startLoad()
: hls.destroy()
}
})
return hls
}
videoEl.src = src // MP4/webm 或 Safari 原生 HLS
videoEl.play().catch(() => {})
return null
}
关键点:
capLevelToPlayerSize: true 防止为 400px 的播放器加载 4K 视频cancelled 标志以防止快速导航后附着陈旧实例| 问题 | 原因 | 修复方法 |
|---|---|---|
| 字体样式未应用 | 单独访问字体属性 | 展开整个字体对象:...props.font |
| 水合不匹配 | 在渲染中使用浏览器 API | 使用 isClient 状态模式 |
| 覆盖 props 未定义 | 期望属性控件 | 覆盖不支持 addPropertyControls |
| 滚动动画损坏 | 容器上使用 overflow: scroll | 在视口上使用 IntersectionObserver |
| 着色器附着错误 | 编译失败返回空着色器 | 在 attachShader() 之前检查 createShader() 的返回值 |
| 组件显示名称 | 在 Framer UI 中需要自定义名称 | Component.displayName = "Name" |
TypeScript Timeout 错误 | 使用 NodeJS.Timeout 类型 | 改用 number — 浏览器环境 |
| 覆盖层卡在内容下方 | 父级的堆叠上下文 | 使用 React Portal 在 document.body 级别渲染 |
| 所有曲线缓动感觉相同 | 未跟踪初始距离 | 目标更改时跟踪 initialDiff 以进行进度计算 |
| HLS 视频永久像素化 | Chrome 中未使用 HLS.js 的 .m3u8 | 使用 HLS.js 动态导入模式(见上方 HLS 部分) |
对于粒子系统和繁重动画:
touchAction: "none" 防止滚动干扰CMS 内容在水合后异步加载。处理顺序:
处理 CMS 数据前添加延迟:
useEffect(() => {
if (isClient && props.children) {
const timer = setTimeout(() => {
processContent(props.children)
}, 100)
return () => clearTimeout(timer)
}
}, [isClient, props.children])
Framer 文本使用深层嵌套结构。递归处理:
const processChildren = (children) => {
if (typeof children === "string") {
return processText(children)
}
if (isValidElement(children)) {
return cloneElement(children, {
...children.props,
children: processChildren(children.props.children)
})
}
if (Array.isArray(children)) {
return children.map(child => processChildren(child))
}
return children
}
将定位与动画分离:
<motion.div
style={{
position: "absolute",
left: `${offset}px`, // 静态定位
x: animatedValue, // 动画变换
}}
/>
拆分动画阶段以实现自然运动:
// 向上:快速弹出
transition={{ duration: 0.15, ease: [0, 0, 0.39, 2.99] }}
// 向下:平滑沉降
transition={{ duration: 0.15, ease: [0.25, 0.46, 0.45, 0.94] }}
强制 GPU 加速以实现平滑的 SVG 动画:
style={{
willChange: "transform",
transform: "translateZ(0)",
backfaceVisibility: "hidden",
}}
问题: 具有 position: absolute 的组件继承其父级的堆叠上下文。即使使用 z-index: 9999,它们也无法出现在父级外部的元素之上。
解决方案: 使用 React Portal 在 document.body 级别渲染:
import { createPortal } from "react-dom"
export default function ComponentWithOverlay(props) {
const [showOverlay, setShowOverlay] = useState(false)
return (
<div style={{ position: "relative" }}>
{/* 主组件内容 */}
{/* 在父级层次结构外部渲染的覆盖层 */}
{showOverlay && createPortal(
<div style={{
position: "fixed", // 相对于视口固定
inset: 0,
zIndex: 9999,
background: "rgba(0, 0, 0, 0.8)",
}}>
{/* 覆盖层内容 */}
</div>,
document.body
)}
</div>
)
}
关键区别:
position: "fixed" 相对于视口定位,而非父级画布与已发布: Portals 在画布编辑器和已发布站点中均有效。无需 RenderTarget 检查。
模式: 显示加载覆盖层,并在内容准备好前防止页面滚动。
const [isLoading, setIsLoading] = useState(true)
const [fadeOut, setFadeOut] = useState(false)
// 加载时防止滚动(仅限已发布站点)
useEffect(() => {
const isPublished = RenderTarget.current() !== "CANVAS"
if (!isPublished || !isLoading) return
const originalOverflow = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => {
document.body.style.overflow = originalOverflow
}
}, [isLoading])
// 两阶段隐藏:淡出 → 从 DOM 移除
const hideLoader = () => {
setFadeOut(true)
setTimeout(() => setIsLoading(false), 300) // 匹配 CSS 过渡
}
加载时滚动到顶部(修复变体序列问题):
useEffect(() => {
const isPublished = RenderTarget.current() !== "CANVAS"
if (isPublished) {
window.scrollTo(0, 0)
}
}, [])
问题: 指数 lerp (value += diff * speed) 自然产生缓出效果。需要跟踪初始距离以实现其他曲线。
解决方案: 动画开始时跟踪 initialDiff:
const animated = useRef({
property: {
current: 0,
target: 0,
initialDiff: 0, // 用于缓动计算
}
})
// 目标更改时,存储初始距离
const updateTarget = (newTarget) => {
const entry = animated.current.property
entry.initialDiff = Math.abs(newTarget - entry.current)
entry.target = newTarget
}
// 在动画循环中应用缓动
const applyEasing = (easingCurve) => {
const v = animated.current.property
const diff = v.target - v.current
let speed = 0.05 // 基础速度
if (easingCurve !== "ease-out") {
// 计算进度:开始时为 0,接近目标时为 1
const diffMagnitude = Math.abs(diff)
const progress = v.initialDiff > 0
? Math.max(0, Math.min(1, 1 - (diffMagnitude / v.initialDiff)))
: 1
if (easingCurve === "ease-in") {
// 慢-快(三次方)
speed *= (0.05 + Math.pow(progress, 3) * 10)
} else if (easingCurve === "ease-in-out") {
// 慢-快-慢(更平滑)
const smoothed = progress * progress * progress *
(progress * (progress * 6 - 15) + 10)
speed *= (0.2 + smoothed * 3)
}
}
// ease-out:使用默认的指数衰减
v.current += diff * speed
}
为什么需要激进的曲线? 指数 lerp 在接近目标时自然减速。为了产生明显的缓入效果,需要极端的乘数(0.05x → 10x)来克服自然衰减。
属性控件:
easingCurve: {
type: ControlType.Enum,
title: "缓动曲线",
options: ["ease-out", "ease-in", "ease-in-out"],
optionTitles: ["缓出", "缓入", "缓入缓出"],
defaultValue: "ease-out",
}
每周安装次数
137
仓库
GitHub 星标数
120
首次出现
2026年1月30日
安全审计
安装于
opencode111
codex108
gemini-cli103
claude-code98
cursor96
github-copilot93
Code Components : Custom React components added to canvas. Support addPropertyControls.
Code Overrides : Higher-order components wrapping existing canvas elements. Do NOT support addPropertyControls.
Always include at minimum:
/**
* @framerDisableUnlink
* @framerIntrinsicWidth 100
* @framerIntrinsicHeight 100
*/
Full set:
@framerDisableUnlink — Prevents unlinking when modified@framerIntrinsicWidth / @framerIntrinsicHeight — Default dimensions@framerSupportedLayoutWidth / @framerSupportedLayoutHeight — any, auto, fixed, any-prefer-fixedimport type { ComponentType } from "react"
import { useState, useEffect } from "react"
/**
* @framerDisableUnlink
*/
export function withFeatureName(Component): ComponentType {
return (props) => {
// State and logic here
return <Component {...props} />
}
}
Naming: Always use withFeatureName prefix.
import { motion } from "framer-motion"
import { addPropertyControls, ControlType } from "framer"
/**
* @framerDisableUnlink
* @framerIntrinsicWidth 300
* @framerIntrinsicHeight 200
*/
export default function MyComponent(props) {
const { style } = props
return <motion.div style={{ ...style }}>{/* content */}</motion.div>
}
MyComponent.defaultProps = {
// Always define defaults
}
addPropertyControls(MyComponent, {
// Controls here
})
Never access font properties individually. Always spread the entire font object.
// ❌ BROKEN - Will not work
style={{
fontFamily: props.font.fontFamily,
fontSize: props.font.fontSize,
}}
// ✅ CORRECT - Spread entire object
style={{
...props.font,
}}
Font control definition:
font: {
type: ControlType.Font,
controls: "extended",
defaultValue: {
fontFamily: "Inter",
fontWeight: 500,
fontSize: 16,
lineHeight: "1.5em",
},
}
All React state updates in Framer must be wrapped in startTransition():
import { startTransition } from "react"
// ❌ WRONG - May cause issues in Framer's rendering pipeline
setCount(count + 1)
// ✅ CORRECT - Always wrap state updates
startTransition(() => {
setCount(count + 1)
})
This is Framer-specific and prevents performance issues with concurrent rendering.
Framer pre-renders on server. Browser APIs unavailable during SSR.
Two-phase rendering pattern:
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (!isClient) {
return <Component {...props} /> // SSR-safe fallback
}
// Client-only logic here
Never access directly at render time:
window, document, navigatorlocalStorage, sessionStoragewindow.innerWidth, window.innerHeightimport { RenderTarget } from "framer"
const isOnCanvas = RenderTarget.current() === RenderTarget.canvas
// Show debug only in editor
{isOnCanvas && <DebugOverlay />}
Use for:
See references/property-controls.md for complete control types and patterns.
See references/patterns.md for implementations: shared state, keyboard detection, show-once logic, scroll effects, magnetic hover, animation triggers.
Cannot read variant names from props (may be hashed). Manage internally:
export function withVariantControl(Component): ComponentType {
return (props) => {
const [currentVariant, setCurrentVariant] = useState("variant-1")
// Logic to change variant
setCurrentVariant("variant-2")
return <Component {...props} variant={currentVariant} />
}
}
Framer's scroll detection uses viewport-based IntersectionObserver. Applying overflow: scroll to containers breaks this detection.
For scroll-triggered animations, use:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasEntered) {
setHasEntered(true)
}
})
},
{ threshold: 0.1 }
)
See references/webgl-shaders.md for shader implementation patterns including transparency handling.
Standard import (preferred):
import { Component } from "package-name"
Force specific version via CDN when Framer cache is stuck:
import { Component } from "https://esm.sh/package-name@1.2.3?external=react,react-dom"
Always include ?external=react,react-dom for React components.
Chrome/Firefox do not natively support HLS streams. A plain <video src="...m3u8"> will either fail or play the lowest quality rendition permanently. Safari handles HLS natively.
Fix: Use HLS.js via dynamic import with silent fallback:
let HlsModule = null
let hlsImportAttempted = false
async function loadHls() {
if (hlsImportAttempted) return HlsModule
hlsImportAttempted = true
try {
const mod = await import("https://esm.sh/hls.js@1?external=react,react-dom")
HlsModule = mod.default || mod
} catch {
HlsModule = null // Fallback to native video
}
return HlsModule
}
function attachHls(videoEl, src) {
if (typeof window === "undefined") return null // SSR guard
const Hls = HlsModule
if (src.includes(".m3u8") && Hls?.isSupported()) {
const hls = new Hls({ startLevel: -1, capLevelToPlayerSize: true })
hls.loadSource(src)
hls.attachMedia(videoEl)
hls.on(Hls.Events.MANIFEST_PARSED, () => videoEl.play().catch(() => {}))
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
data.type === Hls.ErrorTypes.NETWORK_ERROR
? hls.startLoad()
: hls.destroy()
}
})
return hls
}
videoEl.src = src // MP4/webm or Safari native HLS
videoEl.play().catch(() => {})
return null
}
Key points:
capLevelToPlayerSize: true prevents loading 4K for a 400px playercancelled flag in effects to prevent stale attachment after fast navigation| Issue | Cause | Fix |
|---|---|---|
| Font styles not applying | Accessing font props individually | Spread entire font object: ...props.font |
| Hydration mismatch | Browser API in render | Use isClient state pattern |
| Override props undefined | Expecting property controls | Overrides don't support addPropertyControls |
| Scroll animation broken | overflow: scroll on container | Use IntersectionObserver on viewport |
| Shader attach error | Null shader from compilation failure | Check return before |
For particle systems and heavy animations:
touchAction: "none" to prevent scroll interferenceCMS content loads asynchronously after hydration. Processing sequence:
Add delay before processing CMS data:
useEffect(() => {
if (isClient && props.children) {
const timer = setTimeout(() => {
processContent(props.children)
}, 100)
return () => clearTimeout(timer)
}
}, [isClient, props.children])
Framer text uses deeply nested structure. Process recursively:
const processChildren = (children) => {
if (typeof children === "string") {
return processText(children)
}
if (isValidElement(children)) {
return cloneElement(children, {
...children.props,
children: processChildren(children.props.children)
})
}
if (Array.isArray(children)) {
return children.map(child => processChildren(child))
}
return children
}
Separate positioning from animation:
<motion.div
style={{
position: "absolute",
left: `${offset}px`, // Static positioning
x: animatedValue, // Animation transform
}}
/>
Split animation phases for natural motion:
// Up: snappy pop
transition={{ duration: 0.15, ease: [0, 0, 0.39, 2.99] }}
// Down: smooth settle
transition={{ duration: 0.15, ease: [0.25, 0.46, 0.45, 0.94] }}
Force GPU acceleration for smooth SVG animations:
style={{
willChange: "transform",
transform: "translateZ(0)",
backfaceVisibility: "hidden",
}}
Problem: Components with position: absolute inherit their parent's stacking context. Even with z-index: 9999, they can't appear above elements outside the parent.
Solution: Use React Portal to render at document.body level:
import { createPortal } from "react-dom"
export default function ComponentWithOverlay(props) {
const [showOverlay, setShowOverlay] = useState(false)
return (
<div style={{ position: "relative" }}>
{/* Main component content */}
{/* Overlay rendered outside parent hierarchy */}
{showOverlay && createPortal(
<div style={{
position: "fixed", // Fixed to viewport
inset: 0,
zIndex: 9999,
background: "rgba(0, 0, 0, 0.8)",
}}>
{/* Overlay content */}
</div>,
document.body
)}
</div>
)
}
Key differences:
position: "fixed" positions relative to viewport, not parentCanvas vs Published: Portals work in both canvas editor and published site. No RenderTarget check needed.
Pattern: Show loading overlay and prevent page scroll until content is ready.
const [isLoading, setIsLoading] = useState(true)
const [fadeOut, setFadeOut] = useState(false)
// Prevent scroll while loading (published site only)
useEffect(() => {
const isPublished = RenderTarget.current() !== "CANVAS"
if (!isPublished || !isLoading) return
const originalOverflow = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => {
document.body.style.overflow = originalOverflow
}
}, [isLoading])
// Two-phase hide: fade-out → remove from DOM
const hideLoader = () => {
setFadeOut(true)
setTimeout(() => setIsLoading(false), 300) // Match CSS transition
}
Scroll to top on load (fixes variant sequence issues):
useEffect(() => {
const isPublished = RenderTarget.current() !== "CANVAS"
if (isPublished) {
window.scrollTo(0, 0)
}
}, [])
Problem: Exponential lerp (value += diff * speed) naturally gives ease-out. Need to track initial distance to implement other curves.
Solution: Track initialDiff when animation starts:
const animated = useRef({
property: {
current: 0,
target: 0,
initialDiff: 0, // Track for easing calculations
}
})
// When target changes, store initial distance
const updateTarget = (newTarget) => {
const entry = animated.current.property
entry.initialDiff = Math.abs(newTarget - entry.current)
entry.target = newTarget
}
// Apply easing in animation loop
const applyEasing = (easingCurve) => {
const v = animated.current.property
const diff = v.target - v.current
let speed = 0.05 // Base speed
if (easingCurve !== "ease-out") {
// Calculate progress: 0 at start, 1 near target
const diffMagnitude = Math.abs(diff)
const progress = v.initialDiff > 0
? Math.max(0, Math.min(1, 1 - (diffMagnitude / v.initialDiff)))
: 1
if (easingCurve === "ease-in") {
// Start slow, end fast (cubic)
speed *= (0.05 + Math.pow(progress, 3) * 10)
} else if (easingCurve === "ease-in-out") {
// Slow-fast-slow (smootherstep)
const smoothed = progress * progress * progress *
(progress * (progress * 6 - 15) + 10)
speed *= (0.2 + smoothed * 3)
}
}
// ease-out: use default exponential decay
v.current += diff * speed
}
Why aggressive curves? Exponential lerp naturally slows down approaching target. To create noticeable ease-in, need extreme multipliers (0.05x → 10x) to overcome the natural decay.
Property control:
easingCurve: {
type: ControlType.Enum,
title: "Easing Curve",
options: ["ease-out", "ease-in", "ease-in-out"],
optionTitles: ["Ease Out", "Ease In", "Ease In/Out"],
defaultValue: "ease-out",
}
Weekly Installs
137
Repository
GitHub Stars
120
First Seen
Jan 30, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode111
codex108
gemini-cli103
claude-code98
cursor96
github-copilot93
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
113,700 周安装
ASP.NET Core依赖注入模式:扩展方法组织服务注册,提升代码可维护性
136 周安装
Git Hooks 设置指南:Husky 配置、代码质量检查与自动化提交验证
136 周安装
压力测试指南:使用k6和JMeter进行系统极限与崩溃点测试
136 周安装
agent-browser 浏览器自动化测试工具 - 基于引用的 AI 友好型端到端测试 CLI
136 周安装
App Store Connect 参考指南:崩溃分析、TestFlight反馈与性能指标导出
136 周安装
iOS Apple Intelligence 路由器使用指南 - Foundation Models 与 AI 方法分流
136 周安装
createShader()attachShader()| Component display name | Need custom name in Framer UI | Component.displayName = "Name" |
TypeScript Timeout errors | Using NodeJS.Timeout type | Use number instead — browser environment |
| Overlay stuck under content | Stacking context from parent | Use React Portal to render at document.body level |
| Easing feels same for all curves | Not tracking initial distance | Track initialDiff when target changes for progress calculation |
| HLS video permanently pixelated | .m3u8 in Chrome without HLS.js | Use HLS.js dynamic import pattern (see HLS section above) |