Custom Framer Component · Free, unlicensed
Testimonials Mobile Carousel
A swipeable, mobile-first testimonials carousel with glass-morphism styling options, auto-rotate, and an avatar/dots selector.
testimonialscarouselmobilereviews
This is a standalone Framer Code File — not installed with the CLI, not gated behind a subscription. Copy the whole file and paste it into Framer via Assets → Code → Create Code File.
Property controls
These are Framer's own addPropertyControls fields,
which show up as the right-hand properties panel once this is pasted into Framer. They are
not React props and have nothing to do with this site's licensed component API.
| Panel label | Control key | Control type |
|---|---|---|
| Testimonials | testimonials | Array |
| Quote Font | quoteFont | Font |
| Quote Color | quoteColor | Color |
| Background | backgroundColor | Color |
| Background Blur | backgroundBlur | Number |
| Active Pill Color | activeColor | Color |
| Hover Color | hoverColor | Color |
| Role Color | roleColor | Color |
| Show Quotes | showQuotationMarks | Boolean |
| Mark Color | quoteMarkColor | Color |
| Mark Size | quoteMarkSize | Number |
| Mark Opacity | quotationMarkOpacity | Number |
| Animation Speed | animationDuration | Number |
| Gap | gap | Number |
| Avatar Size | avatarSize | Number |
| Border Radius | borderRadius | Number |
| Mobile Breakpoint | // Mobile-specific controls mobileBreakpoint | Number |
| Mobile Font Size | mobileFontSize | String |
| Mobile Padding | mobilePadding | Number |
| Mobile Avatar Size | mobileAvatarSize | Number |
| Mobile Quote Size | mobileQuoteMarkSize | Number |
| Swipe Gestures | enableSwipeGestures | Boolean |
| Auto Rotate (Mobile) | autoRotate | Boolean |
| Rotate Interval | autoRotateInterval | Number |
| Mobile Max Width | mobileMaxWidth | String |
| Mobile Gap | mobileGap | Number |
| Show Dots (Mobile) | showDotsOnMobile | Boolean |
| Dot Size | mobileButtonSize | Number |
| Glass Effect | // Glass morphism controls (simplified grouping) enableGlassMorphism | Boolean |
| Glass Opacity | glassOpacity | Number |
| Glass Blur | glassBlur | Number |
| Iridescence | enableIridescence | Boolean |
| Iridescent Intensity | iridescentIntensity | Number |
| Iridescent Speed | iridescentSpeed | Number |
| Noise Texture | enableNoise | Boolean |
| Noise Opacity | noiseOpacity | Number |
| Specular Highlight | enableSpecularHighlight | Boolean |
| Highlight Opacity | specularOpacity | Number |
| Edge Glow | enableEdgeGlow | Boolean |
| Glow Color | edgeGlowColor | Color |
| Glow Intensity | edgeGlowIntensity | Number |
| Refraction Distortion | enableRefraction | Boolean |
| Refraction Intensity | refractionIntensity | Number |
| Refraction Blur | refractionBlur | Number |
Component source
The complete, unmodified file — copy the whole thing.
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { addPropertyControls, ControlType, RenderTarget } from "framer"
export const META = {
slug: "testimonials-mobile-carousel",
title: "Testimonials Mobile Carousel",
description: "A swipeable, mobile-first testimonials carousel with glass-morphism styling options, auto-rotate, and an avatar/dots selector.",
tags: ["testimonials", "carousel", "mobile", "reviews"],
category: "custom-framer",
}
interface TestimonialItem {
name: string
location: string
rating: number
service: string
date: string
review: string
avatar?: string
}
interface Props {
testimonials: TestimonialItem[]
quoteFont: {
fontSize: string
fontWeight: number
fontFamily?: string
fontStyle?: string
letterSpacing?: string
lineHeight?: string | number
}
quoteColor: string
backgroundColor: string
activeColor: string
hoverColor: string
roleColor: string
// Quotation Mark Props
showQuotationMarks: boolean
quoteMarkColor: string
quoteMarkSize: number
quotationMarkOpacity: number
animationDuration: number
gap: number
avatarSize: number
backgroundBlur: number
borderRadius: number
// Glass morphism props
enableGlassMorphism: boolean
glassOpacity: number
glassBlur: number
enableIridescence: boolean
iridescentIntensity: number
iridescentSpeed: number
enableNoise: boolean
noiseOpacity: number
enableSpecularHighlight: boolean
specularOpacity: number
enableEdgeGlow: boolean
edgeGlowColor: string
edgeGlowIntensity: number
enableRefraction: boolean
refractionIntensity: number
refractionBlur: number
// Mobile-specific props
mobileBreakpoint: number
mobileFontSize: string
mobilePadding: number
mobileAvatarSize: number
mobileQuoteMarkSize: number
enableSwipeGestures: boolean
autoRotate: boolean
autoRotateInterval: number
mobileMaxWidth: string
mobileGap: number
showDotsOnMobile: boolean
mobileButtonSize: number
}
export default function TestimonialsMobileCarousel(props: Props) {
const {
testimonials,
quoteFont,
quoteColor,
backgroundColor,
activeColor,
hoverColor,
roleColor,
showQuotationMarks,
quoteMarkColor,
quoteMarkSize,
quotationMarkOpacity,
animationDuration,
gap,
avatarSize,
backgroundBlur,
borderRadius,
enableGlassMorphism,
glassOpacity,
glassBlur,
enableIridescence,
iridescentIntensity,
iridescentSpeed,
enableNoise,
noiseOpacity,
enableSpecularHighlight,
specularOpacity,
enableEdgeGlow,
edgeGlowColor,
edgeGlowIntensity,
enableRefraction,
refractionIntensity,
refractionBlur,
mobileBreakpoint,
mobileFontSize,
mobilePadding,
mobileAvatarSize,
mobileQuoteMarkSize,
enableSwipeGestures,
autoRotate,
autoRotateInterval,
mobileMaxWidth,
mobileGap,
showDotsOnMobile,
mobileButtonSize,
} = props
const [activeIndex, setActiveIndex] = useState(0)
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
const [isMobile, setIsMobile] = useState(false)
const [touchStart, setTouchStart] = useState<number | null>(null)
const [touchEnd, setTouchEnd] = useState<number | null>(null)
const isCanvas = RenderTarget.current() === RenderTarget.canvas
// Check for mobile viewport
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= mobileBreakpoint)
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [mobileBreakpoint])
// Auto-rotate functionality
useEffect(() => {
if (!autoRotate || !isMobile) return
const interval = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % testimonials.length)
}, autoRotateInterval * 1000)
return () => clearInterval(interval)
}, [autoRotate, autoRotateInterval, isMobile, testimonials.length])
// Touch handling for swipe gestures
const handleTouchStart = (e: React.TouchEvent) => {
if (!enableSwipeGestures || !isMobile) return
setTouchEnd(null)
setTouchStart(e.targetTouches[0].clientX)
}
const handleTouchMove = (e: React.TouchEvent) => {
if (!enableSwipeGestures || !isMobile) return
setTouchEnd(e.targetTouches[0].clientX)
}
const handleTouchEnd = () => {
if (!touchStart || !touchEnd || !enableSwipeGestures || !isMobile) return
const distance = touchStart - touchEnd
const isLeftSwipe = distance > 50
const isRightSwipe = distance < -50
if (isLeftSwipe && activeIndex < testimonials.length - 1) {
setActiveIndex(activeIndex + 1)
}
if (isRightSwipe && activeIndex > 0) {
setActiveIndex(activeIndex - 1)
}
}
const handleSelect = (index: number) => {
if (index !== activeIndex) {
setActiveIndex(index)
}
}
const activeTestimonial = testimonials[activeIndex]
// Responsive values
const currentPadding = isMobile ? mobilePadding : 32
const currentAvatarSize = isMobile ? mobileAvatarSize : avatarSize
const currentQuoteMarkSize = isMobile ? mobileQuoteMarkSize : quoteMarkSize
const currentGap = isMobile ? mobileGap : gap
const currentFontSize = isMobile ? mobileFontSize : quoteFont.fontSize
const currentMaxWidth = isMobile ? mobileMaxWidth : "512px"
return (
<div
style={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: enableGlassMorphism
? `rgba(255, 255, 255, ${glassOpacity})`
: backgroundColor,
backdropFilter: enableGlassMorphism
? `blur(${glassBlur}px)`
: `blur(${backgroundBlur}px)`,
WebkitBackdropFilter: enableGlassMorphism
? `blur(${glassBlur}px)`
: `blur(${backgroundBlur}px)`,
borderRadius: `${borderRadius}px`,
padding: `${currentPadding}px`,
overflow: "hidden",
boxShadow: enableEdgeGlow
? `inset 0 0 ${edgeGlowIntensity}px ${edgeGlowColor}`
: "none",
touchAction: enableSwipeGestures && isMobile ? "pan-y" : "auto",
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{/* Glass morphism effects - simplified for mobile performance */}
{enableGlassMorphism && (!isMobile || window.devicePixelRatio <= 2) && (
<>
{/* Noise Texture Overlay */}
{enableNoise && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: noiseOpacity,
borderRadius: `${borderRadius}px`,
background: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' version='1.1' viewBox='0 0 100 100'%3e%3cdefs%3e%3cfilter id='noiseFilter'%3e%3cfeTurbulence baseFrequency='0.9' numOctaves='4' result='noise'/%3e%3cfeColorMatrix in='noise' type='saturate' values='0'/%3e%3c/filter%3e%3c/defs%3e%3crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3e%3c/svg%3e")`,
mixBlendMode: "overlay",
pointerEvents: "none",
}}
/>
)}
{/* Simplified iridescent effects for mobile */}
{enableIridescence && !isMobile && (
<motion.div
animate={{
background: [
`conic-gradient(from 0deg, transparent, rgba(255, 0, 150, ${iridescentIntensity}), transparent, rgba(0, 255, 255, ${iridescentIntensity}), transparent)`,
`conic-gradient(from 120deg, transparent, rgba(255, 0, 150, ${iridescentIntensity}), transparent, rgba(0, 255, 255, ${iridescentIntensity}), transparent)`,
`conic-gradient(from 240deg, transparent, rgba(255, 0, 150, ${iridescentIntensity}), transparent, rgba(0, 255, 255, ${iridescentIntensity}), transparent)`,
`conic-gradient(from 360deg, transparent, rgba(255, 0, 150, ${iridescentIntensity}), transparent, rgba(0, 255, 255, ${iridescentIntensity}), transparent)`,
],
}}
transition={{
duration: iridescentSpeed,
repeat: Infinity,
ease: "linear",
}}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: `${borderRadius}px`,
mixBlendMode: "overlay",
pointerEvents: "none",
}}
/>
)}
{/* Disable complex effects on mobile for performance */}
{!isMobile && enableSpecularHighlight && (
<motion.div
animate={{
x: ["-20%", "120%"],
y: ["-20%", "120%"],
}}
transition={{
duration: iridescentSpeed * 2,
repeat: Infinity,
ease: "linear",
}}
style={{
position: "absolute",
width: "30%",
height: "30%",
background: `radial-gradient(ellipse, rgba(255, 255, 255, ${specularOpacity}) 0%, transparent 70%)`,
borderRadius: "50%",
filter: "blur(2px)",
pointerEvents: "none",
}}
/>
)}
</>
)}
{/* Content Container */}
<div
style={{
position: "relative",
zIndex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: `${currentGap}px`,
paddingTop: isMobile ? "32px" : "64px",
paddingBottom: isMobile ? "32px" : "64px",
width: "100%",
maxWidth: "100%",
}}
>
{/* Quote Container */}
<div
style={{
position: "relative",
paddingLeft: isMobile ? "16px" : "32px",
paddingRight: isMobile ? "16px" : "32px",
width: "100%",
boxSizing: "border-box",
}}
>
{showQuotationMarks && (
<span
style={{
position: "absolute",
left: isMobile ? "0px" : `-${currentQuoteMarkSize * 0.15}px`,
top: `-${currentQuoteMarkSize * 0.35}px`,
fontSize: `${currentQuoteMarkSize}px`,
fontFamily: "Georgia, serif",
color: quoteMarkColor,
opacity: quotationMarkOpacity,
userSelect: "none",
pointerEvents: "none",
lineHeight: 1,
}}
>
"
</span>
)}
<AnimatePresence mode="wait">
<motion.p
key={activeIndex}
initial={
isCanvas
? false
: {
opacity: 0,
filter: isMobile ? "none" : "blur(4px)",
scale: 0.98,
}
}
animate={{
opacity: 1,
filter: "blur(0px)",
scale: 1,
}}
exit={{
opacity: 0,
filter: isMobile ? "none" : "blur(4px)",
scale: 0.98,
}}
transition={{
duration: animationDuration,
ease: "easeOut",
}}
style={{
fontSize: currentFontSize,
fontWeight: quoteFont.fontWeight,
fontFamily: quoteFont.fontFamily,
fontStyle: quoteFont.fontStyle,
letterSpacing: quoteFont.letterSpacing,
lineHeight: quoteFont.lineHeight || 1.6,
color: quoteColor,
textAlign: "center",
maxWidth: currentMaxWidth,
margin: "0 auto",
wordWrap: "break-word",
hyphens: "auto",
}}
>
{activeTestimonial.review}
</motion.p>
</AnimatePresence>
{showQuotationMarks && (
<span
style={{
position: "absolute",
right: isMobile ? "0px" : `-${currentQuoteMarkSize * 0.15}px`,
bottom: `-${currentQuoteMarkSize * 0.5}px`,
fontSize: `${currentQuoteMarkSize}px`,
fontFamily: "Georgia, serif",
color: quoteMarkColor,
opacity: quotationMarkOpacity,
userSelect: "none",
pointerEvents: "none",
lineHeight: 1,
}}
>
"
</span>
)}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: isMobile ? "16px" : "24px",
marginTop: "8px",
width: "100%",
}}
>
{/* Role text */}
<AnimatePresence mode="wait">
<motion.p
key={activeIndex}
initial={isCanvas ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{
duration: animationDuration * 1.25,
ease: "easeOut",
}}
style={{
fontSize: isMobile ? "10px" : "11px",
color: roleColor,
letterSpacing: "0.2em",
textTransform: "uppercase",
margin: 0,
textAlign: "center",
padding: isMobile ? "0 16px" : "0",
}}
>
{activeTestimonial.service} —{" "}
{activeTestimonial.location}
</motion.p>
</AnimatePresence>
{/* Mobile-optimized avatar selector */}
{isMobile && showDotsOnMobile ? (
// Dots indicator for mobile
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
justifyContent: "center",
padding: "8px",
}}
>
{testimonials.map((_, index) => (
<motion.button
key={index}
onClick={() => handleSelect(index)}
style={{
width: mobileButtonSize,
height: mobileButtonSize,
borderRadius: "50%",
border: "none",
cursor: "pointer",
padding: 0,
}}
animate={{
backgroundColor: activeIndex === index ? activeColor : "rgba(0, 0, 0, 0.15)",
scale: activeIndex === index ? 1.2 : 1,
}}
transition={{
duration: animationDuration * 0.75,
ease: "easeOut",
}}
whileTap={{ scale: 0.95 }}
/>
))}
</div>
) : (
// Avatar row - with responsive scroll on mobile
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
overflowX: isMobile ? "auto" : "visible",
maxWidth: "100%",
padding: isMobile ? "8px 0" : "0",
WebkitOverflowScrolling: "touch",
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
>
{testimonials.map((testimonial, index) => {
const isActive = activeIndex === index
const isHovered = hoveredIndex === index && !isActive
const showName = (isActive || isHovered) && !isMobile
const initials = testimonial.name
.split(" ")
.map((part) => part[0])
.filter(Boolean)
.slice(0, 2)
.join("")
.toUpperCase()
return (
<motion.button
key={index}
onClick={() => handleSelect(index)}
onMouseEnter={() => !isMobile && setHoveredIndex(index)}
onMouseLeave={() => !isMobile && setHoveredIndex(null)}
style={{
position: "relative",
display: "flex",
alignItems: "center",
gap: 0,
borderRadius: "9999px",
cursor: "pointer",
border: "none",
overflow: "hidden",
flexShrink: 0,
WebkitTapHighlightColor: "transparent",
}}
animate={{
backgroundColor: isActive
? activeColor
: isHovered
? hoverColor
: "transparent",
paddingRight: showName ? 16 : 2,
paddingLeft: showName ? 8 : 2,
paddingTop: showName ? 8 : 2,
paddingBottom: showName ? 8 : 2,
boxShadow: isActive
? isMobile
? "0 4px 6px -1px rgba(0, 0, 0, 0.1)"
: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)"
: "0 0 0 0 rgba(0, 0, 0, 0)",
}}
transition={{
duration: animationDuration * 1.25,
ease: [0.4, 0, 0.2, 1],
}}
whileTap={{ scale: 0.98 }}
>
{/* Avatar — initials by default. Pass a real `avatar` URL
per testimonial to show a photo instead. */}
<div
style={{
position: "relative",
flexShrink: 0,
}}
>
{testimonial.avatar ? (
<motion.img
src={testimonial.avatar}
alt={testimonial.name}
style={{
width: `${currentAvatarSize}px`,
height: `${currentAvatarSize}px`,
borderRadius: "9999px",
objectFit: "cover",
}}
animate={{
boxShadow: isActive
? "0 0 0 2px rgba(255, 255, 255, 0.3)"
: "0 0 0 0px rgba(255, 255, 255, 0)",
}}
transition={{
duration: animationDuration * 1.25,
ease: [0.4, 0, 0.2, 1],
}}
whileHover={!isActive && !isMobile ? { scale: 1.05 } : {}}
/>
) : (
<motion.div
style={{
width: `${currentAvatarSize}px`,
height: `${currentAvatarSize}px`,
borderRadius: "9999px",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: isActive ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.08)",
color: isActive ? "#ffffff" : quoteColor,
fontSize: Math.max(10, currentAvatarSize * 0.38),
fontWeight: 600,
}}
animate={{
boxShadow: isActive
? "0 0 0 2px rgba(255, 255, 255, 0.3)"
: "0 0 0 0px rgba(255, 255, 255, 0)",
}}
transition={{
duration: animationDuration * 1.25,
ease: [0.4, 0, 0.2, 1],
}}
whileHover={!isActive && !isMobile ? { scale: 1.05 } : {}}
>
{initials}
</motion.div>
)}
</div>
{/* Name - only show on desktop when active/hovered */}
{!isMobile && (
<motion.div
style={{
display: "grid",
overflow: "hidden",
}}
animate={{
gridTemplateColumns: showName ? "1fr" : "0fr",
opacity: showName ? 1 : 0,
marginLeft: showName ? 8 : 0,
}}
transition={{
duration: animationDuration * 1.25,
ease: [0.4, 0, 0.2, 1],
}}
>
<div style={{ overflow: "hidden" }}>
<motion.span
style={{
fontSize: "14px",
fontWeight: 500,
whiteSpace: "nowrap",
display: "block",
}}
animate={{
color: isActive ? "#ffffff" : quoteColor,
}}
transition={{
duration: animationDuration * 0.75,
}}
>
{testimonial.name}
</motion.span>
</div>
</motion.div>
)}
</motion.button>
)
})}
</div>
)}
{/* Swipe hint for mobile */}
{isMobile && enableSwipeGestures && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
transition={{ delay: 2, duration: 1 }}
style={{
fontSize: "11px",
color: roleColor,
margin: 0,
marginTop: "8px",
}}
>
Swipe to navigate
</motion.p>
)}
</div>
</div>
</div>
)
}
TestimonialsMobileCarousel.defaultProps = {
testimonials: [
{
name: "Jamie R.",
location: "Brisbane, QLD",
rating: 5,
service: "Full renovation",
date: "November 2025",
review: "From the first call to the final walkthrough, everything was handled clearly and on schedule. The team kept us informed at every step and the result exceeded what we expected. From start to finish it was about 6 weeks and the quality is outstanding. Can't recommend them enough!",
},
{
name: "Morgan T.",
location: "Newcastle, NSW",
rating: 5,
service: "Consultation",
date: "September 2025",
review: "I'd been putting off dealing with this for months. A mate recommended this team and honestly, they made it painless. Their project team sorted the scope of works end to end, and everyone who came out was professional and respectful of our property. Good as new.",
},
{
name: "Casey O.",
location: "Toowoomba, QLD",
rating: 5,
service: "Project delivery",
date: "January 2026",
review: "I was nervous about a big job on my home. The team were genuinely lovely — they explained every step, used quality materials, and even cleaned up afterwards. It has completely transformed the look of our place. Five stars all day.",
},
],
quoteFont: {
fontSize: "28px",
fontWeight: 300,
lineHeight: 1.6,
},
quoteColor: "#000000",
backgroundColor: "#ffffff",
activeColor: "#000000",
hoverColor: "rgba(0, 0, 0, 0.05)",
roleColor: "#6b7280",
showQuotationMarks: true,
quoteMarkColor: "#000000",
quoteMarkSize: 72,
quotationMarkOpacity: 0.06,
animationDuration: 0.4,
gap: 40,
avatarSize: 32,
backgroundBlur: 0,
borderRadius: 16,
enableGlassMorphism: false,
glassOpacity: 0.1,
glassBlur: 20,
enableIridescence: true,
iridescentIntensity: 0.15,
iridescentSpeed: 8,
enableNoise: true,
noiseOpacity: 0.05,
enableSpecularHighlight: true,
specularOpacity: 0.3,
enableEdgeGlow: true,
edgeGlowColor: "rgba(255, 255, 255, 0.2)",
edgeGlowIntensity: 20,
enableRefraction: true,
refractionIntensity: 3,
refractionBlur: 8,
// Mobile defaults
mobileBreakpoint: 768,
mobileFontSize: "20px",
mobilePadding: 20,
mobileAvatarSize: 28,
mobileQuoteMarkSize: 48,
enableSwipeGestures: true,
autoRotate: false,
autoRotateInterval: 5,
mobileMaxWidth: "100%",
mobileGap: 24,
showDotsOnMobile: false,
mobileButtonSize: 8,
}
addPropertyControls(TestimonialsMobileCarousel, {
testimonials: {
type: ControlType.Array,
title: "Testimonials",
control: {
type: ControlType.Object,
controls: {
name: {
type: ControlType.String,
title: "Name",
defaultValue: "John Doe",
},
location: {
type: ControlType.String,
title: "Location",
defaultValue: "Sydney, NSW",
},
rating: {
type: ControlType.Number,
title: "Rating",
defaultValue: 5,
min: 1,
max: 5,
step: 1,
},
service: {
type: ControlType.String,
title: "Service",
defaultValue: "Full renovation",
},
date: {
type: ControlType.String,
title: "Date",
defaultValue: "January 2026",
},
review: {
type: ControlType.String,
title: "Review",
defaultValue: "Great service and quality work.",
},
avatar: {
type: ControlType.Image,
title: "Avatar",
description: "Optional. Leave empty to show the person's initials instead.",
},
},
},
},
quoteFont: {
type: ControlType.Font,
title: "Quote Font",
defaultValue: {
fontSize: "28px",
fontWeight: 300,
lineHeight: 1.4,
},
controls: "extended",
},
quoteColor: {
type: ControlType.Color,
title: "Quote Color",
defaultValue: "#000000",
},
backgroundColor: {
type: ControlType.Color,
title: "Background",
defaultValue: "#ffffff",
},
backgroundBlur: {
type: ControlType.Number,
title: "Background Blur",
defaultValue: 0,
min: 0,
max: 50,
step: 1,
unit: "px",
displayStepper: true,
},
activeColor: {
type: ControlType.Color,
title: "Active Pill Color",
defaultValue: "#000000",
},
hoverColor: {
type: ControlType.Color,
title: "Hover Color",
defaultValue: "rgba(0, 0, 0, 0.05)",
},
roleColor: {
type: ControlType.Color,
title: "Role Color",
defaultValue: "#6b7280",
},
showQuotationMarks: {
type: ControlType.Boolean,
title: "Show Quotes",
defaultValue: true,
enabledTitle: "Show",
disabledTitle: "Hide",
},
quoteMarkColor: {
type: ControlType.Color,
title: "Mark Color",
defaultValue: "#000000",
hidden: (props) => props.showQuotationMarks === false,
},
quoteMarkSize: {
type: ControlType.Number,
title: "Mark Size",
defaultValue: 72,
min: 24,
max: 200,
step: 4,
displayStepper: true,
hidden: (props) => props.showQuotationMarks === false,
},
quotationMarkOpacity: {
type: ControlType.Number,
title: "Mark Opacity",
defaultValue: 0.06,
min: 0,
max: 1,
step: 0.01,
hidden: (props) => props.showQuotationMarks === false,
},
animationDuration: {
type: ControlType.Number,
title: "Animation Speed",
defaultValue: 0.4,
min: 0.1,
max: 2,
step: 0.1,
unit: "s",
displayStepper: true,
},
gap: {
type: ControlType.Number,
title: "Gap",
defaultValue: 40,
min: 10,
max: 100,
step: 5,
unit: "px",
displayStepper: true,
},
avatarSize: {
type: ControlType.Number,
title: "Avatar Size",
defaultValue: 32,
min: 24,
max: 64,
step: 4,
unit: "px",
displayStepper: true,
},
borderRadius: {
type: ControlType.Number,
title: "Border Radius",
defaultValue: 16,
min: 0,
max: 50,
step: 2,
unit: "px",
displayStepper: true,
},
// Mobile-specific controls
mobileBreakpoint: {
type: ControlType.Number,
title: "Mobile Breakpoint",
defaultValue: 768,
min: 320,
max: 1024,
step: 10,
unit: "px",
displayStepper: true,
},
mobileFontSize: {
type: ControlType.String,
title: "Mobile Font Size",
defaultValue: "20px",
},
mobilePadding: {
type: ControlType.Number,
title: "Mobile Padding",
defaultValue: 20,
min: 10,
max: 50,
step: 2,
unit: "px",
displayStepper: true,
},
mobileAvatarSize: {
type: ControlType.Number,
title: "Mobile Avatar Size",
defaultValue: 28,
min: 20,
max: 48,
step: 2,
unit: "px",
displayStepper: true,
},
mobileQuoteMarkSize: {
type: ControlType.Number,
title: "Mobile Quote Size",
defaultValue: 48,
min: 24,
max: 100,
step: 4,
displayStepper: true,
},
enableSwipeGestures: {
type: ControlType.Boolean,
title: "Swipe Gestures",
defaultValue: true,
enabledTitle: "Enabled",
disabledTitle: "Disabled",
},
autoRotate: {
type: ControlType.Boolean,
title: "Auto Rotate (Mobile)",
defaultValue: false,
enabledTitle: "On",
disabledTitle: "Off",
},
autoRotateInterval: {
type: ControlType.Number,
title: "Rotate Interval",
defaultValue: 5,
min: 2,
max: 10,
step: 1,
unit: "s",
displayStepper: true,
hidden: (props) => !props.autoRotate,
},
mobileMaxWidth: {
type: ControlType.String,
title: "Mobile Max Width",
defaultValue: "100%",
},
mobileGap: {
type: ControlType.Number,
title: "Mobile Gap",
defaultValue: 24,
min: 10,
max: 60,
step: 2,
unit: "px",
displayStepper: true,
},
showDotsOnMobile: {
type: ControlType.Boolean,
title: "Show Dots (Mobile)",
defaultValue: false,
enabledTitle: "Dots",
disabledTitle: "Avatars",
},
mobileButtonSize: {
type: ControlType.Number,
title: "Dot Size",
defaultValue: 8,
min: 4,
max: 16,
step: 2,
unit: "px",
displayStepper: true,
hidden: (props) => !props.showDotsOnMobile,
},
// Glass morphism controls (simplified grouping)
enableGlassMorphism: {
type: ControlType.Boolean,
title: "Glass Effect",
defaultValue: false,
enabledTitle: "Enabled",
disabledTitle: "Disabled",
},
glassOpacity: {
type: ControlType.Number,
title: "Glass Opacity",
defaultValue: 0.1,
min: 0,
max: 1,
step: 0.05,
hidden: (props) => !props.enableGlassMorphism,
},
glassBlur: {
type: ControlType.Number,
title: "Glass Blur",
defaultValue: 20,
min: 0,
max: 50,
step: 2,
unit: "px",
hidden: (props) => !props.enableGlassMorphism,
displayStepper: true,
},
enableIridescence: {
type: ControlType.Boolean,
title: "Iridescence",
defaultValue: true,
enabledTitle: "On",
disabledTitle: "Off",
hidden: (props) => !props.enableGlassMorphism,
},
iridescentIntensity: {
type: ControlType.Number,
title: "Iridescent Intensity",
defaultValue: 0.15,
min: 0,
max: 1,
step: 0.05,
hidden: (props) => !props.enableGlassMorphism || !props.enableIridescence,
},
iridescentSpeed: {
type: ControlType.Number,
title: "Iridescent Speed",
defaultValue: 8,
min: 2,
max: 20,
step: 1,
unit: "s",
hidden: (props) => !props.enableGlassMorphism || !props.enableIridescence,
displayStepper: true,
},
enableNoise: {
type: ControlType.Boolean,
title: "Noise Texture",
defaultValue: true,
enabledTitle: "On",
disabledTitle: "Off",
hidden: (props) => !props.enableGlassMorphism,
},
noiseOpacity: {
type: ControlType.Number,
title: "Noise Opacity",
defaultValue: 0.05,
min: 0,
max: 0.3,
step: 0.01,
hidden: (props) => !props.enableGlassMorphism || !props.enableNoise,
},
enableSpecularHighlight: {
type: ControlType.Boolean,
title: "Specular Highlight",
defaultValue: true,
enabledTitle: "On",
disabledTitle: "Off",
hidden: (props) => !props.enableGlassMorphism,
},
specularOpacity: {
type: ControlType.Number,
title: "Highlight Opacity",
defaultValue: 0.3,
min: 0,
max: 1,
step: 0.05,
hidden: (props) => !props.enableGlassMorphism || !props.enableSpecularHighlight,
},
enableEdgeGlow: {
type: ControlType.Boolean,
title: "Edge Glow",
defaultValue: true,
enabledTitle: "On",
disabledTitle: "Off",
hidden: (props) => !props.enableGlassMorphism,
},
edgeGlowColor: {
type: ControlType.Color,
title: "Glow Color",
defaultValue: "rgba(255, 255, 255, 0.2)",
hidden: (props) => !props.enableGlassMorphism || !props.enableEdgeGlow,
},
edgeGlowIntensity: {
type: ControlType.Number,
title: "Glow Intensity",
defaultValue: 20,
min: 0,
max: 50,
step: 2,
unit: "px",
hidden: (props) => !props.enableGlassMorphism || !props.enableEdgeGlow,
displayStepper: true,
},
enableRefraction: {
type: ControlType.Boolean,
title: "Refraction Distortion",
defaultValue: false, // Disabled by default on mobile
enabledTitle: "On",
disabledTitle: "Off",
hidden: (props) => !props.enableGlassMorphism,
},
refractionIntensity: {
type: ControlType.Number,
title: "Refraction Intensity",
defaultValue: 3,
min: 0,
max: 10,
step: 0.5,
unit: "px",
hidden: (props) => !props.enableGlassMorphism || !props.enableRefraction,
displayStepper: true,
},
refractionBlur: {
type: ControlType.Number,
title: "Refraction Blur",
defaultValue: 8,
min: 0,
max: 20,
step: 1,
unit: "px",
hidden: (props) => !props.enableGlassMorphism || !props.enableRefraction,
displayStepper: true,
},
})
- 1. In Framer: Assets → Code → Create Code File.
- 2. Delete the placeholder and paste this entire file — it already carries its default export and addPropertyControls.