Pacote React
Coloque o Opini direto no seu app React. Quatro níveis de estilo, do pronto para usar ao totalmente sem interface. Inputs comuns, sem componentes de campo: seu layout continua seu.
Instalar
# pnpm
pnpm add @opini-dev/react
# npm
npm install @opini-dev/reactRequer React 18 ou 19 como peer dependency. Só ESM. Seguro para renderização no servidor: os portais do modo flutuante só montam depois do primeiro efeito no cliente.
Nível 1: pronto para usar
Um componente. Os estilos padrão já vêm ligados, então ele parece um widget de feedback de cara.
import { OpiniWidget } from "@opini-dev/react"
export default function App() {
return <OpiniWidget projectKey="pk_live_xxx" />
}Passe unstyled se quiser começar do zero.
Nível 2: classes por parte
Aplique classes do Tailwind, CSS Modules ou CSS puro às partes estruturais. O estilo dos campos fica no HTML do seu formulário (veja o Nível 3).
<OpiniWidget
projectKey="pk_live_xxx"
unstyled
className="fixed bottom-6 right-6"
classNames={{
trigger: "rounded-full bg-black px-5 py-2.5 text-sm text-white",
popover: "w-96 rounded-2xl border bg-white p-5 shadow-xl",
submit: "rounded-md bg-violet-600 px-3 py-2 text-white hover:bg-violet-700",
}}
/>Para transições só com CSS, cada parte expõe data-state="open|closed" e data-loading="true|false". Quem usa Tailwind pode ligar entrada e saída com data-[state=open]:scale-100.
Nível 3: peças combináveis
Use seu próprio botão, seus textos e seu layout de campos. <input> e <textarea> comuns com name="message", "email" ou "name" são o contrato dos campos.
import {
OpiniWidgetRoot,
OpiniTrigger,
OpiniPopover,
OpiniForm,
OpiniSubmit,
OpiniSuccess,
} from "@opini-dev/react"
<OpiniWidgetRoot projectKey="pk_live_xxx">
<OpiniTrigger asChild>
<MyBrandedButton>Send feedback →</MyBrandedButton>
</OpiniTrigger>
<OpiniPopover className="my-popover">
<h2>Pitch a chapter.</h2>
<OpiniForm className="space-y-3">
<textarea
name="message"
placeholder="Albums that auto-curate themselves on a trip…"
/>
<input name="name" placeholder="name (optional)" />
<input name="email" type="email" placeholder="email" />
<OpiniSubmit asChild>
{({ status }) => (
<MyBrandedButton variant="primary" disabled={status === "submitting"}>
{status === "submitting" ? "Sending…" : "SEND IT →"}
</MyBrandedButton>
)}
</OpiniSubmit>
</OpiniForm>
<OpiniSuccess>You'll hear back. Promise.</OpiniSuccess>
</OpiniPopover>
</OpiniWidgetRoot>Nível 4: useOpiniSubmit (sem interface)
Quando você quer o cliente da API e a máquina de estados sem nenhuma interface.
import { useOpiniSubmit } from "@opini-dev/react"
function MyOwnForm() {
const { submit, status, error } = useOpiniSubmit({ projectKey: "pk_live_xxx" })
return (
<form
onSubmit={(e) => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
void submit({
text: String(fd.get("message") ?? ""),
email: fd.get("email") ? String(fd.get("email")) : undefined,
})
}}
>
<textarea name="message" />
<input name="email" />
<button disabled={status === "submitting"}>Send</button>
{error ? <p>{error.message}</p> : null}
</form>
)
}Props
Todas as props são tipadas, então seu editor as lista enquanto você digita. Não existe a prop customCss: esse campo no painel vale só para o widget por tag de script. No React você estiliza com o que já usa.
Nomes das partes
A lista completa: trigger, popover, arrow, close, form, actions, submit, success, msg. Cada parte também tem um atributo data-opini-part="…" para quem usa CSS-in-JS.
Contrato dos campos
<OpiniForm> lê o formulário com FormData ao enviar. São aceitos exatamente três campos:
name="message": obrigatório, o texto do feedback.name="email": opcional, validado quando presente.name="name": opcional, texto livre.
Todo o resto é descartado antes do POST, então você pode adicionar seus próprios campos de estado sem poluir o envio.
SSR
O conteúdo dos portais do modo flutuante só renderiza depois do primeiro efeito no cliente; não há saída de portal no HTML inicial. A hidratação não faz nada. O modo inline renderiza direto na sua árvore, no servidor e no cliente.