@bradlet/floaty
v0.1.0
Published
Tiny, zero-dependency React library for draggable, resizable floating modal windows with a dead-simple open/close API.
Maintainers
Readme
@bradlet/floaty
Tiny, zero-dependency React library for draggable, resizable floating modal windows with a dead-simple open/close API.
- 🪶 Zero runtime dependencies — React/ReactDOM are the only peer deps. No styled-components, no CSS import.
- 🎛️ Simple state API — one
useModal()hook per window:{ isOpen, open, close, toggle }. - 🖱️ Draggable & resizable — pointer-event based, works with mouse and touch (Safari included).
- 💾 Persistent — size, position, and your children's state survive close → reopen.
- 🎨 Fully restyleable —
className/style/headerStyle/bodyStyleescape hatches.
Install
npm install @bradlet/floatyReact 18+ is a peer dependency.
Quickstart
import { useModal, Modal } from '@bradlet/floaty';
function App() {
const chat = useModal(); // starts closed
const players = useModal(true); // starts open
return (
<>
<button onClick={chat.toggle}>Chat {chat.isOpen ? '(open)' : ''}</button>
<button onClick={players.toggle}>Players</button>
<Modal isOpen={chat.isOpen} onClose={chat.close} title="Chat">
<ChatPanel />
</Modal>
<Modal isOpen={players.isOpen} onClose={players.close} title="Players">
<PlayerList />
</Modal>
</>
);
}Each useModal() call is fully independent, so you can create as many windows as you want, each
with its own open / close / toggle and readable isOpen.
Resizing & how children reflow
Modals are resizable by default via a corner grip. The size persists across open/close (the window
stays mounted while hidden). The content area is a flex container with flex-wrap by default so
children reflow as the window grows or shrinks. Control that with bodyStyle / bodyClassName:
<Modal isOpen={m.isOpen} onClose={m.close} title="Gallery"
defaultSize={{ width: 360, height: 280 }}
minWidth={200}
minHeight={160}
bodyStyle={{ gap: 8, padding: 12 }} // merged over the default flex + flex-wrap
>
{items.map((it) => <Thumb key={it.id} {...it} />)}
</Modal>Want children laid out in a column instead? bodyStyle={{ flexDirection: 'column' }}.
Restyling
@bradlet/floaty ships a neutral default look (injected once at runtime — nothing to import) and
exposes per-part escape hatches:
<Modal
isOpen={m.isOpen}
onClose={m.close}
title="Custom"
className="my-window"
style={{ background: '#232a38', color: '#fff' }}
headerStyle={{ background: 'linear-gradient(180deg,#313a4d,#232a38)' }}
bodyStyle={{ padding: 16 }}
>
...
</Modal>Or target the stable class names (also exported as FLOATY_CLASS):
floaty-window, floaty-header, floaty-title, floaty-close, floaty-body, floaty-resize-handle.
API
useModal(initialOpen?: boolean): ModalControls
Returns { isOpen, open, close, toggle }. Actions are referentially stable across renders.
<Modal> props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| isOpen | boolean | — | Visibility. Wire to useModal().isOpen. |
| onClose | () => void | — | Called by the × button. Wire to useModal().close. |
| title | ReactNode | — | Title bar content; also the drag handle. |
| defaultPosition | { x, y } | { x: 40, y: 40 } | Initial top-left position. |
| defaultSize | { width, height } | { width: 400, height: 300 } | Initial size. |
| minWidth / minHeight | number | 160 / 120 | Resize minimums. |
| draggable | boolean | true | Drag by the title bar. |
| resizable | boolean | true | Resize from the corner grip. |
| showCloseButton | boolean | true | Render the × button. |
| closeLabel | string | "Close" | Accessible label for the × button. |
| unmountOnClose | boolean | false | Unmount when closed instead of hiding. |
| className / style | — | — | Container overrides. |
| headerClassName / headerStyle | — | — | Title bar overrides. |
| bodyClassName / bodyStyle | — | { display: 'flex', flexWrap: 'wrap' } | Content area overrides. |
Advanced: the useDraggable and useResizable hooks are also exported for building custom chrome.
Playground
A standalone Vite app for manual testing lives in playground/ (never published to npm):
npm install
cd playground && npm install && npm run devEditing library source under src/ hot-reloads in the playground.
License
MIT © Bradley Thompson
