I'm implementing an expandable input field that transforms into a span when losing focus. The input expands on focus and shrinks when converting back to a span. While the width animation works correctly, I'm experiencing an unwanted flickering effect in the text content during the transition.
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>
)
}
Current Behavior:
Expected Behavior:
How can I eliminate the text flickering effect while maintaining the smooth width animation during the input-to-span transition?
It seems the cause of the flicker is due to the properties you are transitioning. Namely, flex-basis
, where you toggle between auto
(implicitly from flex-initial
) and 0%
(from flex-1
).
Instead, it sounds like you'd want to have the width stop/start at the width of the <span>
. If so, consider transitioning and changing flex-grow
only:
const { useState, useRef, useEffect } = React;
// Simple className merger function to replace cn()
const cn = (...classes) => {
return classes.filter(Boolean).join(' ')
}
function Editable({
className,
inputClassName,
inlineTextClassName,
placeholder = 'Enter your text here...',
maxLength = 50,
isBold = false,
onFocusChange,
inlineText = 'inline text'
}) {
const [text, setText] = useState(placeholder)
const [isFocused, setIsFocused] = useState(false)
const inputRef = useRef(null)
const containerRef = useRef(null)
useEffect(() => {
const handleClickOutside = (event) => {
if (inputRef.current && !inputRef.current.contains(event.target)) {
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) => {
setText(e.target.value.slice(0, maxLength))
}
const handleKeyDown = (e) => {
if (e.key === 'Enter') inputRef.current.blur()
}
return (
<div className={cn('flex items-center w-full', className)}>
<div
ref={containerRef}
className={cn(
'transition-[flex-grow] duration-300 ease-in-out',
isFocused ? 'grow' : '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>
)
}
ReactDOM.createRoot(document.getElementById('app')).render(<Editable onFocusChange={() => {}} />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.15"></script>
<div id="app"></div>