├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.html ├── lib ├── components │ └── ResponsiveIframeViewer │ │ ├── consts.tsx │ │ └── index.tsx ├── main.ts └── vite-env.d.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── index.css ├── main.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig-build.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vite.website.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore all files and directories by default 2 | * 3 | 4 | # Allow the inclusion of necessary files and directories 5 | !package.json 6 | !README.md 7 | !LICENSE 8 | !dist/** 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dan Mindru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot showing responsive iframe viewer demo](https://github.com/danmindru/react-responsive-iframe-viewer/assets/1515742/cbc09f80-f9a7-4a3a-ba23-1dd8b9b7fe47) 2 | 3 | ![Screenshot showing multiple demos, including dark mode](https://github.com/danmindru/react-responsive-iframe-viewer/assets/1515742/94a50b53-0344-4b91-bec6-1c4d33034f9b) 4 | 5 | 6 | # React Responsive Iframe Viewer 7 | [![npm version](https://badge.fury.io/js/react-responsive-iframe-viewer.svg)](https://badge.fury.io/js/react-responsive-iframe-viewer) 8 | 9 | View iframe content in a responsive container that can: 10 | 11 | - Switch between common devices sizes 12 | - Mobile 13 | - Tablet 14 | - Desktop 15 | - Resize using the provided handles 16 | - ✨ all animated & pretty 17 | - 🌚 and with dark mode support 18 | 19 | 20 | [Demo 🚀](https://react-responsive-iframe-viewer.vercel.app/) / [Usage examples 👨‍💻](https://github.com/danmindru/react-responsive-iframe-viewer/blob/main/src/App.tsx) 21 | 22 | ## Getting started 23 | 24 | 25 | ### Install 26 | Grab the package from npm: 27 | 28 | ```sh 29 | npm install react-responsive-iframe-viewer 30 | ``` 31 | 32 | ### Setup styles 33 | 34 | #### With TailwindCSS 35 | If you use TailwindCSS, you need to mark this package as content: 36 | 37 | **tailwind.config.js** 38 | 39 | ```js 40 | module.exports = { 41 | content: [ 42 | + 'node_modules/react-responsive-iframe-viewer/**/*.{js,ts,jsx,tsx,html}', 43 | ... 44 | ] 45 | } 46 | ``` 47 | 48 | Dark mode is supported out of the box for TailwindCSS. 49 | 50 | #### Without TailwindCSS 51 | If you don't use TailwindCSS, you can import the styles directly: 52 | 53 | ```tsx 54 | import 'react-responsive-iframe-viewer/dist/style.css' 55 | ``` 56 | 57 | ## Usage 58 | 59 | ```tsx 60 | import { ResponsiveIframeViewer, ViewportSize } from 'react-responsive-iframe-viewer'; 61 | 62 | 67 | ``` 68 | 69 | ## Options & Props 70 | `src` - The URL of the iframe content
71 | `title` - The title of the iframe content
72 | `size` - The size of the iframe container
73 | `minWidth` - The minimum width to resize down to (**default: 200**)
74 | `minHeight` - The minimum height to resize down to (**default: 200**)
75 | `showControls` - Whether to show device controls or not (**default: true**)
76 | `enabledControls` - An array of controls to enable (**default: [ViewportSize.mobile, ViewportSize.tablet, ViewportSize.desktop, ViewportSize.fluid]**)
77 | `allowResizingY` - Whether to allow resizing the iframe container vertically (**default: false**)
78 | `allowResizingX` - Whether to allow resizing the iframe container horizontally (**default: false**)
79 | `fluidX` - Forces the width to 100% regardless of other settings (**default: false**)
80 | `fluidY` - Forces the height to 100% regardless of other settings (**default: false**)
81 | `onIframeLoad` - Event handler called when the iframe content has finished loading
82 | 83 | 84 | ### Custom sizes 85 | 86 | You can provide a custom width/height for the iframe container: 87 | - `width` - The width of the iframe container 88 | - `height` - The height of the iframe container 89 | 90 | The `size` prop will be ignored if `width` and `height` are provided. 91 | 92 | ```tsx 93 | 99 | ``` 100 | 101 | ## Custom controls 102 | 103 | It is possible to only show a subset of the available viewport toggles by passing in a list of enabled controls: 104 | 105 | ```tsx 106 | import { ResponsiveIframeViewer, ViewportSize } from "../lib/main"; 107 | 108 | 115 | ``` 116 | 117 | ## Supported sizes 118 | 119 | ```tsx 120 | export const VIEWPORT_SIZES = { 121 | miniMobile: { 122 | width: 320, 123 | height: 480, 124 | }, 125 | mobile: { 126 | width: 375, 127 | height: 667, 128 | }, 129 | tablet: { 130 | width: 768, 131 | height: 1024, 132 | }, 133 | desktop: { 134 | width: 1024, 135 | height: 768, 136 | }, 137 | fluid: { 138 | width: "100%", 139 | height: "100%", 140 | }, 141 | 142 | // Tailwind Viewports 143 | sm: { 144 | width: 640, 145 | height: 1136, 146 | }, 147 | 148 | md: { 149 | width: 768, 150 | height: 1024, 151 | }, 152 | 153 | lg: { 154 | width: 1024, 155 | height: 768, 156 | }, 157 | 158 | xl: { 159 | width: 1280, 160 | height: 720, 161 | }, 162 | 163 | "2xl": { 164 | width: 1536, 165 | height: 864, 166 | }, 167 | 168 | "3xl": { 169 | width: 1920, 170 | height: 1080, 171 | }, 172 | }; 173 | ``` 174 | 175 | ![Screenshot showing a nice demo with a shadow](https://github.com/danmindru/react-responsive-iframe-viewer/assets/1515742/aa130a18-9997-4dfd-a607-1e3c65c4840c) 176 | 177 | ----------------- 178 | 179 | 180 | Apihustle Logo 181 | 182 | 183 | ----------------- 184 | 185 | Save 10s of hours of work by using Shipixen to generate a customized codebases with your branding, pages and blog
186 | ― then deploy it to Vercel with 1 click. 187 | 188 | | | | 189 | | :- | :- | 190 | | Shipixen Logo
Shipixen
Create a blog & landing page in minutes with Shipixen.
Try the app on shipixen.com. | Shipixen Logo | 191 | 192 | ----------------- 193 | 194 | Apihustle is a collection of tools to test, improve and get to know your API inside and out.
195 | [apihustle.com](https://apihustle.com)
196 | 197 | | | | | | 198 | | :- | :- | :- | :- | 199 | | Shipixen Logo | **Shipixen** | Create a personalized blog & landing page in minutes | [shipixen.com](https://shipixen.com) | 200 | | Page UI Logo | **Page UI** | Landing page UI components for React & Next.js | [pageui.dev](https://pageui.dev) | 201 | | Clobbr Logo | **Clobbr** | Load test your API endpoints. | [clobbr.app](https://clobbr.app) | 202 | | Crontap Logo | **Crontap** | Schedule API calls using cron syntax. | [crontap.com](https://crontap.com) | 203 | | CronTool Logo | **CronTool** | Debug multiple cron expressions on a calendar. | [tool.crontap.com](https://tool.crontap.com) | 204 | 205 | ----------------- 206 | 207 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/components/ResponsiveIframeViewer/consts.tsx: -------------------------------------------------------------------------------- 1 | export const VIEWPORT_SIZES = { 2 | miniMobile: { 3 | width: 320, 4 | height: 480, 5 | }, 6 | mobile: { 7 | width: 375, 8 | height: 667, 9 | }, 10 | tablet: { 11 | width: 768, 12 | height: 1024, 13 | }, 14 | desktop: { 15 | width: 1024, 16 | height: 768, 17 | }, 18 | fluid: { 19 | width: "100%", 20 | height: "100%", 21 | }, 22 | 23 | // Tailwind Viewports 24 | sm: { 25 | width: 640, 26 | height: 1136, 27 | }, 28 | 29 | md: { 30 | width: 768, 31 | height: 1024, 32 | }, 33 | 34 | lg: { 35 | width: 1024, 36 | height: 768, 37 | }, 38 | 39 | xl: { 40 | width: 1280, 41 | height: 720, 42 | }, 43 | 44 | "2xl": { 45 | width: 1536, 46 | height: 864, 47 | }, 48 | 49 | "3xl": { 50 | width: 1920, 51 | height: 1080, 52 | }, 53 | }; 54 | 55 | export type ViewportSizeType = keyof typeof VIEWPORT_SIZES; 56 | export const ViewportSize = { 57 | miniMobile: "miniMobile" as ViewportSizeType, 58 | mobile: "mobile" as ViewportSizeType, 59 | tablet: "tablet" as ViewportSizeType, 60 | desktop: "desktop" as ViewportSizeType, 61 | fluid: "fluid" as ViewportSizeType, 62 | sm: "sm" as ViewportSizeType, 63 | md: "md" as ViewportSizeType, 64 | lg: "lg" as ViewportSizeType, 65 | xl: "xl" as ViewportSizeType, 66 | "2xl": "2xl" as ViewportSizeType, 67 | "3xl": "3xl" as ViewportSizeType, 68 | }; 69 | -------------------------------------------------------------------------------- /lib/components/ResponsiveIframeViewer/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCallback, useEffect, useState } from "react"; 3 | import { VIEWPORT_SIZES, ViewportSizeType, ViewportSize } from "./consts"; 4 | import { Resizable } from "re-resizable"; 5 | import { 6 | GripHorizontalIcon, 7 | GripVerticalIcon, 8 | SmartphoneIcon, 9 | Laptop2Icon, 10 | MonitorIcon, 11 | FullscreenIcon, 12 | Tv2Icon, 13 | ScanIcon, 14 | RectangleVertical, 15 | } from "lucide-react"; 16 | 17 | interface ResponsiveIframeViewerProps 18 | extends React.HTMLAttributes { 19 | src: string; 20 | title: string; 21 | width?: number; 22 | height?: number; 23 | size?: ViewportSizeType; 24 | minWidth?: number; 25 | minHeight?: number; 26 | showControls?: boolean; 27 | enabledControls?: ViewportSizeType[]; 28 | allowResizingY?: boolean; 29 | allowResizingX?: boolean; 30 | fluidX?: boolean; 31 | fluidY?: boolean; 32 | onIframeLoad?: ( 33 | event: React.SyntheticEvent 34 | ) => void; 35 | overrideViewportSizes?: Partial< 36 | Record< 37 | ViewportSizeType, 38 | { 39 | width: number | string; 40 | height: number | string; 41 | } 42 | > 43 | >; 44 | iframeClassName?: string; 45 | resizableContainerClassName?: string; 46 | controlsClassName?: string; 47 | controlsContainerClassName?: string; 48 | controlsPreComponent?: React.ReactNode; 49 | controlsPostComponent?: React.ReactNode; 50 | } 51 | 52 | interface ViewportChangeButtonProps 53 | extends React.HTMLAttributes { 54 | size: ViewportSizeType; 55 | selected?: boolean; 56 | } 57 | 58 | const ViewportChangeButton = (props: ViewportChangeButtonProps) => { 59 | const { size, ...rest } = props; 60 | const iconSize = 18; 61 | 62 | let icon = ; 63 | switch (size) { 64 | case ViewportSize.miniMobile: 65 | icon = ; 66 | break; 67 | 68 | case ViewportSize.mobile: 69 | case ViewportSize.sm: 70 | icon = ; 71 | break; 72 | case ViewportSize.tablet: 73 | case ViewportSize.md: 74 | icon = ; 75 | break; 76 | case ViewportSize.desktop: 77 | case ViewportSize.lg: 78 | case ViewportSize.xl: 79 | icon = ; 80 | break; 81 | 82 | case ViewportSize["2xl"]: 83 | case ViewportSize["3xl"]: 84 | icon = ; 85 | break; 86 | 87 | default: 88 | icon = ; 89 | break; 90 | } 91 | 92 | return ( 93 | 106 | ); 107 | }; 108 | 109 | const CornerHandle = (props: React.HTMLAttributes) => ( 110 |
117 | ); 118 | 119 | const CustomHandle = (props: React.HTMLAttributes) => ( 120 |
127 | ); 128 | 129 | const HorizontalHandle = ({ className }: { className?: string }) => ( 130 | 131 | 132 | 133 | ); 134 | 135 | const VerticalHandle = ({ className }: { className?: string }) => ( 136 | 137 | 138 | 139 | ); 140 | 141 | export const ResponsiveIframeViewer = (props: ResponsiveIframeViewerProps) => { 142 | const { 143 | width = VIEWPORT_SIZES.desktop.width, 144 | height = VIEWPORT_SIZES.desktop.height, 145 | size, 146 | minWidth = 200, 147 | minHeight = 200, 148 | enabledControls = [ 149 | ViewportSize.mobile, 150 | ViewportSize.tablet, 151 | ViewportSize.desktop, 152 | ViewportSize.fluid, 153 | ], 154 | overrideViewportSizes, 155 | showControls = true, 156 | allowResizingY = false, 157 | allowResizingX = false, 158 | fluidX = false, 159 | fluidY = false, 160 | iframeClassName = "", 161 | resizableContainerClassName = "", 162 | controlsPreComponent, 163 | controlsPostComponent, 164 | controlsContainerClassName, 165 | controlsClassName, 166 | className, 167 | src, 168 | title, 169 | onIframeLoad, 170 | ...iframeProps 171 | } = props; 172 | 173 | const getViewportSize = useCallback(() => { 174 | // Apply fluid settings first 175 | const fluidDimensions = { 176 | width: fluidX ? "100%" : width, 177 | height: fluidY ? "100%" : height, 178 | }; 179 | 180 | // If size is specified and neither fluidX nor fluidY are set, use the viewport size 181 | if (size && !fluidX && !fluidY) { 182 | return VIEWPORT_SIZES[size]; 183 | } 184 | 185 | // If size is specified but either fluidX or fluidY are set, 186 | // selectively override the dimensions 187 | if (size) { 188 | return { 189 | width: fluidX ? "100%" : VIEWPORT_SIZES[size].width, 190 | height: fluidY ? "100%" : VIEWPORT_SIZES[size].height, 191 | }; 192 | } 193 | 194 | // Otherwise use the fluid dimensions (which might include width/height props) 195 | return fluidDimensions; 196 | }, [size, width, height, fluidX, fluidY]); 197 | 198 | const [viewportSizeInternal, setViewportSizeInternal] = useState<{ 199 | width: number | string; 200 | height: number | string; 201 | }>(getViewportSize()); 202 | 203 | const updateViewportSize = useCallback( 204 | ( 205 | { width, height }: { width: number | string; height: number | string }, 206 | size?: ViewportSizeType 207 | ) => { 208 | // Handle known viewports, applying overrides if necessary. 209 | if (size) { 210 | const viewportWidth = 211 | overrideViewportSizes?.[size]?.width || VIEWPORT_SIZES[size].width; 212 | const viewportHeight = 213 | overrideViewportSizes?.[size]?.height || VIEWPORT_SIZES[size].height; 214 | 215 | // Apply fluid settings on top of viewport sizes 216 | setViewportSizeInternal({ 217 | width: fluidX ? "100%" : viewportWidth, 218 | height: fluidY ? "100%" : viewportHeight, 219 | }); 220 | 221 | return; 222 | } 223 | 224 | // Arbitrary dimensions. 225 | if (typeof width === "number" && typeof height === "number") { 226 | const nextWidth = width < (minWidth || 0) ? minWidth : width; 227 | const nextHeight = height < (minHeight || 0) ? minHeight : height; 228 | 229 | // Apply fluid settings for arbitrary dimensions 230 | setViewportSizeInternal({ 231 | width: fluidX ? "100%" : nextWidth, 232 | height: fluidY ? "100%" : nextHeight, 233 | }); 234 | } else { 235 | // If width/height are already strings (potentially "100%"), 236 | // respect fluidX/fluidY for explicit overrides 237 | setViewportSizeInternal({ 238 | width: fluidX ? "100%" : width, 239 | height: fluidY ? "100%" : height, 240 | }); 241 | } 242 | }, 243 | [ 244 | setViewportSizeInternal, 245 | minWidth, 246 | minHeight, 247 | overrideViewportSizes, 248 | fluidX, 249 | fluidY, 250 | ] 251 | ); 252 | 253 | const isSizeSelected = (size: ViewportSizeType) => { 254 | return ( 255 | viewportSizeInternal.width === VIEWPORT_SIZES[size].width && 256 | viewportSizeInternal.height === VIEWPORT_SIZES[size].height 257 | ); 258 | }; 259 | 260 | const Controls = () => { 261 | return showControls ? ( 262 |
268 | {controlsPreComponent} 269 |
275 | {enabledControls.map((size) => { 276 | return ( 277 | updateViewportSize(VIEWPORT_SIZES[size], size)} 281 | selected={isSizeSelected(size)} 282 | /> 283 | ); 284 | })} 285 |
286 | {controlsPostComponent} 287 |
288 | ) : null; 289 | }; 290 | 291 | useEffect(() => { 292 | updateViewportSize(getViewportSize(), size); 293 | }, [getViewportSize, updateViewportSize, size]); 294 | 295 | if (!allowResizingX && !allowResizingY) { 296 | return ( 297 |
303 | 304 |