Animated Input
Animated Input é um componente de formulário para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
forms
O que é
Animated Input é um componente de formulário para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Animated Input
import { motion, useReducedMotion } from "motion/react";
import { useId, useRef, useState } from "react";
const EASE_IN_OUT_CUBIC_X1 = 0.4;
const EASE_IN_OUT_CUBIC_Y1 = 0;
const EASE_IN_OUT_CUBIC_X2 = 0.2;
const EASE_IN_OUT_CUBIC_Y2 = 1;
const LABEL_TRANSITION = {
duration: 0.28,
ease: [
EASE_IN_OUT_CUBIC_X1,
EASE_IN_OUT_CUBIC_Y1,
EASE_IN_OUT_CUBIC_X2,
EASE_IN_OUT_CUBIC_Y2,
] as [number, number, number, number], // cubic-bezier tuple
};
export interface AnimatedInputProps {
className?: string;
defaultValue?: string;
disabled?: boolean;
icon?: React.ReactNode;
inputClassName?: string;
label: string;
labelClassName?: string;
onChange?: (value: string) => void;
placeholder?: string;
value?: string;
}
export default function AnimatedInput({
value,
defaultValue = "",
onChange,
label,
placeholder = "",
disabled = false,
className = "",
inputClassName = "",
labelClassName = "",
icon,
}: AnimatedInputProps) {
const [internalValue, setInternalValue] = useState(defaultValue);
const isControlled = value !== undefined;
const val = isControlled ? value : internalValue;
const inputRef = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
const isFloating = !!val || isFocused;
const shouldReduceMotion = useReducedMotion();
const reactId = useId();
const inputId = `animated-input-${reactId.replace(/:/g, "")}`;
const getLabelAnimation = () => {
if (shouldReduceMotion) {
return {};
}
if (isFloating) {
return {
borderColor: "var(--color-brand)",
color: "var(--color-brand)",
scale: 0.85,
y: -24,
};
}
return { color: "#6b7280", scale: 1, y: 0 };
};
const getLabelStyle = () => {
if (!shouldReduceMotion) {
return {};
}
if (isFloating) {
return {
borderColor: "var(--color-brand)",
color: "var(--color-brand)",
transform: "translateY(-24px) scale(0.85)",
};
}
return {
color: "#6b7280",
transform: "translateY(0) scale(1)",
};
};
return (
<div className={`relative flex items-center ${className}`}>
{icon ? (
<span
aria-hidden="true"
className="absolute top-1/2 left-3 -translate-y-1/2"
>
{icon}
</span>
) : null}
<input
aria-label={label}
className={`peer w-full rounded-sm border bg-background px-3 py-2 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${icon ? "pl-10" : ""} ${inputClassName}`}
disabled={disabled}
id={inputId}
onBlur={() => setIsFocused(false)}
onChange={(e) => {
if (!isControlled) {
setInternalValue(e.target.value);
}
onChange?.(e.target.value);
}}
onFocus={() => setIsFocused(true)}
placeholder={isFloating ? placeholder : ""}
ref={inputRef}
type="text"
value={val}
/>
<motion.label
animate={getLabelAnimation()}
className={`pointer-events-none absolute top-1/2 left-3 origin-left -translate-y-1/2 rounded-sm border border-transparent bg-background px-1 text-foreground transition-all ${labelClassName}`}
htmlFor={inputId}
style={{
zIndex: 2,
...getLabelStyle(),
}}
transition={shouldReduceMotion ? { duration: 0 } : LABEL_TRANSITION}
>
{label}
</motion.label>
</div>
);
}