popup-flutuante-react
v0.1.0
Published
Componente e hook React pra colocar qualquer conteudo numa janela picture-in-picture flutuante.
Downloads
19
Maintainers
Readme
popup-flutuante-react
Componente e hook React para colocar qualquer pedaço do seu app numa janela flutuante sempre por cima de tudo, usando a API nativa Document Picture-in-Picture do navegador. Sem Electron, sem extensão, sem dependências além do próprio React.
Sumário
- O que é
- Demo
- Compatibilidade
- Instalação
- API
- Exemplos de uso
- Casos de uso comuns
- Como funciona por baixo dos panos
- Publicar no npm
- Estrutura do projeto
O que é
popup-flutuante-react é uma biblioteca minimalista que expõe dois primitivos React — um componente declarativo e um hook imperativo — para abrir uma janela flutuante nativa do sistema operacional diretamente a partir do navegador.
A janela é criada pela Document Picture-in-Picture API (Chrome 116+), que dá ao navegador a capacidade de abrir uma janela do SO, sempre visível por cima das outras janelas, sem precisar de nenhum plugin. O conteúdo renderizado dentro dela continua sendo gerenciado pelo mesmo React, com os mesmos estados, contextos e hooks — porque usa ReactDOM.createPortal no documento da nova janela.
Diferença em relação a outras abordagens
| Abordagem | Dependências | Estado React compartilhado | Sempre por cima |
|---|---|---|---|
| Este projeto | Nenhuma | Sim (portal) | Sim (nativo SO) |
| Electron | Electron (~200 MB) | Não (IPC) | Sim |
| Extensão de navegador | Instalação pelo usuário | Não | Sim |
| window.open + iframe | Nenhuma | Não | Não |
| Modal / drawer | Nenhuma | Sim | Apenas dentro da aba |
Demo
git clone <repositorio>
cd popup-flutuante-react
npm install
npm run devAbre em http://localhost:5173. A demo inclui dois exemplos lado a lado:
- Exemplo declarativo — contador ao vivo que continua incrementando dentro da janela flutuante, demonstrando que o estado React é preservado.
- Exemplo imperativo — controle manual via hook, com acesso direto ao objeto
Windowda janela aberta.
Funciona melhor no Chrome, Edge, Brave ou Opera (versões recentes para desktop).
Compatibilidade
| Navegador | Suporte | |---|---| | Chrome 116+ | Sim | | Edge (Chromium) | Sim | | Brave | Sim | | Opera | Sim | | Firefox | Nao (API nao implementada) | | Safari | Nao (API nao implementada) |
A propriedade isSupported do hook detecta em tempo de execucão se o navegador suporta a API, permitindo mostrar um aviso ou fallback ao usuário.
Instalação
Opção A — Copiar o arquivo (mais rápido)
Copie src/PictureInPicture.jsx para dentro do seu projeto React e importe diretamente:
import { PictureInPicture, usePictureInPicture } from './PictureInPicture';Nenhuma dependência adicional necessária — só React e ReactDOM (que você já tem).
Opção B — Publicar e instalar via npm
Se quiser reutilizar em vários projetos via npm install:
# 1. No package.json deste projeto, troque "name" por algo único,
# por exemplo "@seu-usuario/popup-flutuante-react"
# 2. Faça login no npm (apenas uma vez)
npm login
# 3. Publique
npm publish --access publicEm qualquer outro projeto:
npm install @seu-usuario/popup-flutuante-reactimport { PictureInPicture, usePictureInPicture } from '@seu-usuario/popup-flutuante-react';API
Componente <PictureInPicture>
Abordagem declarativa, controlada por props. Ideal para a maioria dos casos.
import { useState } from 'react';
import { PictureInPicture } from './PictureInPicture';
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Abrir janela flutuante</button>
<PictureInPicture
isOpen={open}
onClose={() => setOpen(false)}
width={400}
height={300}
>
<h1>Conteúdo flutuante</h1>
<p>Esta janela fica por cima de tudo, sempre visível.</p>
</PictureInPicture>
</>
);
}Props
| Prop | Tipo | Obrigatório | Default | Descrição |
|---|---|---|---|---|
| isOpen | boolean | Sim | — | Controla se a janela está aberta. Quando passa de false para true, abre a janela; quando volta para false, fecha. |
| onClose | () => void | Sim | — | Chamado quando o usuário fecha a janela pelo botão X do sistema operacional. Use para sincronizar o estado do seu componente pai. |
| width | number | Não | 400 | Largura inicial da janela em pixels. O usuário pode redimensioná-la depois. |
| height | number | Não | 300 | Altura inicial da janela em pixels. |
| children | ReactNode | Sim | — | Qualquer conteúdo React. Renderizado com createPortal na janela flutuante, mantendo acesso ao mesmo contexto, estado e hooks do app principal. |
Hook usePictureInPicture
Abordagem imperativa com controle total. Use quando precisar acessar o objeto Window diretamente, ou quando a lógica de abertura/fechamento precisar ser desacoplada da árvore de componentes.
import { usePictureInPicture } from './PictureInPicture';
function PlayerControls() {
const { open, close, isOpen, isSupported, pipWindow } = usePictureInPicture();
if (!isSupported) {
return <p>Seu navegador não suporta janelas flutuantes.</p>;
}
async function handleOpen() {
const win = await open({ width: 500, height: 400 });
win.document.title = 'Player flutuante';
}
return (
<button onClick={isOpen ? close : handleOpen}>
{isOpen ? 'Fechar janela' : 'Abrir janela flutuante'}
</button>
);
}Retorno do hook
| Campo | Tipo | Descrição |
|---|---|---|
| isSupported | boolean | true se o navegador implementa a Document Picture-in-Picture API. Verifique antes de tentar abrir. |
| isOpen | boolean | true se a janela flutuante está aberta no momento. |
| pipWindow | Window \| null | Referência direta ao objeto Window da janela flutuante, ou null se fechada. Permite manipulações avançadas (ex: definir document.title, acessar document.body). |
| open | (opts?: { width?: number; height?: number }) => Promise<Window> | Abre a janela flutuante. Retorna uma Promise que resolve com o objeto Window criado. Se a janela já estiver aberta, retorna a existente. |
| close | () => void | Fecha a janela flutuante programaticamente. |
Exemplos de uso
Contador ao vivo com estado compartilhado
O estado count é o mesmo tanto na aba principal quanto na janela flutuante — sem sincronização manual, sem eventos, sem contexto adicional.
import { useState, useEffect } from 'react';
import { PictureInPicture } from './PictureInPicture';
function LiveCounter() {
const [open, setOpen] = useState(false);
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<>
<button onClick={() => setOpen(o => !o)}>
{open ? 'Fechar' : 'Destacar contador'}
</button>
<PictureInPicture isOpen={open} onClose={() => setOpen(false)} width={300} height={200}>
<div style={{ fontSize: 64, textAlign: 'center', padding: 32 }}>
{count}s
</div>
</PictureInPicture>
</>
);
}Verificação de suporte antes de renderizar
import { usePictureInPicture } from './PictureInPicture';
function FloatButton() {
const { isSupported, open, close, isOpen } = usePictureInPicture();
if (!isSupported) return null; // esconde o botão em navegadores sem suporte
return (
<button onClick={isOpen ? close : () => open({ width: 400, height: 300 })}>
{isOpen ? 'Fechar janela' : 'Abrir em janela flutuante'}
</button>
);
}Manipulação direta da janela via pipWindow
import { useEffect } from 'react';
import { usePictureInPicture } from './PictureInPicture';
function AdvancedFloat() {
const { open, pipWindow, isOpen } = usePictureInPicture();
async function handleOpen() {
const win = await open({ width: 600, height: 400 });
win.document.title = 'Dashboard ao vivo';
win.document.body.style.background = '#111';
}
return <button onClick={handleOpen}>Abrir dashboard flutuante</button>;
}Casos de uso comuns
| Caso de uso | Descrição | |---|---| | Player de vídeo flutuante | O usuário navega no resto do site enquanto o vídeo continua tocando em uma janela separada sempre visível. | | Chat / suporte ao cliente | Janela de mensagens que não desaparece quando o usuário troca de aba ou minimiza o site. | | Dashboard de monitoramento | Métricas, gráficos e alertas sempre visíveis enquanto o usuário trabalha em outra janela. | | Timer / pomodoro | Contagem regressiva que acompanha o usuário independente do que ele estiver fazendo na tela. | | Anotações rápidas | Bloco de notas flutuante que persiste enquanto o usuário navega entre páginas do app. | | Reprodução de podcast/rádio | Controles de áudio que ficam acessíveis sem ocupar espaço na aba principal. | | Cockpit de trading | Preços ao vivo e ordens rápidas sempre visíveis sem dedicar um monitor inteiro para isso. |
Como funciona por baixo dos panos
1. Abertura da janela
Quando open() é chamado (ou isOpen passa para true), o componente chama:
window.documentPictureInPicture.requestWindow({ width, height })O navegador cria uma nova janela nativa do SO, sem barra de endereços e sem abas, que fica sempre posicionada acima de todas as outras janelas. Isso é garantido pelo SO — não tem como outra janela cobrir.
2. Injeção de estilos
Sem intervenção, o CSS do seu app não existiria na nova janela. O componente resolve isso copiando automaticamente todas as <link rel="stylesheet"> e <style> do documento principal para o documento da janela flutuante. Para folhas de estilo protegidas por CORS, cria um <link> apontando para a URL original.
Além disso, replica estilos computados do <body> (background, color, font-family, margin) para garantir consistência visual.
3. Renderização via portal
O conteúdo passado como children é renderizado com:
ReactDOM.createPortal(children, pipWindow.document.body)Isso significa que o React renderiza na janela flutuante, mas dentro da mesma árvore de componentes. O children tem acesso a qualquer Context, estado, hook ou ref que teria na aba principal — sem nenhuma sincronização manual.
4. Detecção de fechamento externo
O usuário pode fechar a janela pelo botão X do SO. O componente escuta o evento pagehide na janela flutuante para detectar isso e disparar o callback onClose, mantendo o estado do app sincronizado.
5. Fluxo completo
isOpen = true
│
▼
requestWindow() ──► Nova janela nativa do SO (sempre por cima)
│
▼
copyStylesTo(pipWindow) ──► CSS do app copiado para a janela
│
▼
createPortal(children, pipWindow.document.body) ──► React renderiza na janela
│
▼
Usuário fecha janela ──► evento "pagehide" ──► onClose()Publicar no npm
O package.json já está configurado com "main" apontando para src/PictureInPicture.jsx e "files" incluindo apenas esse arquivo. Para publicar:
- Edite
"name"nopackage.jsonpara um nome único (ex:@seu-usuario/popup-flutuante-react) - Ajuste
"version"conforme necessário - Execute:
npm login # apenas na primeira vez
npm publish --access publicEstrutura do projeto
popup-flutuante-react/
├── src/
│ ├── PictureInPicture.jsx # Biblioteca — componente + hook (arquivo único)
│ ├── App.jsx # App de demonstração
│ ├── App.css # Estilos do app de demonstração
│ └── main.jsx # Entry point React
├── index.html
├── vite.config.js
└── package.jsonO código da biblioteca inteira está em src/PictureInPicture.jsx (~150 linhas). O restante é apenas a demo.
