如何消除React+Tailwind中输入框转Span的文本闪烁问题
解决可展开输入框过渡时文本闪烁问题
问题背景
实现了一个可展开的输入框组件,失焦时转为Span元素,聚焦时展开为输入框。宽度动画正常,但过渡期间文本出现不必要的闪烁:

当前表现
- 输入框获取焦点时展开
- 失焦时转换为Span并收缩
- 宽度动画期间文本内容出现闪烁
期望表现
- 输入框与Span状态间平滑过渡
- 动画期间无文本闪烁
原组件代码:
import React, { useState, useRef, useEffect } from 'react' // Simple className merger function to replace cn() const cn = (...classes: (string | undefined)[]) => { return classes.filter(Boolean).join(' ') } interface EditableProps { className?: string inputClassName?: string inlineTextClassName?: string placeholder?: string maxLength?: number isBold?: boolean onFocusChange?: (focused: boolean) => void inlineText?: string } export default function Editable({ className, inputClassName, inlineTextClassName, placeholder = 'Enter your text here...', maxLength = 50, isBold = false, onFocusChange, inlineText = 'inline text' }: EditableProps) { const [text, setText] = useState(placeholder) const [isFocused, setIsFocused] = useState(false) const inputRef = useRef<HTMLInputElement>(null) const containerRef = useRef<HTMLDivElement>(null) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (inputRef.current && !inputRef.current.contains(event.target as Node)) { setIsFocused(false) onFocusChange?.(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [onFocusChange]) const handleFocus = () => { setIsFocused(true) onFocusChange?.(true) setTimeout(() => inputRef.current?.select(), 0) } const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value.slice(0, maxLength)) } const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') inputRef.current?.blur() } return ( <div className={cn('flex items-center w-full', className)}> <div ref={containerRef} className={cn( 'transition-[flex] duration-300 ease-in-out', isFocused ? 'flex-1' : 'flex-initial' )} > {isFocused ? ( <div className="flex items-center bg-gray-100 rounded-md w-full"> <input ref={inputRef} value={text} onChange={handleChange} onBlur={() => setIsFocused(false)} onKeyDown={handleKeyDown} className={cn( 'input-ghost focus:outline-none px-2 py-1 border-none bg-gray-100 rounded-md w-full', isBold ? 'font-bold' : 'font-normal', inputClassName )} style={{ fontWeight: isBold ? 'bold' : 'normal' }} /> <div className="flex-shrink-0 px-2 text-sm text-gray-400"> {text.length}/{maxLength} </div> </div> ) : ( <span onClick={handleFocus} className={cn( 'cursor-pointer hover:opacity-80 px-2 py-1 inline-block', isBold ? 'font-bold' : 'font-normal', inputClassName )} > {text} </span> )} </div> <span className={cn('ml-2 text-sm text-gray-500 flex-shrink-0', inlineTextClassName)}>{inlineText}</span> </div> ) }
问题原因
闪烁的核心原因是条件渲染导致DOM元素的卸载/挂载:当isFocused切换时,React会完全移除输入框DOM、创建Span元素,这个过程中浏览器会触发重绘,加上动画期间的布局变化,就会出现文本闪烁。
解决方案
不要切换DOM元素,而是同时保留输入框和Span,通过CSS控制它们的可见性与交互能力,确保两者样式完全对齐,避免布局偏移。具体修改如下:
修改后的代码
import React, { useState, useRef, useEffect } from 'react' const cn = (...classes: (string | undefined)[]) => { return classes.filter(Boolean).join(' ') } interface EditableProps { className?: string inputClassName?: string inlineTextClassName?: string placeholder?: string maxLength?: number isBold?: boolean onFocusChange?: (focused: boolean) => void inlineText?: string } export default function Editable({ className, inputClassName, inlineTextClassName, placeholder = 'Enter your text here...', maxLength = 50, isBold = false, onFocusChange, inlineText = 'inline text' }: EditableProps) { const [text, setText] = useState(placeholder) const [isFocused, setIsFocused] = useState(false) const inputRef = useRef<HTMLInputElement>(null) const containerRef = useRef<HTMLDivElement>(null) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (inputRef.current && !inputRef.current.contains(event.target as Node)) { setIsFocused(false) onFocusChange?.(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [onFocusChange]) const handleFocus = () => { setIsFocused(true) onFocusChange?.(true) setTimeout(() => inputRef.current?.select(), 0) } const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value.slice(0, maxLength)) } const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') inputRef.current?.blur() } // 统一文本显示的基础样式 const textBaseClasses = cn( 'px-2 py-1 font-normal', isBold ? 'font-bold' : '', inputClassName ) return ( <div className={cn('flex items-center w-full', className)}> <div ref={containerRef} className={cn( 'relative transition-[flex] duration-300 ease-in-out', isFocused ? 'flex-1' : 'flex-initial' )} > {/* 输入框容器 - 聚焦时显示 */} <div className={cn( 'flex items-center bg-gray-100 rounded-md w-full absolute inset-0', isFocused ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none' )} > <input ref={inputRef} value={text} onChange={handleChange} onBlur={() => setIsFocused(false)} onKeyDown={handleKeyDown} className={cn( 'focus:outline-none border-none bg-transparent rounded-md w-full', textBaseClasses )} /> <div className="flex-shrink-0 px-2 text-sm text-gray-400"> {text.length}/{maxLength} </div> </div> {/* 静态Span - 失焦时显示 */} <span onClick={handleFocus} className={cn( 'cursor-pointer hover:opacity-80 inline-block', textBaseClasses, isFocused ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto' )} > {text} </span> </div> <span className={cn('ml-2 text-sm text-gray-500 flex-shrink-0', inlineTextClassName)}>{inlineText}</span> </div> ) }
关键修改点
- 统一DOM结构:同时保留输入框容器和Span,通过
opacity控制可见性,pointer-events控制是否可交互,避免DOM卸载/挂载 - 样式对齐:提取
textBaseClasses统一文本的内边距、字体样式,确保输入框和Span的文本布局完全一致 - 定位处理:给输入框容器设置
absolute inset-0,让它和Span在容器内完全重叠,过渡期间不会出现位置偏移 - 交互控制:聚焦时让输入框容器可交互、可见,Span不可见;失焦时反之,保持逻辑不变但避免DOM切换
这样修改后,宽度动画会保留,同时因为DOM元素始终存在,不会出现文本闪烁的问题。
内容的提问来源于stack exchange,提问作者Yehezkiel L
相关产品推荐
相关产品推荐

