From 4c8d2596b072757a66db43f79f12c0c140432d5c Mon Sep 17 00:00:00 2001 From: "Julian S." <70039325+Julian-AT@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:03:33 +0200 Subject: [PATCH 1/3] accessibility: add proper screenreader support Add screen reader support for component text. Previously, screen readers were unable to properly read the text displayed in the component, resulting in a poor user experience for visually impaired users. --- registry/magicui/text-animate.tsx | 66 +++++++++++++++++++------------ 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/registry/magicui/text-animate.tsx b/registry/magicui/text-animate.tsx index dc988dddb..f393126d1 100644 --- a/registry/magicui/text-animate.tsx +++ b/registry/magicui/text-animate.tsx @@ -62,6 +62,10 @@ interface TextAnimateProps extends MotionProps { * The animation preset to use */ animation?: AnimationVariant; + /** + * Whether to enable accessibility features (default: true) + */ + accessible?: boolean; } const staggerTimings: Record = { @@ -309,6 +313,7 @@ const TextAnimateBase = ({ once = false, by = "word", animation = "fadeIn", + accessible = true, ...props }: TextAnimateProps) => { const MotionComponent = motion.create(Component); @@ -332,47 +337,47 @@ const TextAnimateBase = ({ const finalVariants = variants ? { + container: { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + opacity: { duration: 0.01, delay }, + delayChildren: delay, + staggerChildren: duration / segments.length, + }, + }, + exit: { + opacity: 0, + transition: { + staggerChildren: duration / segments.length, + staggerDirection: -1, + }, + }, + }, + item: variants, + } + : animation + ? { container: { - hidden: { opacity: 0 }, + ...defaultItemAnimationVariants[animation].container, show: { - opacity: 1, + ...defaultItemAnimationVariants[animation].container.show, transition: { - opacity: { duration: 0.01, delay }, delayChildren: delay, staggerChildren: duration / segments.length, }, }, exit: { - opacity: 0, + ...defaultItemAnimationVariants[animation].container.exit, transition: { staggerChildren: duration / segments.length, staggerDirection: -1, }, }, }, - item: variants, + item: defaultItemAnimationVariants[animation].item, } - : animation - ? { - container: { - ...defaultItemAnimationVariants[animation].container, - show: { - ...defaultItemAnimationVariants[animation].container.show, - transition: { - delayChildren: delay, - staggerChildren: duration / segments.length, - }, - }, - exit: { - ...defaultItemAnimationVariants[animation].container.exit, - transition: { - staggerChildren: duration / segments.length, - staggerDirection: -1, - }, - }, - }, - item: defaultItemAnimationVariants[animation].item, - } : { container: defaultContainerVariants, item: defaultItemVariants }; return ( @@ -385,8 +390,15 @@ const TextAnimateBase = ({ exit="exit" className={cn("whitespace-pre-wrap", className)} viewport={{ once }} + aria-label={accessible ? children : undefined} + role={accessible ? "text" : undefined} {...props} > + {accessible && ( + + {children} + + )} {segments.map((segment, i) => ( {segment} From 32f70b421498bc05460eed17f00ca4eef901cd90 Mon Sep 17 00:00:00 2001 From: "Julian S." <70039325+Julian-AT@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:11:16 +0200 Subject: [PATCH 2/3] refactor: overwrite file with prettier --- registry/magicui/text-animate.tsx | 52 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/registry/magicui/text-animate.tsx b/registry/magicui/text-animate.tsx index f393126d1..e933f7b0b 100644 --- a/registry/magicui/text-animate.tsx +++ b/registry/magicui/text-animate.tsx @@ -337,47 +337,47 @@ const TextAnimateBase = ({ const finalVariants = variants ? { - container: { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - opacity: { duration: 0.01, delay }, - delayChildren: delay, - staggerChildren: duration / segments.length, - }, - }, - exit: { - opacity: 0, - transition: { - staggerChildren: duration / segments.length, - staggerDirection: -1, - }, - }, - }, - item: variants, - } - : animation - ? { container: { - ...defaultItemAnimationVariants[animation].container, + hidden: { opacity: 0 }, show: { - ...defaultItemAnimationVariants[animation].container.show, + opacity: 1, transition: { + opacity: { duration: 0.01, delay }, delayChildren: delay, staggerChildren: duration / segments.length, }, }, exit: { - ...defaultItemAnimationVariants[animation].container.exit, + opacity: 0, transition: { staggerChildren: duration / segments.length, staggerDirection: -1, }, }, }, - item: defaultItemAnimationVariants[animation].item, + item: variants, } + : animation + ? { + container: { + ...defaultItemAnimationVariants[animation].container, + show: { + ...defaultItemAnimationVariants[animation].container.show, + transition: { + delayChildren: delay, + staggerChildren: duration / segments.length, + }, + }, + exit: { + ...defaultItemAnimationVariants[animation].container.exit, + transition: { + staggerChildren: duration / segments.length, + staggerDirection: -1, + }, + }, + }, + item: defaultItemAnimationVariants[animation].item, + } : { container: defaultContainerVariants, item: defaultItemVariants }; return ( From 66a5dbfc91ab4bb4a7e65f762b550b9877844f8d Mon Sep 17 00:00:00 2001 From: Arghya Das Date: Fri, 22 Aug 2025 22:11:37 +0530 Subject: [PATCH 3/3] feat: updated with lint fix --- public/r/text-animate.json | 2 +- registry/magicui/text-animate.tsx | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/public/r/text-animate.json b/public/r/text-animate.json index 687388618..3ba28d063 100644 --- a/public/r/text-animate.json +++ b/public/r/text-animate.json @@ -10,7 +10,7 @@ "files": [ { "path": "registry/magicui/text-animate.tsx", - "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, MotionProps, Variants } from \"motion/react\";\nimport { ElementType, memo } from \"react\";\n\ntype AnimationType = \"text\" | \"word\" | \"character\" | \"line\";\ntype AnimationVariant =\n | \"fadeIn\"\n | \"blurIn\"\n | \"blurInUp\"\n | \"blurInDown\"\n | \"slideUp\"\n | \"slideDown\"\n | \"slideLeft\"\n | \"slideRight\"\n | \"scaleUp\"\n | \"scaleDown\";\n\ninterface TextAnimateProps extends MotionProps {\n /**\n * The text content to animate\n */\n children: string;\n /**\n * The class name to be applied to the component\n */\n className?: string;\n /**\n * The class name to be applied to each segment\n */\n segmentClassName?: string;\n /**\n * The delay before the animation starts\n */\n delay?: number;\n /**\n * The duration of the animation\n */\n duration?: number;\n /**\n * Custom motion variants for the animation\n */\n variants?: Variants;\n /**\n * The element type to render\n */\n as?: ElementType;\n /**\n * How to split the text (\"text\", \"word\", \"character\")\n */\n by?: AnimationType;\n /**\n * Whether to start animation when component enters viewport\n */\n startOnView?: boolean;\n /**\n * Whether to animate only once\n */\n once?: boolean;\n /**\n * The animation preset to use\n */\n animation?: AnimationVariant;\n}\n\nconst staggerTimings: Record = {\n text: 0.06,\n word: 0.05,\n character: 0.03,\n line: 0.06,\n};\n\nconst defaultContainerVariants = {\n hidden: { opacity: 1 },\n show: {\n opacity: 1,\n transition: {\n delayChildren: 0,\n staggerChildren: 0.05,\n },\n },\n exit: {\n opacity: 0,\n transition: {\n staggerChildren: 0.05,\n staggerDirection: -1,\n },\n },\n};\n\nconst defaultItemVariants: Variants = {\n hidden: { opacity: 0 },\n show: {\n opacity: 1,\n },\n exit: {\n opacity: 0,\n },\n};\n\nconst defaultItemAnimationVariants: Record<\n AnimationVariant,\n { container: Variants; item: Variants }\n> = {\n fadeIn: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, y: 20 },\n show: {\n opacity: 1,\n y: 0,\n transition: {\n duration: 0.3,\n },\n },\n exit: {\n opacity: 0,\n y: 20,\n transition: { duration: 0.3 },\n },\n },\n },\n blurIn: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, filter: \"blur(10px)\" },\n show: {\n opacity: 1,\n filter: \"blur(0px)\",\n transition: {\n duration: 0.3,\n },\n },\n exit: {\n opacity: 0,\n filter: \"blur(10px)\",\n transition: { duration: 0.3 },\n },\n },\n },\n blurInUp: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, filter: \"blur(10px)\", y: 20 },\n show: {\n opacity: 1,\n filter: \"blur(0px)\",\n y: 0,\n transition: {\n y: { duration: 0.3 },\n opacity: { duration: 0.4 },\n filter: { duration: 0.3 },\n },\n },\n exit: {\n opacity: 0,\n filter: \"blur(10px)\",\n y: 20,\n transition: {\n y: { duration: 0.3 },\n opacity: { duration: 0.4 },\n filter: { duration: 0.3 },\n },\n },\n },\n },\n blurInDown: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, filter: \"blur(10px)\", y: -20 },\n show: {\n opacity: 1,\n filter: \"blur(0px)\",\n y: 0,\n transition: {\n y: { duration: 0.3 },\n opacity: { duration: 0.4 },\n filter: { duration: 0.3 },\n },\n },\n },\n },\n slideUp: {\n container: defaultContainerVariants,\n item: {\n hidden: { y: 20, opacity: 0 },\n show: {\n y: 0,\n opacity: 1,\n transition: {\n duration: 0.3,\n },\n },\n exit: {\n y: -20,\n opacity: 0,\n transition: {\n duration: 0.3,\n },\n },\n },\n },\n slideDown: {\n container: defaultContainerVariants,\n item: {\n hidden: { y: -20, opacity: 0 },\n show: {\n y: 0,\n opacity: 1,\n transition: { duration: 0.3 },\n },\n exit: {\n y: 20,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n slideLeft: {\n container: defaultContainerVariants,\n item: {\n hidden: { x: 20, opacity: 0 },\n show: {\n x: 0,\n opacity: 1,\n transition: { duration: 0.3 },\n },\n exit: {\n x: -20,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n slideRight: {\n container: defaultContainerVariants,\n item: {\n hidden: { x: -20, opacity: 0 },\n show: {\n x: 0,\n opacity: 1,\n transition: { duration: 0.3 },\n },\n exit: {\n x: 20,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n scaleUp: {\n container: defaultContainerVariants,\n item: {\n hidden: { scale: 0.5, opacity: 0 },\n show: {\n scale: 1,\n opacity: 1,\n transition: {\n duration: 0.3,\n scale: {\n type: \"spring\",\n damping: 15,\n stiffness: 300,\n },\n },\n },\n exit: {\n scale: 0.5,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n scaleDown: {\n container: defaultContainerVariants,\n item: {\n hidden: { scale: 1.5, opacity: 0 },\n show: {\n scale: 1,\n opacity: 1,\n transition: {\n duration: 0.3,\n scale: {\n type: \"spring\",\n damping: 15,\n stiffness: 300,\n },\n },\n },\n exit: {\n scale: 1.5,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n};\n\nconst TextAnimateBase = ({\n children,\n delay = 0,\n duration = 0.3,\n variants,\n className,\n segmentClassName,\n as: Component = \"p\",\n startOnView = true,\n once = false,\n by = \"word\",\n animation = \"fadeIn\",\n ...props\n}: TextAnimateProps) => {\n const MotionComponent = motion.create(Component);\n\n let segments: string[] = [];\n switch (by) {\n case \"word\":\n segments = children.split(/(\\s+)/);\n break;\n case \"character\":\n segments = children.split(\"\");\n break;\n case \"line\":\n segments = children.split(\"\\n\");\n break;\n case \"text\":\n default:\n segments = [children];\n break;\n }\n\n const finalVariants = variants\n ? {\n container: {\n hidden: { opacity: 0 },\n show: {\n opacity: 1,\n transition: {\n opacity: { duration: 0.01, delay },\n delayChildren: delay,\n staggerChildren: duration / segments.length,\n },\n },\n exit: {\n opacity: 0,\n transition: {\n staggerChildren: duration / segments.length,\n staggerDirection: -1,\n },\n },\n },\n item: variants,\n }\n : animation\n ? {\n container: {\n ...defaultItemAnimationVariants[animation].container,\n show: {\n ...defaultItemAnimationVariants[animation].container.show,\n transition: {\n delayChildren: delay,\n staggerChildren: duration / segments.length,\n },\n },\n exit: {\n ...defaultItemAnimationVariants[animation].container.exit,\n transition: {\n staggerChildren: duration / segments.length,\n staggerDirection: -1,\n },\n },\n },\n item: defaultItemAnimationVariants[animation].item,\n }\n : { container: defaultContainerVariants, item: defaultItemVariants };\n\n return (\n \n \n {segments.map((segment, i) => (\n \n {segment}\n \n ))}\n \n \n );\n};\n\n// Export the memoized version\nexport const TextAnimate = memo(TextAnimateBase);\n", + "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, MotionProps, Variants } from \"motion/react\";\nimport { ElementType, memo } from \"react\";\n\ntype AnimationType = \"text\" | \"word\" | \"character\" | \"line\";\ntype AnimationVariant =\n | \"fadeIn\"\n | \"blurIn\"\n | \"blurInUp\"\n | \"blurInDown\"\n | \"slideUp\"\n | \"slideDown\"\n | \"slideLeft\"\n | \"slideRight\"\n | \"scaleUp\"\n | \"scaleDown\";\n\ninterface TextAnimateProps extends MotionProps {\n /**\n * The text content to animate\n */\n children: string;\n /**\n * The class name to be applied to the component\n */\n className?: string;\n /**\n * The class name to be applied to each segment\n */\n segmentClassName?: string;\n /**\n * The delay before the animation starts\n */\n delay?: number;\n /**\n * The duration of the animation\n */\n duration?: number;\n /**\n * Custom motion variants for the animation\n */\n variants?: Variants;\n /**\n * The element type to render\n */\n as?: ElementType;\n /**\n * How to split the text (\"text\", \"word\", \"character\")\n */\n by?: AnimationType;\n /**\n * Whether to start animation when component enters viewport\n */\n startOnView?: boolean;\n /**\n * Whether to animate only once\n */\n once?: boolean;\n /**\n * The animation preset to use\n */\n animation?: AnimationVariant;\n /**\n * Whether to enable accessibility features (default: true)\n */\n accessible?: boolean;\n}\n\nconst staggerTimings: Record = {\n text: 0.06,\n word: 0.05,\n character: 0.03,\n line: 0.06,\n};\n\nconst defaultContainerVariants = {\n hidden: { opacity: 1 },\n show: {\n opacity: 1,\n transition: {\n delayChildren: 0,\n staggerChildren: 0.05,\n },\n },\n exit: {\n opacity: 0,\n transition: {\n staggerChildren: 0.05,\n staggerDirection: -1,\n },\n },\n};\n\nconst defaultItemVariants: Variants = {\n hidden: { opacity: 0 },\n show: {\n opacity: 1,\n },\n exit: {\n opacity: 0,\n },\n};\n\nconst defaultItemAnimationVariants: Record<\n AnimationVariant,\n { container: Variants; item: Variants }\n> = {\n fadeIn: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, y: 20 },\n show: {\n opacity: 1,\n y: 0,\n transition: {\n duration: 0.3,\n },\n },\n exit: {\n opacity: 0,\n y: 20,\n transition: { duration: 0.3 },\n },\n },\n },\n blurIn: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, filter: \"blur(10px)\" },\n show: {\n opacity: 1,\n filter: \"blur(0px)\",\n transition: {\n duration: 0.3,\n },\n },\n exit: {\n opacity: 0,\n filter: \"blur(10px)\",\n transition: { duration: 0.3 },\n },\n },\n },\n blurInUp: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, filter: \"blur(10px)\", y: 20 },\n show: {\n opacity: 1,\n filter: \"blur(0px)\",\n y: 0,\n transition: {\n y: { duration: 0.3 },\n opacity: { duration: 0.4 },\n filter: { duration: 0.3 },\n },\n },\n exit: {\n opacity: 0,\n filter: \"blur(10px)\",\n y: 20,\n transition: {\n y: { duration: 0.3 },\n opacity: { duration: 0.4 },\n filter: { duration: 0.3 },\n },\n },\n },\n },\n blurInDown: {\n container: defaultContainerVariants,\n item: {\n hidden: { opacity: 0, filter: \"blur(10px)\", y: -20 },\n show: {\n opacity: 1,\n filter: \"blur(0px)\",\n y: 0,\n transition: {\n y: { duration: 0.3 },\n opacity: { duration: 0.4 },\n filter: { duration: 0.3 },\n },\n },\n },\n },\n slideUp: {\n container: defaultContainerVariants,\n item: {\n hidden: { y: 20, opacity: 0 },\n show: {\n y: 0,\n opacity: 1,\n transition: {\n duration: 0.3,\n },\n },\n exit: {\n y: -20,\n opacity: 0,\n transition: {\n duration: 0.3,\n },\n },\n },\n },\n slideDown: {\n container: defaultContainerVariants,\n item: {\n hidden: { y: -20, opacity: 0 },\n show: {\n y: 0,\n opacity: 1,\n transition: { duration: 0.3 },\n },\n exit: {\n y: 20,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n slideLeft: {\n container: defaultContainerVariants,\n item: {\n hidden: { x: 20, opacity: 0 },\n show: {\n x: 0,\n opacity: 1,\n transition: { duration: 0.3 },\n },\n exit: {\n x: -20,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n slideRight: {\n container: defaultContainerVariants,\n item: {\n hidden: { x: -20, opacity: 0 },\n show: {\n x: 0,\n opacity: 1,\n transition: { duration: 0.3 },\n },\n exit: {\n x: 20,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n scaleUp: {\n container: defaultContainerVariants,\n item: {\n hidden: { scale: 0.5, opacity: 0 },\n show: {\n scale: 1,\n opacity: 1,\n transition: {\n duration: 0.3,\n scale: {\n type: \"spring\",\n damping: 15,\n stiffness: 300,\n },\n },\n },\n exit: {\n scale: 0.5,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n scaleDown: {\n container: defaultContainerVariants,\n item: {\n hidden: { scale: 1.5, opacity: 0 },\n show: {\n scale: 1,\n opacity: 1,\n transition: {\n duration: 0.3,\n scale: {\n type: \"spring\",\n damping: 15,\n stiffness: 300,\n },\n },\n },\n exit: {\n scale: 1.5,\n opacity: 0,\n transition: { duration: 0.3 },\n },\n },\n },\n};\n\nconst TextAnimateBase = ({\n children,\n delay = 0,\n duration = 0.3,\n variants,\n className,\n segmentClassName,\n as: Component = \"p\",\n startOnView = true,\n once = false,\n by = \"word\",\n animation = \"fadeIn\",\n accessible = true,\n ...props\n}: TextAnimateProps) => {\n const MotionComponent = motion.create(Component);\n\n let segments: string[] = [];\n switch (by) {\n case \"word\":\n segments = children.split(/(\\s+)/);\n break;\n case \"character\":\n segments = children.split(\"\");\n break;\n case \"line\":\n segments = children.split(\"\\n\");\n break;\n case \"text\":\n default:\n segments = [children];\n break;\n }\n\n const finalVariants = variants\n ? {\n container: {\n hidden: { opacity: 0 },\n show: {\n opacity: 1,\n transition: {\n opacity: { duration: 0.01, delay },\n delayChildren: delay,\n staggerChildren: duration / segments.length,\n },\n },\n exit: {\n opacity: 0,\n transition: {\n staggerChildren: duration / segments.length,\n staggerDirection: -1,\n },\n },\n },\n item: variants,\n }\n : animation\n ? {\n container: {\n ...defaultItemAnimationVariants[animation].container,\n show: {\n ...defaultItemAnimationVariants[animation].container.show,\n transition: {\n delayChildren: delay,\n staggerChildren: duration / segments.length,\n },\n },\n exit: {\n ...defaultItemAnimationVariants[animation].container.exit,\n transition: {\n staggerChildren: duration / segments.length,\n staggerDirection: -1,\n },\n },\n },\n item: defaultItemAnimationVariants[animation].item,\n }\n : { container: defaultContainerVariants, item: defaultItemVariants };\n\n return (\n \n \n {accessible && {children}}\n {segments.map((segment, i) => (\n \n {segment}\n \n ))}\n \n \n );\n};\n\n// Export the memoized version\nexport const TextAnimate = memo(TextAnimateBase);\n", "type": "registry:ui", "target": "components/magicui/text-animate.tsx" } diff --git a/registry/magicui/text-animate.tsx b/registry/magicui/text-animate.tsx index e933f7b0b..ad6a0562c 100644 --- a/registry/magicui/text-animate.tsx +++ b/registry/magicui/text-animate.tsx @@ -391,14 +391,9 @@ const TextAnimateBase = ({ className={cn("whitespace-pre-wrap", className)} viewport={{ once }} aria-label={accessible ? children : undefined} - role={accessible ? "text" : undefined} {...props} > - {accessible && ( - - {children} - - )} + {accessible && {children}} {segments.map((segment, i) => ( {segment}