5 de setembro de 2025
5 de setembro de 2025
5 de setembro de 2025
Tutorial
Tutorial
Personalize as Mensagens de Erro nos Formulários do Framer
Aprenda como substituir as mensagens de validação padrão do browser por tooltips personalizados nos formulários do Framer. Esse code override é compatível com o form builder nativo e oferece controle total sobre as mensagens de erro.
Compartilhe
Compartilhe
Compartilhe
Publicado por

Se você já tentou criar um formulário no Framer, provavelmente percebeu que as mensagens de erro padrão do navegador não são nada atraentes. Elas variam de browser para browser e não combinam com o design do seu site.
Com este code override, você pode substituir as mensagens padrão por tooltips personalizados, que aparecem ao lado do campo com erro. Assim, você mantém o estilo visual consistente e tem controle total sobre o texto e as cores.
Crie um novo arquivo
No seu projeto no Framer, vá até Assets → Code → New File para criar um novo arquivo de código.

Cole o código
Apague o conteúdo padrão e cole este código inteiro:
import { useEffect, useRef } from "react" import type { Override } from "framer" // ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", } // Elemento tooltip function makeTooltip() { const el = document.createElement("div") el.className = "framer-custom-tooltip" // Estilo mínimo; ajuste livremente Object.assign(el.style, { position: "fixed", zIndex: "999999", maxWidth: "260px", fontFamily: "Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", fontSize: "12px", lineHeight: "1.3", background: "#0055ff", color: "#fff", padding: "8px 10px", borderRadius: "8px", boxShadow: "0 6px 24px rgba(0,0,0,0.25)", pointerEvents: "none", transform: "translateY(-6px)", transition: "opacity 120ms ease, transform 120ms ease", opacity: "0", }) document.body.appendChild(el) requestAnimationFrame(() => { el.style.opacity = "1" el.style.transform = "translateY(0)" }) return el } function destroyTooltip(el?: HTMLElement | null) { if (el && el.parentNode) el.parentNode.removeChild(el) } function getMessageFor( input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) { // Prioridade: data-error > built-in validity type > placeholder > generic const custom = input.getAttribute("data-error") if (custom) return custom const v = input.validity if (v.valueMissing) return DEFAULT_MESSAGES.valueMissing if (v.typeMismatch && (input as HTMLInputElement).type === "email") return DEFAULT_MESSAGES.typeMismatchEmail if (v.typeMismatch && (input as HTMLInputElement).type === "url") return DEFAULT_MESSAGES.typeMismatchUrl if (v.patternMismatch) return DEFAULT_MESSAGES.patternMismatch if (v.tooShort) return DEFAULT_MESSAGES.tooShort if (v.tooLong) return DEFAULT_MESSAGES.tooLong if (v.rangeUnderflow) return DEFAULT_MESSAGES.rangeUnderflow if (v.rangeOverflow) return DEFAULT_MESSAGES.rangeOverflow if (v.stepMismatch) return DEFAULT_MESSAGES.stepMismatch if (v.badInput) return DEFAULT_MESSAGES.badInput const ph = (input as HTMLInputElement).placeholder if (ph) return ph return DEFAULT_MESSAGES.generic } function positionTooltipNear(input: Element, tip: HTMLElement) { const r = input.getBoundingClientRect() // posiciona acima e à direita por padrão; mantém dentro do viewport const margin = 8 let top = r.top - tip.offsetHeight - margin let left = r.left + Math.min(16, Math.max(0, r.width - tip.offsetWidth)) // Se cortado acima, posiciona abaixo if (top < 8) top = r.bottom + margin // Mantém dentro do viewport horizontalmente left = Math.max(8, Math.min(left, window.innerWidth - tip.offsetWidth - 8)) tip.style.top = `${Math.round(top)}px` tip.style.left = `${Math.round(left)}px` } export function FormWithCustomTooltips(): Override { const ref = useRef<HTMLElement | null>(null) const tipRef = useRef<HTMLElement | null>(null) const currentFieldRef = useRef<HTMLElement | null>(null) useEffect(() => { // Se aplicado no próprio form, usa o ref.current diretamente const formEl = ( ref.current?.tagName === "FORM" ? ref.current : ref.current?.closest("form") ) as HTMLFormElement | null if (!formEl) return formEl.setAttribute("novalidate", "true") const allFields = formEl.querySelectorAll< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement >("input, textarea, select") const show = ( field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) => { // Atualiza o texto de validade customizada (afeta leitores de tela e API de constraint) const msg = getMessageFor(field) field.setCustomValidity(msg) // Tooltip visual destroyTooltip(tipRef.current) const tip = makeTooltip() tip.textContent = msg positionTooltipNear(field, tip) tipRef.current = tip currentFieldRef.current = field } const hide = () => { destroyTooltip(tipRef.current) tipRef.current = null currentFieldRef.current = null } const onInput = (e: Event) => { const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement field.setCustomValidity("") // limpa assim que o usuário interage hide() } const onInvalid = (e: Event) => { e.preventDefault() // para a bolha do Chrome const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement show(field) } const onSubmit = (e: Event) => { hide() // Força a checagem de validade antes de submeter const isValid = formEl.checkValidity() if (!isValid) { e.preventDefault() // Para o submit e.stopPropagation() // Para a propagação // Foca no primeiro campo inválido const firstInvalid = Array.from(allFields).find( (f) => !f.checkValidity() ) if (firstInvalid) { firstInvalid.focus({ preventScroll: false }) show(firstInvalid) } return false } } // Define handlers que podem ser removidos adequadamente const handleResize = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } const handleScroll = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } // Conecta os eventos allFields.forEach((f) => { f.addEventListener("invalid", onInvalid) f.addEventListener("input", onInput) f.addEventListener("blur", onInput) }) formEl.addEventListener("submit", onSubmit) window.addEventListener("resize", handleResize) window.addEventListener("scroll", handleScroll, true) return () => { hide() allFields.forEach((f) => { f.removeEventListener("invalid", onInvalid) f.removeEventListener("input", onInput) f.removeEventListener("blur", onInput) }) formEl.removeEventListener("submit", onSubmit) window.removeEventListener("resize", handleResize) window.removeEventListener("scroll", handleScroll, true) } }, []) // Anexa a qualquer elemento dentro do seu formulário (ex: o frame ou wrapper do form) return { ref: (node: HTMLElement | null) => { ref.current = node }, } }
import { useEffect, useRef } from "react" import type { Override } from "framer" // ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", } // Elemento tooltip function makeTooltip() { const el = document.createElement("div") el.className = "framer-custom-tooltip" // Estilo mínimo; ajuste livremente Object.assign(el.style, { position: "fixed", zIndex: "999999", maxWidth: "260px", fontFamily: "Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", fontSize: "12px", lineHeight: "1.3", background: "#0055ff", color: "#fff", padding: "8px 10px", borderRadius: "8px", boxShadow: "0 6px 24px rgba(0,0,0,0.25)", pointerEvents: "none", transform: "translateY(-6px)", transition: "opacity 120ms ease, transform 120ms ease", opacity: "0", }) document.body.appendChild(el) requestAnimationFrame(() => { el.style.opacity = "1" el.style.transform = "translateY(0)" }) return el } function destroyTooltip(el?: HTMLElement | null) { if (el && el.parentNode) el.parentNode.removeChild(el) } function getMessageFor( input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) { // Prioridade: data-error > built-in validity type > placeholder > generic const custom = input.getAttribute("data-error") if (custom) return custom const v = input.validity if (v.valueMissing) return DEFAULT_MESSAGES.valueMissing if (v.typeMismatch && (input as HTMLInputElement).type === "email") return DEFAULT_MESSAGES.typeMismatchEmail if (v.typeMismatch && (input as HTMLInputElement).type === "url") return DEFAULT_MESSAGES.typeMismatchUrl if (v.patternMismatch) return DEFAULT_MESSAGES.patternMismatch if (v.tooShort) return DEFAULT_MESSAGES.tooShort if (v.tooLong) return DEFAULT_MESSAGES.tooLong if (v.rangeUnderflow) return DEFAULT_MESSAGES.rangeUnderflow if (v.rangeOverflow) return DEFAULT_MESSAGES.rangeOverflow if (v.stepMismatch) return DEFAULT_MESSAGES.stepMismatch if (v.badInput) return DEFAULT_MESSAGES.badInput const ph = (input as HTMLInputElement).placeholder if (ph) return ph return DEFAULT_MESSAGES.generic } function positionTooltipNear(input: Element, tip: HTMLElement) { const r = input.getBoundingClientRect() // posiciona acima e à direita por padrão; mantém dentro do viewport const margin = 8 let top = r.top - tip.offsetHeight - margin let left = r.left + Math.min(16, Math.max(0, r.width - tip.offsetWidth)) // Se cortado acima, posiciona abaixo if (top < 8) top = r.bottom + margin // Mantém dentro do viewport horizontalmente left = Math.max(8, Math.min(left, window.innerWidth - tip.offsetWidth - 8)) tip.style.top = `${Math.round(top)}px` tip.style.left = `${Math.round(left)}px` } export function FormWithCustomTooltips(): Override { const ref = useRef<HTMLElement | null>(null) const tipRef = useRef<HTMLElement | null>(null) const currentFieldRef = useRef<HTMLElement | null>(null) useEffect(() => { // Se aplicado no próprio form, usa o ref.current diretamente const formEl = ( ref.current?.tagName === "FORM" ? ref.current : ref.current?.closest("form") ) as HTMLFormElement | null if (!formEl) return formEl.setAttribute("novalidate", "true") const allFields = formEl.querySelectorAll< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement >("input, textarea, select") const show = ( field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) => { // Atualiza o texto de validade customizada (afeta leitores de tela e API de constraint) const msg = getMessageFor(field) field.setCustomValidity(msg) // Tooltip visual destroyTooltip(tipRef.current) const tip = makeTooltip() tip.textContent = msg positionTooltipNear(field, tip) tipRef.current = tip currentFieldRef.current = field } const hide = () => { destroyTooltip(tipRef.current) tipRef.current = null currentFieldRef.current = null } const onInput = (e: Event) => { const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement field.setCustomValidity("") // limpa assim que o usuário interage hide() } const onInvalid = (e: Event) => { e.preventDefault() // para a bolha do Chrome const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement show(field) } const onSubmit = (e: Event) => { hide() // Força a checagem de validade antes de submeter const isValid = formEl.checkValidity() if (!isValid) { e.preventDefault() // Para o submit e.stopPropagation() // Para a propagação // Foca no primeiro campo inválido const firstInvalid = Array.from(allFields).find( (f) => !f.checkValidity() ) if (firstInvalid) { firstInvalid.focus({ preventScroll: false }) show(firstInvalid) } return false } } // Define handlers que podem ser removidos adequadamente const handleResize = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } const handleScroll = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } // Conecta os eventos allFields.forEach((f) => { f.addEventListener("invalid", onInvalid) f.addEventListener("input", onInput) f.addEventListener("blur", onInput) }) formEl.addEventListener("submit", onSubmit) window.addEventListener("resize", handleResize) window.addEventListener("scroll", handleScroll, true) return () => { hide() allFields.forEach((f) => { f.removeEventListener("invalid", onInvalid) f.removeEventListener("input", onInput) f.removeEventListener("blur", onInput) }) formEl.removeEventListener("submit", onSubmit) window.removeEventListener("resize", handleResize) window.removeEventListener("scroll", handleScroll, true) } }, []) // Anexa a qualquer elemento dentro do seu formulário (ex: o frame ou wrapper do form) return { ref: (node: HTMLElement | null) => { ref.current = node }, } }
import { useEffect, useRef } from "react" import type { Override } from "framer" // ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", } // Elemento tooltip function makeTooltip() { const el = document.createElement("div") el.className = "framer-custom-tooltip" // Estilo mínimo; ajuste livremente Object.assign(el.style, { position: "fixed", zIndex: "999999", maxWidth: "260px", fontFamily: "Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", fontSize: "12px", lineHeight: "1.3", background: "#0055ff", color: "#fff", padding: "8px 10px", borderRadius: "8px", boxShadow: "0 6px 24px rgba(0,0,0,0.25)", pointerEvents: "none", transform: "translateY(-6px)", transition: "opacity 120ms ease, transform 120ms ease", opacity: "0", }) document.body.appendChild(el) requestAnimationFrame(() => { el.style.opacity = "1" el.style.transform = "translateY(0)" }) return el } function destroyTooltip(el?: HTMLElement | null) { if (el && el.parentNode) el.parentNode.removeChild(el) } function getMessageFor( input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) { // Prioridade: data-error > built-in validity type > placeholder > generic const custom = input.getAttribute("data-error") if (custom) return custom const v = input.validity if (v.valueMissing) return DEFAULT_MESSAGES.valueMissing if (v.typeMismatch && (input as HTMLInputElement).type === "email") return DEFAULT_MESSAGES.typeMismatchEmail if (v.typeMismatch && (input as HTMLInputElement).type === "url") return DEFAULT_MESSAGES.typeMismatchUrl if (v.patternMismatch) return DEFAULT_MESSAGES.patternMismatch if (v.tooShort) return DEFAULT_MESSAGES.tooShort if (v.tooLong) return DEFAULT_MESSAGES.tooLong if (v.rangeUnderflow) return DEFAULT_MESSAGES.rangeUnderflow if (v.rangeOverflow) return DEFAULT_MESSAGES.rangeOverflow if (v.stepMismatch) return DEFAULT_MESSAGES.stepMismatch if (v.badInput) return DEFAULT_MESSAGES.badInput const ph = (input as HTMLInputElement).placeholder if (ph) return ph return DEFAULT_MESSAGES.generic } function positionTooltipNear(input: Element, tip: HTMLElement) { const r = input.getBoundingClientRect() // posiciona acima e à direita por padrão; mantém dentro do viewport const margin = 8 let top = r.top - tip.offsetHeight - margin let left = r.left + Math.min(16, Math.max(0, r.width - tip.offsetWidth)) // Se cortado acima, posiciona abaixo if (top < 8) top = r.bottom + margin // Mantém dentro do viewport horizontalmente left = Math.max(8, Math.min(left, window.innerWidth - tip.offsetWidth - 8)) tip.style.top = `${Math.round(top)}px` tip.style.left = `${Math.round(left)}px` } export function FormWithCustomTooltips(): Override { const ref = useRef<HTMLElement | null>(null) const tipRef = useRef<HTMLElement | null>(null) const currentFieldRef = useRef<HTMLElement | null>(null) useEffect(() => { // Se aplicado no próprio form, usa o ref.current diretamente const formEl = ( ref.current?.tagName === "FORM" ? ref.current : ref.current?.closest("form") ) as HTMLFormElement | null if (!formEl) return formEl.setAttribute("novalidate", "true") const allFields = formEl.querySelectorAll< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement >("input, textarea, select") const show = ( field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) => { // Atualiza o texto de validade customizada (afeta leitores de tela e API de constraint) const msg = getMessageFor(field) field.setCustomValidity(msg) // Tooltip visual destroyTooltip(tipRef.current) const tip = makeTooltip() tip.textContent = msg positionTooltipNear(field, tip) tipRef.current = tip currentFieldRef.current = field } const hide = () => { destroyTooltip(tipRef.current) tipRef.current = null currentFieldRef.current = null } const onInput = (e: Event) => { const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement field.setCustomValidity("") // limpa assim que o usuário interage hide() } const onInvalid = (e: Event) => { e.preventDefault() // para a bolha do Chrome const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement show(field) } const onSubmit = (e: Event) => { hide() // Força a checagem de validade antes de submeter const isValid = formEl.checkValidity() if (!isValid) { e.preventDefault() // Para o submit e.stopPropagation() // Para a propagação // Foca no primeiro campo inválido const firstInvalid = Array.from(allFields).find( (f) => !f.checkValidity() ) if (firstInvalid) { firstInvalid.focus({ preventScroll: false }) show(firstInvalid) } return false } } // Define handlers que podem ser removidos adequadamente const handleResize = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } const handleScroll = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } // Conecta os eventos allFields.forEach((f) => { f.addEventListener("invalid", onInvalid) f.addEventListener("input", onInput) f.addEventListener("blur", onInput) }) formEl.addEventListener("submit", onSubmit) window.addEventListener("resize", handleResize) window.addEventListener("scroll", handleScroll, true) return () => { hide() allFields.forEach((f) => { f.removeEventListener("invalid", onInvalid) f.removeEventListener("input", onInput) f.removeEventListener("blur", onInput) }) formEl.removeEventListener("submit", onSubmit) window.removeEventListener("resize", handleResize) window.removeEventListener("scroll", handleScroll, true) } }, []) // Anexa a qualquer elemento dentro do seu formulário (ex: o frame ou wrapper do form) return { ref: (node: HTMLElement | null) => { ref.current = node }, } }
import { useEffect, useRef } from "react" import type { Override } from "framer" // ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", } // Elemento tooltip function makeTooltip() { const el = document.createElement("div") el.className = "framer-custom-tooltip" // Estilo mínimo; ajuste livremente Object.assign(el.style, { position: "fixed", zIndex: "999999", maxWidth: "260px", fontFamily: "Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", fontSize: "12px", lineHeight: "1.3", background: "#0055ff", color: "#fff", padding: "8px 10px", borderRadius: "8px", boxShadow: "0 6px 24px rgba(0,0,0,0.25)", pointerEvents: "none", transform: "translateY(-6px)", transition: "opacity 120ms ease, transform 120ms ease", opacity: "0", }) document.body.appendChild(el) requestAnimationFrame(() => { el.style.opacity = "1" el.style.transform = "translateY(0)" }) return el } function destroyTooltip(el?: HTMLElement | null) { if (el && el.parentNode) el.parentNode.removeChild(el) } function getMessageFor( input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) { // Prioridade: data-error > built-in validity type > placeholder > generic const custom = input.getAttribute("data-error") if (custom) return custom const v = input.validity if (v.valueMissing) return DEFAULT_MESSAGES.valueMissing if (v.typeMismatch && (input as HTMLInputElement).type === "email") return DEFAULT_MESSAGES.typeMismatchEmail if (v.typeMismatch && (input as HTMLInputElement).type === "url") return DEFAULT_MESSAGES.typeMismatchUrl if (v.patternMismatch) return DEFAULT_MESSAGES.patternMismatch if (v.tooShort) return DEFAULT_MESSAGES.tooShort if (v.tooLong) return DEFAULT_MESSAGES.tooLong if (v.rangeUnderflow) return DEFAULT_MESSAGES.rangeUnderflow if (v.rangeOverflow) return DEFAULT_MESSAGES.rangeOverflow if (v.stepMismatch) return DEFAULT_MESSAGES.stepMismatch if (v.badInput) return DEFAULT_MESSAGES.badInput const ph = (input as HTMLInputElement).placeholder if (ph) return ph return DEFAULT_MESSAGES.generic } function positionTooltipNear(input: Element, tip: HTMLElement) { const r = input.getBoundingClientRect() // posiciona acima e à direita por padrão; mantém dentro do viewport const margin = 8 let top = r.top - tip.offsetHeight - margin let left = r.left + Math.min(16, Math.max(0, r.width - tip.offsetWidth)) // Se cortado acima, posiciona abaixo if (top < 8) top = r.bottom + margin // Mantém dentro do viewport horizontalmente left = Math.max(8, Math.min(left, window.innerWidth - tip.offsetWidth - 8)) tip.style.top = `${Math.round(top)}px` tip.style.left = `${Math.round(left)}px` } export function FormWithCustomTooltips(): Override { const ref = useRef<HTMLElement | null>(null) const tipRef = useRef<HTMLElement | null>(null) const currentFieldRef = useRef<HTMLElement | null>(null) useEffect(() => { // Se aplicado no próprio form, usa o ref.current diretamente const formEl = ( ref.current?.tagName === "FORM" ? ref.current : ref.current?.closest("form") ) as HTMLFormElement | null if (!formEl) return formEl.setAttribute("novalidate", "true") const allFields = formEl.querySelectorAll< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement >("input, textarea, select") const show = ( field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) => { // Atualiza o texto de validade customizada (afeta leitores de tela e API de constraint) const msg = getMessageFor(field) field.setCustomValidity(msg) // Tooltip visual destroyTooltip(tipRef.current) const tip = makeTooltip() tip.textContent = msg positionTooltipNear(field, tip) tipRef.current = tip currentFieldRef.current = field } const hide = () => { destroyTooltip(tipRef.current) tipRef.current = null currentFieldRef.current = null } const onInput = (e: Event) => { const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement field.setCustomValidity("") // limpa assim que o usuário interage hide() } const onInvalid = (e: Event) => { e.preventDefault() // para a bolha do Chrome const field = e.target as | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement show(field) } const onSubmit = (e: Event) => { hide() // Força a checagem de validade antes de submeter const isValid = formEl.checkValidity() if (!isValid) { e.preventDefault() // Para o submit e.stopPropagation() // Para a propagação // Foca no primeiro campo inválido const firstInvalid = Array.from(allFields).find( (f) => !f.checkValidity() ) if (firstInvalid) { firstInvalid.focus({ preventScroll: false }) show(firstInvalid) } return false } } // Define handlers que podem ser removidos adequadamente const handleResize = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } const handleScroll = () => { if (tipRef.current && currentFieldRef.current) { positionTooltipNear(currentFieldRef.current, tipRef.current) } } // Conecta os eventos allFields.forEach((f) => { f.addEventListener("invalid", onInvalid) f.addEventListener("input", onInput) f.addEventListener("blur", onInput) }) formEl.addEventListener("submit", onSubmit) window.addEventListener("resize", handleResize) window.addEventListener("scroll", handleScroll, true) return () => { hide() allFields.forEach((f) => { f.removeEventListener("invalid", onInvalid) f.removeEventListener("input", onInput) f.removeEventListener("blur", onInput) }) formEl.removeEventListener("submit", onSubmit) window.removeEventListener("resize", handleResize) window.removeEventListener("scroll", handleScroll, true) } }, []) // Anexa a qualquer elemento dentro do seu formulário (ex: o frame ou wrapper do form) return { ref: (node: HTMLElement | null) => { ref.current = node }, } }
Personalize
Você só precisa mexer em dois pontos dentro da função makeTooltip():
background: "#0055ff", // cor de fundo da tooltip color: "#fff", // cor do texto dentro da tooltip
background: "#0055ff", // cor de fundo da tooltip color: "#fff", // cor do texto dentro da tooltip
background: "#0055ff", // cor de fundo da tooltip color: "#fff", // cor do texto dentro da tooltip
background: "#0055ff", // cor de fundo da tooltip color: "#fff", // cor do texto dentro da tooltip
Você pode usar:
Nomes:
red,blue,black,whiteHexadecimal:
#000000(preto),#ffffff(branco)RGB:
rgb(0, 85, 255)
Dica: garanta contraste suficiente entre background e color para leitura fácil. Em geral, texto claro em fundo escuro vice-versa.
Mensagens de erro
Você também pode alterar as mensagens de erro por campo. Encontre esse bloco no início do código e altere o texto, sem deletar nenhum outro elemento:
// ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", }
// ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", }
// ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", }
// ---- Altere se necessário ---- const DEFAULT_MESSAGES = { valueMissing: "Por favor, preencha este campo.", typeMismatchEmail: "Digite um e-mail válido.", typeMismatchUrl: "Digite uma URL válida.", patternMismatch: "O formato não corresponde ao esperado.", tooShort: "O valor está muito curto.", tooLong: "O valor está muito longo.", rangeUnderflow: "O valor é muito baixo.", rangeOverflow: "O valor é muito alto.", stepMismatch: "O valor não corresponde ao passo definido.", badInput: "Valor inválido.", generic: "Verifique este campo.", }
Aplique o override
Siga os passos abaixo para aplicar o efeito no seu formulário:
Selecione a camada de texto que está conectada a um campo do CMS.
Vá para a aba "Code" > "Overrides".
Escolha o arquivo de código criado.
Aplique o override "FormWithCustomTooltips"
Pronto! As mensagens de validação do navegador serão substituídas pelo seu tooltip personalizado.

Design e performance juntos
Crie sua conta gratuita e construa sites profissionais com o Framer, sem código.

Design e performance juntos
Crie sua conta gratuita e construa sites profissionais com o Framer, sem código.
