I want to change the order of elements in the DOM based on different browser sizes.
I've looked into using intention.js but feel that it might be overkill for what I need (it depends on underscore.js).
So, i'm considering using jQuery's .resize(), but want to know if you think something like the following would be acceptable, and in line with best practices...
var layout = 'desktop';
$( window ).resize(function() {
var ww = $( window ).width();
if(ww<=767 && layout !== 'mobile'){
layout = 'mobile';
// Do something here
}else if((ww>767 && ww<=1023) && layout !== 'tablet'){
layout = 'tablet';
// Do something here
}else if(ww>1023 && layout !== 'desktop'){
layout = 'desktop';
// Do something here
}
}).trigger('resize');
I'm storing the current layout in the layout variable so as to only trigger the functions when the window enters the next breakpoint.
Media queries are generally preferred. However, if I am in a situation where I am in a single page application that has a lot of manipulation during runtime, I will use onresize() instead. Javascript gives you a bit more freedom to work with dynamically setting breakpoints (especially if you are moving elements around inside the DOM tree with stuff like append()). The setup you have is pretty close to the one I use:
function setWidthBreakpoints(windowWidth) {
if (windowWidth >= 1200) {
newWinWidth = 'lg';
} else if (windowWidth >= 992) {
newWinWidth = 'md';
} else if (windowWidth >= 768) {
newWinWidth = 'sm';
} else {
newWinWidth = 'xs';
}
}
window.onresize = function () {
setWidthBreakpoints($(this).width());
if (newWinWidth !== winWidth) {
onSizeChange();
winWidth = newWinWidth;
}
};
function onSizeChange() {
// do some size changing events here.
}
The one thing that you have not included that is considered best practice is a debouncing function, such as the one below provided by Paul Irish, which prevents repeated firing of the resize event in a browser window:
(function($,sr){
// debouncing function from John Hann
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
var debounce = function (func, threshold, execAsap) {
var timeout;
return function debounced () {
var obj = this, args = arguments;
function delayed () {
if (!execAsap)
func.apply(obj, args);
timeout = null;
};
if (timeout)
clearTimeout(timeout);
else if (execAsap)
func.apply(obj, args);
timeout = setTimeout(delayed, threshold || 100);
};
}
// smartresize
jQuery.fn[sr] = function(fn){ return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); };
})(jQuery,'smartresize');
// usage:
$(window).smartresize(function(){
// code that takes it easy...
});
So incorporate a debouncer into your resize function and you should be golden.
In the practice is better to use Media Queries
Try this, I'm in a hurry atm and will refactor later.
SCSS:
body, html, .wrapper { width: 100%; height: 100% }
.sidebar { width: 20%; height: 500px; float: left;
&.mobile { display: none } }
.content { float: right; width: 80% }
.red { background-color: red }
.blue { background-color: blue }
.green { background-color: green }
#media all and (max-width: 700px) {
.content { width: 100%; float: left }
.sidebar { display: none
&.mobile { display: block; width: 100% }
}
}
HAML
.wrapper
.sidebar.blue
.content.red
.content.green
.sidebar.mobile.blue
On 700 px page breaks, sidebar disappears and mobile sidebar appears.
This can be much more elegant but you get the picture.
Only possible downside to this approach is duplication of sidebar.
That's it, no JS.
Ok, the reason for my original question was because I couldn't find a way to move a left sidebar (which appears first in the HTML) to appear after the content on mobiles.
Despite the comments, I still can't see how using media queries and position or display alone would reliably solve the problem (perhaps someone can give an example?).
But, it did lead me to investigate the flexbox model - display: flex, and so I have ended up using that, and specifically flex's order property to re-arrange the order of the sidebars and content area.
Good guide here - https://css-tricks.com/snippets/css/a-guide-to-flexbox/
Related
I'm using the new position: sticky (info) to create an iOS-like list of content.
It's working well and far superior than the previous JavaScript alternative (example) however as far as I know no event is fired when it's triggered, which means I can't do anything when the bar hits the top of the page, unlike with the previous solution.
I'd like to add a class (e.g. stuck) when an element with position: sticky hits the top of the page. Is there a way to listen for this with JavaScript? Usage of jQuery is fine.
Demo with IntersectionObserver (use a trick):
// get the sticky element
const stickyElm = document.querySelector('header')
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
header{
position: sticky;
top: -1px; /* ➜ the trick */
padding: 1em;
padding-top: calc(1em + 1px); /* ➜ compensate for the trick */
background: salmon;
transition: .1s;
}
/* styles for when the header is in sticky mode */
header.isSticky{
font-size: .8em;
opacity: .5;
}
<section>Space</section>
<header>Sticky Header</header>
The top value needs to be -1px or the element will never intersect with the top of the browser window (thus never triggering the intersection observer).
To counter this 1px of hidden content, an additional 1px of space should be added to either the border or the padding of the sticky element.
💡 Alternatively, if you wish to keep the CSS as is (top:0), then you can apply the "correction" at the intersection observer-level by adding the setting rootMargin: '-1px 0px 0px 0px' (as #mattrick showed in his answer)
Demo with old-fashioned scroll event listener:
auto-detecting first scrollable parent
Throttling the scroll event
Functional composition for concerns-separation
Event callback caching: scrollCallback (to be able to unbind if needed)
// get the sticky element
const stickyElm = document.querySelector('header');
// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);
// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;
// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)
// Act if sticky or not
const onSticky = isSticky => {
console.clear()
console.log(isSticky)
stickyElm.classList.toggle('isSticky', isSticky)
}
// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)
const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)
// OPTIONAL CODE BELOW ///////////////////
// find-first-scrollable-parent
// Credit: https://stackoverflow.com/a/42543908/104380
function getScrollParent(element, includeHidden) {
var style = getComputedStyle(element),
excludeStaticParent = style.position === "absolute",
overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
if (style.position !== "fixed")
for (var parent = element; (parent = parent.parentElement); ){
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === "static")
continue;
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX))
return parent;
}
return window
}
// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
header{
position: sticky;
top: 0;
/* not important styles */
background: salmon;
padding: 1em;
transition: .1s;
}
header.isSticky{
/* styles for when the header is in sticky mode */
font-size: .8em;
opacity: .5;
}
/* not important styles*/
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
<section>Space</section>
<header>Sticky Header</header>
Here's a React component demo which uses the first technique
I found a solution somewhat similar to #vsync's answer, but it doesn't require the "hack" that you need to add to your stylesheets. You can simply change the boundaries of the IntersectionObserver to avoid needing to move the element itself outside of the viewport:
const observer = new IntersectionObserver(callback, {
rootMargin: '-1px 0px 0px 0px',
threshold: [1],
});
observer.observe(element);
If anyone gets here via Google one of their own engineers has a solution using IntersectionObserver, custom events, and sentinels:
https://developers.google.com/web/updates/2017/09/sticky-headers
Just use vanilla JS for it. You can use throttle function from lodash to prevent some performance issues as well.
const element = document.getElementById("element-id");
document.addEventListener(
"scroll",
_.throttle(e => {
element.classList.toggle(
"is-sticky",
element.offsetTop <= window.scrollY
);
}, 500)
);
After Chrome added position: sticky, it was found to be not ready enough and relegated to to --enable-experimental-webkit-features flag. Paul Irish said in February "feature is in a weird limbo state atm".
I was using the polyfill until it become too much of a headache. It works nicely when it does, but there are corner cases, like CORS problems, and it slows page loads by doing XHR requests for all your CSS links and reparsing them for the "position: sticky" declaration that the browser ignored.
Now I'm using ScrollToFixed, which I like better than StickyJS because it doesn't mess up my layout with a wrapper.
There is currently no native solution. See Targeting position:sticky elements that are currently in a 'stuck' state. However I have a CoffeeScript solution that works with both native position: sticky and with polyfills that implement the sticky behavior.
Add 'sticky' class to elements you want to be sticky:
.sticky {
position: -webkit-sticky;
position: -moz-sticky;
position: -ms-sticky;
position: -o-sticky;
position: sticky;
top: 0px;
z-index: 1;
}
CoffeeScript to monitor 'sticky' element positions and add the 'stuck' class when they are in the 'sticky' state:
$ -> new StickyMonitor
class StickyMonitor
SCROLL_ACTION_DELAY: 50
constructor: ->
$(window).scroll #scroll_handler if $('.sticky').length > 0
scroll_handler: =>
#scroll_timer ||= setTimeout(#scroll_handler_throttled, #SCROLL_ACTION_DELAY)
scroll_handler_throttled: =>
#scroll_timer = null
#toggle_stuck_state_for_sticky_elements()
toggle_stuck_state_for_sticky_elements: =>
$('.sticky').each ->
$(this).toggleClass('stuck', this.getBoundingClientRect().top - parseInt($(this).css('top')) <= 1)
NOTE: This code only works for vertical sticky position.
I came up with this solution that works like a charm and is pretty small. :)
No extra elements needed.
It does run on the window scroll event though which is a small downside.
apply_stickies()
window.addEventListener('scroll', function() {
apply_stickies()
})
function apply_stickies() {
var _$stickies = [].slice.call(document.querySelectorAll('.sticky'))
_$stickies.forEach(function(_$sticky) {
if (CSS.supports && CSS.supports('position', 'sticky')) {
apply_sticky_class(_$sticky)
}
})
}
function apply_sticky_class(_$sticky) {
var currentOffset = _$sticky.getBoundingClientRect().top
var stickyOffset = parseInt(getComputedStyle(_$sticky).top.replace('px', ''))
var isStuck = currentOffset <= stickyOffset
_$sticky.classList.toggle('js-is-sticky', isStuck)
}
Note: This solution doesn't take elements that have bottom stickiness into account. This only works for things like a sticky header. It can probably be adapted to take bottom stickiness into account though.
I know it has been some time since the question was asked, but I found a good solution to this. The plugin stickybits uses position: sticky where supported, and applies a class to the element when it is 'stuck'. I've used it recently with good results, and, at time of writing, it is active development (which is a plus for me) :)
I'm using this snippet in my theme to add .is-stuck class to .site-header when it is in a stuck position:
// noinspection JSUnusedLocalSymbols
(function (document, window, undefined) {
let windowScroll;
/**
*
* #param element {HTMLElement|Window|Document}
* #param event {string}
* #param listener {function}
* #returns {HTMLElement|Window|Document}
*/
function addListener(element, event, listener) {
if (element.addEventListener) {
element.addEventListener(event, listener);
} else {
// noinspection JSUnresolvedVariable
if (element.attachEvent) {
element.attachEvent('on' + event, listener);
} else {
console.log('Failed to attach event.');
}
}
return element;
}
/**
* Checks if the element is in a sticky position.
*
* #param element {HTMLElement}
* #returns {boolean}
*/
function isSticky(element) {
if ('sticky' !== getComputedStyle(element).position) {
return false;
}
return (1 >= (element.getBoundingClientRect().top - parseInt(getComputedStyle(element).top)));
}
/**
* Toggles is-stuck class if the element is in sticky position.
*
* #param element {HTMLElement}
* #returns {HTMLElement}
*/
function toggleSticky(element) {
if (isSticky(element)) {
element.classList.add('is-stuck');
} else {
element.classList.remove('is-stuck');
}
return element;
}
/**
* Toggles stuck state for sticky header.
*/
function toggleStickyHeader() {
toggleSticky(document.querySelector('.site-header'));
}
/**
* Listen to window scroll.
*/
addListener(window, 'scroll', function () {
clearTimeout(windowScroll);
windowScroll = setTimeout(toggleStickyHeader, 50);
});
/**
* Check if the header is not stuck already.
*/
toggleStickyHeader();
})(document, window);
#vsync 's excellent answer was almost what I needed, except I "uglify" my code via Grunt, and Grunt requires some older JavaScript code styles. Here is the adjusted script I used instead:
var stickyElm = document.getElementById('header');
var observer = new IntersectionObserver(function (_ref) {
var e = _ref[0];
return e.target.classList.toggle('isSticky', e.intersectionRatio < 1);
}, {
threshold: [1]
});
observer.observe( stickyElm );
The CSS from that answer is unchanged
Something like this also works for a fixed scroll height:
// select the header
const header = document.querySelector('header');
// add an event listener for scrolling
window.addEventListener('scroll', () => {
// add the 'stuck' class
if (window.scrollY >= 80) navbar.classList.add('stuck');
// remove the 'stuck' class
else navbar.classList.remove('stuck');
});
I got a confusing bug inside my code and do not know how to fix it. I got to main functions, calling scripts depending on the current window size.
For mobile devices I got a slideToggle() function, which does work when viewing on a mobile device. But when opening above 600px, and scaling down the function is fired, but the slideToggle(); does not work anymore.
Its set to display: block; and immediately set to display: none; again. My slideToggle(); is embedded like this:
$('.box-header').on('click', function() {
$(this).parent().toggleClass('folded').toggleClass('unfolded');
$(this).next('div').stop().slideToggle();
console.log('clicked');
});
JSFIDDLE DEMO
When viewing the demo its important to load the page while the window size is above 600px and than scale down to recreate the problem. If it works please try again, as it does work sometimes.
What am I missing? And is there a better approach to kill and call scripts depending on the viewers viewport after resize events?
Put the if in the click event:
$('.box-header').on('click', function() {
if ($(window).width() < 600) {
$(this).parent().toggleClass('folded unfolded');
$(this).next('div').stop().slideToggle();
} else {
//for other code here for medium+large screens
}
});
https://jsfiddle.net/5puqn9ts/1/
even use a class to toggle between the mobile and large screen, bind a click event to each
$(document).ready(function() {
$('body').on('click', '.mobile', function() {
$(this).parent().toggleClass('folded').toggleClass('unfolded');
$(this).next('div').stop().slideToggle();
console.log('clicked');
});
// init scripts for smartphone or desktops
var initScripts = function() {
if ($(window).width() < 600) {
$('.box-header').addClass('mobile');
}
if ($(window).width() >= 600) {
$('.box-header').removeClass('mobile');
/add class for medium screen then bind event to ti
}
}
initScripts();
var id;
$(window).resize(function() {
initScripts()
});
});
https://jsfiddle.net/75186j96/5/
Your function set an event in every call. Try this correct alternative:
var smartphoneFunctions = $('.box-header').on('click', function() {
$(this).parent().toggleClass('folded').toggleClass('unfolded');
$(this).next('div').stop().slideToggle();
console.log('clicked');
});
https://jsfiddle.net/75186j96/8/
To answer your question, you need to adjust your slideToggle() target to be .box-content rather than the abstract div as this will instead apply the toggle on the .box-header. Also your class toggling could be bundled.
The resulting code would be the following:
$('.box-header').on('click', function() {
$(this).parent().toggleClass('folded unfolded');
$(this).next('.box-content').stop().slideToggle();
console.log('clicked');
});
That being said, there are better ways of doing this.
You could tie your classes to the appearance of .box-content, like this:
.box-content {
transition: height 300ms;
overflow: hidden;
}
.box.folded .box-content {
max-height: 0;
}
.box.unfolded .box-content {
max-height: 300px;
}
hoping to get some JS/CSS help here. I need to have the checkout button on a page of mine go to the top of the page and become fixed if the user can no longer see it scrolling down the page in mobile view. I'm hoping someone can help! The one thing messing me up is that I can't use jQuery
![function checkoutScroll() {
var button = document.querySelector('.cartSidebar__checkoutButton');
window.addEventListener('scroll', function () {
var distanceFromTop = document.body.scrollTop;
if (distanceFromTop === 0) {
button.style.position = 'static';
button.style.top = 'auto';
}
if (distanceFromTop > 0) {
button.style.position = 'fixed';
button.style.top = '0px';
}
});
}
What you are trying to achieve can be done through CSS which would make more sense as it's a visual / UI task. I would add top margin equivalent to the css height of your button and leave it as fixed top. As a benefit, you would be able to take advantage of the media queries to limit the CSS rules to the mobile view.
#media screen and (max-width: 960px) {
.container{
margin: 3em;
}
.checkout_button{
display:block;
position:fixed;
top:0;
left:0;
width:100%;
}
}
something very simple like https://jsfiddle.net/f19Lus43/
If you want to stay in javascript for some obscure reasons ( I can't say compatibility because of document.querySelector is working only on evolved browser ) it's up to you but having an example of your code would help us respond :)
So I take it you want the function to only run on smaller screens/browser viewports? Is that what you mean by "mobile view"? I've been using this for a while. Not sure if its better than Glen's solution but it's worked for me without fault. First we define our functions:
function updateViewportDimensions() {
var w=window,d=document,e=d.documentElement,g=d.getElementsByTagName('body')[0],x=w.innerWidth||e.clientWidth||g.clientWidth,y=w.innerHeight||e.clientHeight||g.clientHeight;
return { width:x,height:y };
}
// setting the viewport width
var viewport = updateViewportDimensions();
function detectMob() {
viewport = updateViewportDimensions();
if (viewport.width <= 768) {
return true;
} else {
return false;
}
}
Then every time you need to check if the size of the viewport is less than 768 pixels wide you do:
if (detectMob){
//your code here
}
I'm working on an assignment for a class where I have to make an external JavaScript file that checks a page's current width and changes the linked CSS style based on it, and have that occur whenever the page loads or is resized. Normally, we are given an example to base our assignment off of, but that was not the case this time around.
Essentially we are to use an if...then statement to change the style. I have no clue what the appropriate statements would be for the function. I've looked around and the potential solutions are either too advanced for the class or don't go over what I need. As far as I know I cannot use jQuery or CSS queries.
If someone could give me an example of how I would write this out, I would be very appreciative.
Try this code
html is
<div id="resize" style="background:red; height: 100px; width: 100px;"></div>
javascript is
var resize = document.getElementById('resize');
window.onresize=function(){
if(window.innerWidth <= 500) {
resize.style = "background:blue; height: 100px; width: 100px;";
}
else {
resize.style = "background:red; height: 100px; width: 100px;";
}
};
i have create a sample for you look at here TESTRESIZE
try resizing your browser and let me know if it is what you are looking for.
//Use This//
function adjustStyle() {
var width = 0;
// get the width.. more cross-browser issues
if (window.innerHeight) {
width = window.innerWidth;
} else if (document.documentElement && document.documentElement.clientHeight) {
width = document.documentElement.clientWidth;
} else if (document.body) {
width = document.body.clientWidth;
}
// now we should have it
if (width < 799) {
document.getElementById("CSS").setAttribute("href", "http://carrasquilla.faculty.asu.edu/GIT237/smallStyle.css");
} else {
document.getElementById("CSS").setAttribute("href", "http://carrasquilla.faculty.asu.edu/GIT237/largeStyle.css");
}
}
// now call it when the window is resized.
window.onresize = function () {
adjustStyle();
};
window.onload = function () {
adjustStyle();
};
Use CSS and media queries. It's bad idea to change style by js.
It appears that altering the dimensions of a canvas clears any drawing already done on that canvas.
Is there an event that fires on canvas resize, so I can hook it and redraw when it occurs?
Update 2020
Jemenake's answer looks good, but in general I would recommend loading in a library which provides debounce functionality, as that's what this is called.
For example with the lodash one, you can do window.addEventListner('resize', _.debounce(onResize, 500). Note that there is a third argument where you can specify the behavior you want (e.g. window.addEventListner('resize', _.debounce(onResize, 500, {leading: true, trailing true})), although the default should be pretty good.
Kit Sunde's answer will do a lot of unnecessary work whilst the browser window is not resized. It is better to check whether the event got resized in response to a browser event and next ignore resize events for a given amount of time after which you do a check again (this will cause two checks after eachother in quick succession and the code could be improved to prevent this, but you get the idea probably).
(function(){
var doCheck = true;
var check = function(){
//do the check here and call some external event function or something.
};
window.addEventListener("resize",function(){
if(doCheck){
check();
doCheck = false;
setTimeout(function(){
doCheck = true;
check();
},500)
}
});
})();
Please note, the code above was typed blindly and not checked.
David Mulder's answer is an improvement, but it looks like it will trigger after waiting timeout milliseconds after the first resize event. In other words, if more resizes happen before the timeout, it doesn't reset the timer. I was looking for the opposite; I wanted something which would wait a little bit of time to let the resize events stop, and then fire after a certain amount of time after the last one. The following code does that.
The ID of any currently-running timer will be in timer_id. So, whenever there's a resize, it checks to see if there's already a time running. If so, it cancels that one and starts a new one.
function setResizeHandler(callback, timeout) {
var timer_id = undefined;
window.addEventListener("resize", function() {
if(timer_id != undefined) {
clearTimeout(timer_id);
timer_id = undefined;
}
timer_id = setTimeout(function() {
timer_id = undefined;
callback();
}, timeout);
});
}
function callback() {
// Here's where you fire-after-resize code goes.
alert("Got called!");
}
setResizeHandler(callback, 1500);
You usually don't want to strictly check for a resize event because they fire a lot when you do a dynamic resize, like $(window).resize in jQuery and as far I'm aware there is no native resize event on elements (there is on window). I would check it on an interval instead:
function onResize( element, callback ){
var elementHeight = element.height,
elementWidth = element.width;
setInterval(function(){
if( element.height !== elementHeight || element.width !== elementWidth ){
elementHeight = element.height;
elementWidth = element.width;
callback();
}
}, 300);
}
var element = document.getElementsByTagName("canvas")[0];
onResize( element, function(){ alert("Woo!"); } );
I didn't find any build-in events to detect new canvas dimensions, so I tried to find a workaround for two scenarios.
function onresize()
{
// handle canvas resize event here
}
var _savedWidth = canvas.clientWidth;
var _savedHeight = canvas.clientHeight;
function isResized()
{
if(_savedWidth != canvas.clientWidth || _savedHeight != canvas.clientHeight)
{
_savedWidth = canvas.clientWidth;
_savedHeight = canvas.clientHeight;
onresize();
}
}
window.addEventListener("resize", isResized);
(new MutationObserver(function(mutations)
{
if(mutations.length > 0) { isResized(); }
})).observe(canvas, { attributes : true, attributeFilter : ['style'] });
For detecting any JavaScript canvas.style changes, the MutationObserver API is good for. It doesn't detect a specific style property, but
in this case it is sufficient to check if canvas.clientWidth and canvas.clientHeight has changed.
If the canvas size is declared in a style-tag with a percent unit, we have a problem to interpret the css selectors correctly (>, *, ::before, ...) by using JavaScript. I think in this case the best way is to set a window.resize handler and also check the canvas client size.
Have you tried ResizeObserver (caniuse.com) in addition to the debounce logic mentioned in the other answers like Jemenake's?
For example:
const pleasantText = 'There is a handle! Let\'s try relaxing while resizing the awesome canvas! ->';
onResize = (entries) => {
document.getElementsByTagName('span')[0].innerHTML =
`${pleasantText} <b>${entries[0].target.offsetWidth}x${entries[0].target.offsetHeight}</b>`;
};
const canvas = document.getElementsByTagName('canvas')[0];
const observer = new ResizeObserver(onResize);
observer.observe(canvas);
body > div:first-of-type {
display: grid;
place-items: center;
width: 100vw;
height: 100vh;
user-select: none;
}
body > div:first-of-type div {
display: grid;
min-width: 12em;
min-height: 6em;
width: 16rem;
height: 6rem;
background: #ccc;
overflow: auto;
place-items: center;
text-align: center;
resize: both;
grid-auto-columns: 20vw 1fr;
grid-auto-flow: column;
grid-gap: 20px;
padding: 0.4rem 0.8rem;
border-radius: 4px;
}
canvas {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
background: #ff9;
}
<div>
<div>
<span></span>
<canvas></canvas>
</div>
</div>
save the canvas state as imageData and then redraw it after resizing
var imgDat=ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height)
after resize
ctx.putImageData(imgDat,0,0)