iOS-style draggable, resizable floating container for React & Next.js. YouTube mini-player inspired picture-in-picture component with momentum physics, dock-to-edge, and glassmorphism styling.
npm install glide-frame

A YouTube mini-player inspired draggable and resizable floating container component for Next.js 16. Create picture-in-picture style floating windows that persist while users navigate your site.
- š±ļø Draggable - Drag from header to reposition anywhere on screen
- š Resizable - Resize from edges and corners with smooth animations
- š± Mobile First - Full touch support with responsive design
- š iOS-Style Momentum - Physics-based throwing with velocity and friction
- šÆ Dock to Edge - Swipe to edge to minimize, tap handle to restore
- šÆ Multi-Instance - Multiple frames with automatic z-index management
- š¾ Persistent State - Position and size saved to localStorage
- ⨠Glassmorphism - Modern blur backdrop with beautiful styling
- š Dark Mode - Full support for light/dark themes via shadcn/ui
- ā” 60 FPS - Hardware-accelerated animations for smooth performance
- š§ Fully Typed - Complete TypeScript support with exported types
- š„ Stateful Detach - Pop-out iframe/video without reloading (preserves state)
``bashInstall dependencies
pnpm add react-rnd lucide-react
š Quick Start
$3
`tsx
import { GlideFrame } from "@/components/glide-frame";function App() {
const [isOpen, setIsOpen] = useState(true);
if (!isOpen) return null;
return (
id="my-frame"
title="My Floating Window"
defaultPosition={{ x: 100, y: 100 }}
defaultSize={{ width: 480, height: 320 }}
onClose={() => setIsOpen(false)}
>
{/ Any content: iframe, video, React components /}
src="https://example.com"
className="w-full h-full border-0"
/>
);
}
`$3
Use
GlideFrameProvider in your layout to keep frames visible while navigating:`tsx
// app/layout.tsx
import { GlideFrameProvider } from "@/components/glide-frame";export default function RootLayout({ children }) {
return (
{children}
);
}// Any page component
import { useGlideFrameContext } from "@/components/glide-frame";
function MyPage() {
const { openFrame, closeFrame } = useGlideFrameContext();
const handleOpenVideo = () => {
openFrame({
id: "video-player",
title: "Video Player",
content: ,
defaultSize: { width: 480, height: 320 },
headerStyle: { backgroundColor: "#dc2626", buttonColor: "#fff" },
});
};
return ;
}
`š API Reference
$3
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
id | string | required | Unique identifier for the frame instance |
| title | string | undefined | Title displayed in the header bar |
| defaultPosition | { x: number, y: number } | Top-right corner | Initial position on screen |
| defaultSize | { width: number, height: number } | 800x600 | Initial dimensions |
| minSize | { width: number, height: number } | 400x300 (desktop) / 280x200 (mobile) | Minimum resize constraints |
| maxSize | { width: number, height: number } | Screen size - 40px | Maximum resize constraints |
| onClose | () => void | undefined | Callback when close button is clicked |
| onStateChange | (state: GlideFrameState) => void | undefined | Callback when state changes |
| persist | boolean | true | Whether to persist position/size to localStorage |
| className | string | undefined | Additional CSS classes for the container |
| children | ReactNode | undefined | Content to render inside the frame |$3
The
onStateChange callback receives a state object with:`typescript
interface GlideFrameState {
position: { x: number; y: number };
size: { width: number; height: number };
isMinimized: boolean;
isMaximized: boolean;
isDocked: boolean;
dockedSide: 'left' | 'right' | null;
isVisible: boolean;
zIndex: number;
}
`š® Controls & Interactions
$3
| Button | Action |
|--------|--------|
| ā” | Maximize to fullscreen |
| āŗ | Restore from maximized/docked state |
| Ć | Close the frame |
$3
- Drag Header - Move the frame around
- Double-click/tap Header - Toggle maximize
- Throw to Edge - Momentum-based dock (swipe fast toward edge)
- Tap Dock Handle - Restore from docked state
- Resize Edges/Corners - Resize the frame
$3
- Frame receives focus on interaction for accessibility
šØ Customization
$3
`tsx
id="styled-frame"
title="Custom Header"
headerStyle={{
backgroundColor: "#dc2626", // Background color or gradient
textColor: "#ffffff", // Title text color
buttonColor: "#ffffff", // Icon button color
buttonHoverColor: "#ffcccc", // Button hover color
height: 40, // Header height in pixels
showMaximize: true, // Show/hide maximize button
showClose: true, // Show/hide close button
}}
>
`$3
`tsx
id="styled-frame"
title="Custom Frame"
frameStyle={{
backgroundColor: "#1e293b", // Frame background color
borderColor: "#dc2626", // Border color
borderWidth: 2, // Border width in pixels
borderRadius: 12, // Corner radius in pixels
boxShadow: "0 0 30px rgba(0,0,0,0.3)", // Custom shadow
}}
>
`$3
`tsx
id="video-player"
title="Video Player"
headerStyle={{
backgroundColor: "linear-gradient(90deg, #f59e0b, #ef4444)",
textColor: "#fff",
buttonColor: "#fff",
height: 36,
}}
frameStyle={{
borderRadius: 16,
boxShadow: "0 0 30px rgba(245, 158, 11, 0.3)",
}}
>
`$3
Convert any inline content (iframe, video, component) to a floating window without losing state:
`tsx
import { DetachableContent } from "@/components/glide-frame";function Page() {
return (
id="video-player"
title="YouTube Video"
headerStyle={{ backgroundColor: "#dc2626", buttonColor: "#fff" }}
frameStyle={{ borderRadius: 12, borderColor: "#dc2626", borderWidth: 2 }}
>
{/ iframe won't reload when detached! /}
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
className="w-full aspect-video"
allowFullScreen
/>
);
}
`How it works:
- Hover over content ā pop-out button appears
- Click pop-out ā content floats without reloading
- Placeholder shows where content was
- Click "Restore here" or close ā content returns to original position
This is perfect for:
- š„ Video players that shouldn't restart
- š® Games with state (canvas, WebGL)
- š Live dashboards with WebSocket connections
- š Forms with user input
$3
Adjust the physics constants in
types.ts:`typescript
export const MOMENTUM_FRICTION = 0.92; // 0-1, higher = slides further
export const MOMENTUM_MIN_VELOCITY = 0.5; // Stop threshold
export const MOMENTUM_MULTIPLIER = 8; // Velocity amplification
export const DOCK_MIN_VELOCITY = 2; // Min speed to trigger dock
`š Project Structure
`text
components/glide-frame/
āāā GlideFrame.tsx # Main component with react-rnd integration
āāā GlideFrameHeader.tsx # Header bar with control buttons
āāā GlideFrameProvider.tsx # Context provider for persistent frames
āāā DetachableContent.tsx # Stateful pop-out wrapper (preserves iframe state)
āāā types.ts # TypeScript interfaces and constants
āāā index.ts # Public exports
āāā hooks/
āāā useGlideFrame.ts # State management hook with localStorage
`š ļø Tech Stack
| Technology | Purpose |
|------------|---------|
| Next.js 16 | React framework with App Router |
| React 19 | UI library |
| TypeScript | Type safety |
| react-rnd | Drag and resize functionality |
| shadcn/ui | UI components and theming |
| Tailwind CSS 4 | Styling |
| Lucide React | Icons |
š» Development
`bash
Clone the repository
git clone https://github.com/atknatk/glide-frame.git
cd glide-frameInstall dependencies
pnpm installStart development server
pnpm devBuild for production
pnpm buildRun linting
pnpm lint
`š Deployment
This project uses GitHub Actions for automatic deployment to GitHub Pages.
Every push to
main triggers:1. Install dependencies
2. Build the Next.js application
3. Deploy to GitHub Pages
š License
MIT License - see LICENSE for details.
š¤ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add amazing feature')
4. Push to the branch (git push origin feature/amazing-feature`)