npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

popup-flutuante-react

v0.1.0

Published

Componente e hook React pra colocar qualquer conteudo numa janela picture-in-picture flutuante.

Downloads

19

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 é

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 dev

Abre 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 Window da 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 public

Em qualquer outro projeto:

npm install @seu-usuario/popup-flutuante-react
import { 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:

  1. Edite "name" no package.json para um nome único (ex: @seu-usuario/popup-flutuante-react)
  2. Ajuste "version" conforme necessário
  3. Execute:
npm login       # apenas na primeira vez
npm publish --access public

Estrutura 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.json

O código da biblioteca inteira está em src/PictureInPicture.jsx (~150 linhas). O restante é apenas a demo.