The Goal
I receive a weather forecast from the NWS that has a very particular HTML formatting that I want to preserve. A forecast will always change in both its length and its width. I want to scale the font size of the text to always fit onto a mobile screen, even if the text is really small, and the length of each line should be preserved. The formatting of the text should never change (e.g. lines should never wrap).
What the Output Should Look LIke
My Current Solution
My current solution is using an SVG, but I’m not quite getting the formatting i’m looking for.
function getFireWeatherForecast() {
/* Sample API response */
return "<h1>Fire Weather Discussion</h1><p>FNUS56 KMFR 252130<br>FWFMFR<p>Fire Weather Forecast for southern Oregon and northern California<br>National Weather Service Medford, OR<br>230 PM PDT Fri Jul 25 2025<p>...RED FLAG WARNING REMAINS IN EFFECT UNTIL 11 PM PDT THIS<br>EVENING FOR ABUNDANT LIGHTNING ON DRY FUELS FOR FIRE WEATHER<br>ZONES 280, 281, 282, 284, 285, 624, AND 625...<p>.DISCUSSION...Another episode of scattered to numerous<br>thunderstorms is expected this evening, focused upon northern <br>California with activity extending into south central Oregon. <br>Storms will be slow-moving and produce rain, but the amount of <br>lightning is expected to exceed critical thresholds. Isolated <br>late day storms are possible for portions of the area Saturday <br>through Monday. The next trough may bring an enhanced risk of<br>thunderstorms during mid-week.</p><h1>Fire Weather Forecast for CAZ282</h1><p>CAZ282-CAZ282-261215-<br>Shasta-Trinity National Forest in Siskiyou County-<br>230 PM PDT Fri Jul 25 2025<p>...RED FLAG WARNING IN EFFECT UNTIL 11 PM PDT THIS EVENING...<p>.Tonight...<br>* Sky/weather...........Mostly cloudy with scattered showers and <br> thunderstorms until midnight, then partly cloudy. Haze after <br> midnight. <br>* Min temperature.......45-55. <br>* Max humidity..........85-100 percent valleys and 65-80 percent <br> ridges. <br>* 20-foot winds......... <br>* Valleys/lwr slopes...Southeast winds 5 to 10 mph. Gusts up to 25 <br> mph in the evening. <br>* Ridges/upr slopes....Southeast winds 6 to 10 mph with gusts to <br> around 25 mph shifting to the southwest 5 to 6 mph after <br> midnight. <br>* Mixing Height.....................4000-7200 ft AGL until 2100, <br> then 100-1600 ft AGL. <br>* Chance of lightning...............44 percent. <br>* Chance of wetting rain (.10 in)...30 percent. <br>* Chance of wetting rain (.25 in)...17 percent. <p>&&<br> TEMP / HUM / POP <br>Mount Shasta 51 92 40<br>.EXTENDED...<br>.SUNDAY NIGHT...Mostly clear. Lows 45 to 55. West winds 5 to<br>8 mph. <br>.MONDAY...Mostly clear. Highs 75 to 85. Light winds becoming<br>southwest 5 to 6 mph in the afternoon and evening.<p>";
}
function createForecastSVG(forecast) {
const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
// 1. SVG shell
const svg = document.createElementNS(SVG_NS, "svg");
svg.setAttribute("preserveAspectRatio", "xMinYMin meet");
// 2. foreignObject
const foreignObject = document.createElementNS(SVG_NS, "foreignObject");
svg.appendChild(foreignObject);
// 3. XHTML <div> that actually holds the text
const div = document.createElementNS(XHTML_NS, "div");
div.innerHTML = forecast;
foreignObject.appendChild(div);
// 4. Measure content size once the browser has rendered a frame
requestAnimationFrame(() => {
const forecastWidth = div.scrollWidth;
const forecastHeight = div.clientHeight;
foreignObject.setAttribute("width", `${forecastWidth}px`);
foreignObject.setAttribute("height", `${forecastHeight}px`);
svg.setAttribute(
"viewBox",
`0 0 ${forecastWidth} ${forecastHeight}`
);
});
return svg;
}
document.getElementById("forecastForm").addEventListener("submit", async (formSubmitEvent) => {
formSubmitEvent.preventDefault();
const fireWeatherForecast = getFireWeatherForecast();
// Write the output text to the page
let forecastWrapper = document.getElementById("forecast-card");
forecastWrapper.innerHTML = ""; // clear previous forecast
const svg = createForecastSVG(fireWeatherForecast);
forecastWrapper.appendChild(svg);
forecastWrapper.style.visibility = "visible";
});
#forecast-card {
/* Positioning inner content styles */
visibility: hidden;
display: block;
overflow-x: scroll;
overflow-y: scroll;
/* Styling for the look of the card */
margin: 2rem auto 0 auto;
background: rgba(30, 32, 35, 0.8);
padding: 1.5rem;
/* HTML content related styles */
font-family: monospace;
font-size: 0.5rem;
color: #e1e1e1;
white-space: pre; /* Preserve exact formatting */
}
.forecast-card svg {
max-width: 100%;
display: inline-block;
}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css" rel="stylesheet"/>
<form id="forecastForm" class="block" action="#">
<div class="button-group">
<button id="coordinates-submit" class="button" type="submit">Submit</button>
</div>
</form>
<!-- SVG will be injected into this div -->
<div id="forecast-card" class="content"></div>
Current Issues
- Lines wrap
- The height of the card doesn’t match the height of the text (the card height is longer)
