Last active
May 28, 2025 03:57
-
-
Save j0hnm4r5/42381b84e626be6345cf31b914171a4e to your computer and use it in GitHub Desktop.
Letterboxer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useState } from "react"; | |
type LetterboxerProps = { | |
/** The target width for the letterbox in pixels */ | |
targetWidth: number; | |
/** The target height for the letterbox in pixels */ | |
targetHeight: number; | |
/** Whether the content can scale larger than the target dimensions */ | |
canScaleLargerThanTarget?: boolean; | |
/** The background color of the letterbox */ | |
backgroundColor?: React.CSSProperties["backgroundColor"]; | |
/** The children to render inside the letterbox */ | |
children: React.ReactNode; | |
}; | |
/** | |
* Letterbox component that maintains a fixed aspect ratio for its children. | |
* It scales the content to fit within the viewport while preserving the target aspect ratio. | |
* It can optionally allow scaling larger than the target dimensions. | |
* | |
* This is useful for applications that need to maintain a specific layout or aspect ratio, even when the viewport size changes dynamically or when viewed on different devices. | |
*/ | |
export const Letterboxer: React.FC<LetterboxerProps> = ({ | |
targetWidth, | |
targetHeight, | |
canScaleLargerThanTarget = false, | |
backgroundColor = "black", | |
children, | |
}) => { | |
const [scale, setScale] = useState(1); | |
useEffect(() => { | |
const updateScale = () => { | |
const { innerWidth, innerHeight } = window; | |
const widthRatio = innerWidth / targetWidth; | |
const heightRatio = innerHeight / targetHeight; | |
const newScale = Math.min( | |
widthRatio, | |
heightRatio, | |
canScaleLargerThanTarget ? Infinity : 1, | |
); | |
setScale(newScale); | |
}; | |
updateScale(); | |
window.addEventListener("resize", updateScale); | |
return () => window.removeEventListener("resize", updateScale); | |
}, [targetWidth, targetHeight, canScaleLargerThanTarget]); | |
return ( | |
<div | |
style={{ | |
position: "fixed", | |
inset: 0, | |
backgroundColor, | |
}} | |
> | |
<div | |
style={{ | |
overflow: "hidden", | |
width: `${targetWidth}px`, | |
height: `${targetHeight}px`, | |
transform: `scale(${scale})`, | |
transformOrigin: "top left", | |
marginTop: `calc(50vh - ${(targetHeight * scale) / 2}px)`, | |
marginLeft: `calc(50vw - ${(targetWidth * scale) / 2}px)`, | |
}} | |
> | |
{children} | |
</div> | |
</div> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment