Пропорциональное изменение размеров дочернего элемента (полностью адаптивный квадрат) при изменении размеров родительского элемента
У нас имеется следующий HTML+CSS код:
function parentH() {
let height = document.querySelector('#parent_range_H').value + 'px';
document.querySelector('#parent').style.height = height;
document.querySelector('#children').style.height = height;
}
function parentW() {
let width = document.querySelector('#parent_range_W').value + 'px';
document.querySelector('#parent').style.width = width;
document.querySelector('#children').style.width = width;
}
#parent {
width: 300px;
height: 300px;
padding: 10px;
border: 5px solid red;
}
#children {
width: 100%;
height: 100%;
background: green;
}
<p>Изменяем высоту (H) родителя: <input id="parent_range_H" type="range" min="10" max="300" value="300" step="1" name="parent_h" oninput="parentH()"></p>
<p>Изменяем ширину (W) родителя: <input id="parent_range_W" type="range" min="10" max="300" value="300" step="1" name="parent_w" oninput="parentW()"></p>
<div id="parent">
<div id="children"></div>
</div>
Родительский элемент <div id="parent"> имеет одинаковые размеры ширина - 300px и высота - 300px, образуя квадрат. Дочерний элемент <div id="children">, имея размеры в процентном соотношении (ширина - 100% и высота - 100%) от размеров своего родительского элемента, полностью заполняя всё его пространство, также образует квадрат. При изменении размеров родительского элемента, дочерний элемент, автоматически уменьшая свои размеры, подстраивается под размеры своего родительского элемента, и нарушает тем самым свои пропорции - квадрата.
Вопрос: Как при изменении размеров родительского элемента, пропорционально изменять размеры дочернего элемента? Дочерний элемент - это должен быть полностью адаптивный квадрат, и при любых изменениях высоты или ширины родительского элемента, дочерний элемент - квадрат - должен всегда оставаться квадратом. Какие есть варианты решения этой задачи?
Ответы (5 шт):
Подобный вопрос довольно часто встречается на ruSO. Поэтому мне кажется, что будет не лишним добавить готовое и полностью рабочее решение, которое может оказаться в будущем для многих пользователей полезным.
Вот один из вариантов такого решения поставленной задачи, с помощью JavaScript кода и с использованием переменных в CSS:
function parentH() {
let height = document.querySelector('#parent_range_H').value;
let width = document.querySelector('#parent_range_W').value;
document.querySelector('#parent').style.height = height + 'px';
document.querySelector('#children').style.setProperty('--size', `${Math.min(height,width)}px`);
}
function parentW() {
let height = document.querySelector('#parent_range_H').value;
let width = document.querySelector('#parent_range_W').value;
document.querySelector('#parent').style.width = width + 'px';
document.querySelector('#children').style.setProperty('--size', `${Math.min(height,width)}px`);
}
#parent {
width: 300px;
height: 300px;
padding: 10px;
border: 5px solid red;
}
#children {
width: var(--size, 100%);
height: var(--size, 100%);
background: green;
}
<p>Изменяем высоту (H) родителя: <input id="parent_range_H" type="range" min="10" max="300" value="300" step="1" name="parent_h" oninput="parentH()"></p>
<p>Изменяем ширину (W) родителя: <input id="parent_range_W" type="range" min="10" max="300" value="300" step="1" name="parent_w" oninput="parentW()"></p>
<div id="parent">
<div id="children"></div>
</div>
И этот же вариант, только с небольшими изменениями в JavaScript и CSS коде. В этом случае установлен уже другой способ позиционирования у дочернего элемента. У родительского элемента значение остается прежним - position: relative - положение элемента установлено относительно своего исходного места. А вот дочерний элемент меняет свое положение на абсолютное позиционирование - position: absolute, где отсчет координат ведется от края родительского элемента:
function parentH() {
let height = document.querySelector('#parent_range_H').valueAsNumber;
let width = document.querySelector('#parent_range_W').valueAsNumber;
document.querySelector('#parent').style.height = height + 'px';
let style = document.querySelector('#children').style;
height <= width && (style.height = style.width = height + 'px');
}
function parentW() {
let height = document.querySelector('#parent_range_H').valueAsNumber;
let width = document.querySelector('#parent_range_W').valueAsNumber;
document.querySelector('#parent').style.width = width + 'px';
let style = document.querySelector('#children').style;
height >= width && (style.height = style.width = width + 'px');
}
#parent {
position: relative;
width: 300px;
height: 300px;
padding: 10px;
border: 5px solid red;
}
#children {
position: absolute;
width: 300px;
height: 300px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
background: green;
}
<p>Изменяем высоту (H) родителя: <input id="parent_range_H" type="range" min="10" max="300" value="300" step="1" name="parent_h" oninput="parentH()"></p>
<p>Изменяем ширину (W) родителя: <input id="parent_range_W" type="range" min="10" max="300" value="300" step="1" name="parent_w" oninput="parentW()"></p>
<div id="parent">
<div id="children"></div>
</div>
Отдельным ответом добавлю ещё один вариант, в котором мы будем менять у родительского элемента только высоту height, а у дочернего элемента только ширину width. При этом у нас всё также дочерний элемент должен пропорционально изменять свои размеры и всегда оставаться полностью адаптивным квадратом.
function parentH() {
let height = document.querySelector('#parent_range_H').value;
let width = document.querySelector('#children_range_W').value;
document.querySelector('#parent').style.height = height + 'px';
document.querySelector('#children').style.setProperty('--size', `${Math.min(height,width)}px`);
}
function childrenW() {
let height = document.querySelector('#parent_range_H').value;
let width = document.querySelector('#children_range_W').value;
document.querySelector('#parent').style.height = height + 'px';
document.querySelector('#parent').style.width = width + 'px';
document.querySelector('#children').style.setProperty('--size', `${Math.min(height,width)}px`);
}
#parent {
width: 300px;
height: 300px;
padding: 10px;
border: 5px solid cornflowerblue;
}
#children {
width: var(--size, 100%);
height: var(--size, 100%);
background: deepskyblue;
}
<p>Изменяем высоту (H) родителя: <input id="parent_range_H" type="range" min="10" max="300" value="300" step="1" name="parent_h" oninput="parentH()"></p>
<p>Изменяем ширину (W) квадрата: <input id="children_range_W" type="range" min="10" max="300" value="300" step="1" name="children_w" oninput="childrenW()"></p>
<div id="parent">
<div id="children"></div>
</div>
Имхо, самое короткое и правильное решение... в будущем... Ибо пока поддержка такого замечательного свойства, как aspect-ratio, оставляет желать лучшего. Но, в Chrome и Edge действительно уже работает "на ура". В общем, как вариант:
#parent {
width: 140px; height: 140px;
padding: 10px; overflow: hidden;
border: 5px solid red; resize: both;
}
#children {
width: 100%; max-height: 100%;
aspect-ratio: 1 / 1;
background: green;
}
<div id="parent">
<div id="children"> </div>
</div>
Вопрос: Как при изменении размеров родительского элемента, пропорционально изменять размеры дочернего элемента? Дочерний элемент должен быть полностью адаптивный квадрат, и при любых изменениях высоты или ширины родительского элемента, дочерний элемент - квадрат - должен всегда оставаться квадратом. Какие есть варианты решения этой задачи?
Короткий ответ:
Наиболее простой путь - использовать SVG, он лучше всего подходит для решения данной задачи.
- Начнем с базового примера, где для простоты понимания исключим влияние
viewBoxна масштабирование и позиционирование. Он будет равенwidth,heightSVG.
Поэтому масштаб в этом примере -1:1
И квадрат будет заполнять все внутреннее пространство SVG.
Примечание:
Красная рамка - это граница SVG
Зеленая рамка - это граница родительского контейнера (в дальнейших примерах)
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100" height="100" viewBox="0 0 100 100" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" stroke="black" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
Метод JS getBBox() вычисляет физические размеры 100х100px рамки окружающей квадрат, то есть в нашем случае - это будут размеры самого квадрата
1. Добавляем родительский контейнер 200х200px
Так как у SVG фиксированные размеры, то он не будет масштабироваться и займет в родительском контейнере 100х100px
.container{
width:200px;
height:200px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100" height="100" viewBox="0 0 100 100" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
2.Родительский контейнер .container{width:300px; height:150px;}
.container{
width:300px;
height:150px;
outline:1px solid green;
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100" height="100" viewBox="0 0 100 100" preserveAspectRatio="xMinYMin meet" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
3. container{width:150px; height:300px;}
.container{
width:150px;
height:300px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100" height="100" viewBox="0 0 100 100" preserveAspectRatio="xMinYMin meet" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
Это были примеры с фиксированными размерами SVG, при которых квадрат занимал место в родительском контейнере равным его собственным размерам 100х100px
Динамическое масштабирование SVG в родительском контейнере
Для этого ширина и высота SVG указывается либо в процентах, либо не указываются width,height, что эквивалентно 100%
width="100%" height="100%" viewBox="0 0 100 100"
#1%. .container{width:200px;height:200px;
Родительский контейнер в два раза больше, но svg с процентными значениями заполняет его полностью. Сравните с примером 1. выше, где у SVG были фиксированные размеры
Обратите внимание!
Квадрат растянулся до размеров родительского контейнера, так как соотношение сторон у контейнера 1:1 и у квадрата 1:1. поэтому на экране он отрендерился 200х200px по размеру родителя, но его физические размеры (как он был задан в SVG) остались прежние 100х100px, это показывает getBBox()
.container{
width:200px;
height:200px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%" height="100%" viewBox="0 0 100 100" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
#2%. container{width:300px;height:100px;
Ширина родительского контейнера в три раза больше высоты, но он не растянулся на всю ширину остался 100х100px так как указан параметр preserveAspectRatio="xMinYMin meet", который заставляет квадрат сохранять свои пропорции и прижиматься к левому краю
.container{
width:300px;
height:100px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMinYMin meet" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
#2.1. preserveAspectRatio="xMaxYMin meet"
Прижимаем квадрат к правому краю родительского контейнера - xMax
.container{
width:300px;
height:100px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMaxYMin meet" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
#2.2. preserveAspectRatio="none"
Этот атрибут говорит SVG, что не надо сохранять пропорции, а так как width="100%" у SVG, то он растягивается на всю ширину родителя
.container{
width:300px;
height:100px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
#3%. container{width:100px;height:300px;
Высота родительского контейнера в три раза больше высоты SVG, поэтому svg квадрат располагается на самом верху.
.container{
width:100px;
height:300px;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMinYMin meet" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
Выводы:
Для адаптивности SVG указывайте
viewBox.width heightв шапке SVG указывайте в процентах или не указывайте никаких значений, что эквивалентно 100%Чтобы SVG полностью заполнил родительский контейнер сделайте одинаковым соотношение
width, heightродительского контейнера иwidth, heightSVG (пример в этом ответе - #1%.)Не применяйте
preserveAspectRatio="none"это нарушает пропорции соотношения сторон
Update
#4. Размеры родительского контейнера заданы в единицах относительно окна браузера
- Сначала происходит масштабирование svg относительно родительского контейнера
- Затем родительский контейнер масштабируется вместе с SVG относительно окна браузера
.container{
width:60vw;
height:20vh;
Понятно, что итоговые размеры родителя будут зависеть от разрешения и выбранного гаджета. Но заметьте, что физический размер квадрата SVG остается без изменений 100x100px см. вывод консоли.
Зачем это нужно?
Это, размеры в исходной (inital) системе координат SVG.
Далее мы можем, как угодно трансформировать SVG, выводить результаты на экран, сжимать, растягивать, но исходная система координат останется без изменений.
.container{
width:60vw;
height:20vh;
outline:1px solid green;
}
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMinYMin meet" style="border:1px solid red;">
<rect id="R1" x="0" y="0" width="100" height="100" rx="15" fill="silver" />
</svg>
</div>
<script>
console.log(R1.getBBox())
</script>
Связанные топики:
CSS
- Как заставить элемент div автоматически изменять размер с сохранением aspect ratio (соотношения сторон)?
- Ответ @web-tiki
SVG
Resize элемента может происходить из-за изменения размеров окна браузера, из-за вставки текста / загрузки картинки, вручную (css-resize: both) или скриптом - из любого места кода.
На JS не нашел простого решения, который капитально покроет все случаи жизни. Поэтому разбиваю ответ на несколько конкретных вариантов:
1: Есть ровно один элемент (например, главное окно мини-игры), его нужно сделать квадратным, и обновлять размеры только при resize окна браузера.
var box = document.querySelector(".box");
updateSize();
window.addEventListener("resize", updateSize);
function updateSize(wid, hei) {
var wid = window.innerWidth; // box.parentNode.clientWidth;
var hei = window.innerHeight; // box.parentNode.clientHeight;
box.style.width = box.style.height = Math.min(wid, hei) + "px";
}
body {
margin: 0;
padding: 0;
background: #500;
}
.box {
background: orange;
margin: auto;
}
<div class="box"></div>
1.1: То же самое с защитой от дурака (Если каждое обновление размеров влечет за собой тяжелые изменения, пусть картинка не обновляется, если начнут играться и быстро ресайзить браузер)
var box = document.querySelector(".box");
updateSize();
window.addEventListener("resize", function resize() {
clearTimeout(resize.timeout);
resize.timeout = setTimeout(updateSize, 300);
// Таймер сбросится и запустится заново, если в течение
// 300 миллисекунд после предыдущего resize, произошло еще одно событие
});
function updateSize(wid, hei) {
var wid = window.innerWidth;
var hei = window.innerHeight;
box.style.width = box.style.height = Math.min(wid, hei) + "px";
}
body {
margin: 0;
padding: 0;
background: #500;
}
.box {
background: orange;
margin: auto;
}
<div class="box"></div>
2: Если предстоит создавать большое количество таких зависимых блоков (плохо представляю, зачем), приятно было бы иметь функцию, которая запомнит, от кого и к кому нужно передать размеры, и в каких пропорциях. А при изменении размеров, лишь вызвать метод, обновляющий размеры зависимых элементов. Примерно так:
let size = new Size({ from: parent, applyTo: child, ratio: 16 / 9, scale: 0.5 });
...onresize = () => size.update();
Резиновые блоки, обновляются и от input-ов, и при resize окна:
function Size(obj) {
this.source = obj.from;
this.target = obj.applyTo;
this.ratio = obj.ratio || 1; // Какое отношение (wid/hgt) соблюдать для target
this.scale = obj.scale || 1; // Полученные размеры target будут домножться на scale
this.update();
}
Size.prototype.update = function() {
var f = parseFloat, s = getComputedStyle(this.source);
var wid = this.source.clientWidth - f(s.paddingLeft) - f(s.paddingRight);
var hei = this.source.clientHeight - f(s.paddingTop) - f(s.paddingBottom);
var target = this.target.style;
magic: {
// Если захочется вникнуть, для удобства выставить ratio = 2/1 или 1/2
if (wid / hei < this.ratio) {
target.width = wid * this.scale + "px"; // Занимает 100% ширину
target.height = wid * (1 / this.ratio) * this.scale + "px";
// Высота подстраивается под ширину.
} else {
target.width = hei * this.ratio * this.scale + "px";
target.height = hei * this.scale + "px";
}
}
}
/*** Использование ***/
let storage = [];
let parent = document.querySelectorAll(".parent");
let child = document.querySelectorAll(".child");
for (let i = 0; i < parent.length; i++) {
storage[i] = new Size({
from: parent[i],
applyTo: child[i],
ratio: 4 / 3,
scale: 0.25 * (i + 1)
});
}
function updateAll() {
storage.forEach(e => e.update());
}
/***/
window.addEventListener("resize", updateAll);
first("#resize-w").addEventListener("input", resize);
first("#resize-h").addEventListener("input", resize);
function resize() {
let dir = this.dataset.dir;
let size = this.value + (dir == "width" ? "vw" : "vh");
parent.forEach(e => e.style[dir] = size);
updateAll();
}
function first(selector) {
return document.querySelector(selector);
}
.parent {
display: inline-block;
width: 20vw;
height: 80vh;
background: #500;
padding: 5px;
margin: 5px;
box-sizing: border-box;
}
.child {
background: orange;
}
<input type="range" min="5" max="20" value="20" step="0.1" id="resize-w" data-dir="width"/> W
<input type="range" min="5" max="80" value="80" step="0.1" id="resize-h" data-dir="height"/> H
<hr>
<div class="parent"><div class="child"></div></div>
<div class="parent"><div class="child"></div></div>
<div class="parent"><div class="child"></div></div>
<div class="parent"><div class="child"></div></div>
Блоки не обязательно должны быть родительскими, могут быть где угодно. К одному блоку можно привязать несколько других, в разных местах.
2.1: Применительно к коду из вопроса:
// *Демонстрирует работоспособность. Бессмысленно воротить такое ради одного элемента.
function Size(obj) {
this.source = obj.from;
this.target = obj.applyTo;
this.ratio = obj.ratio || 1; // Какое отношение (wid/hgt) соблюдать для target
this.scale = obj.scale || 1; // Полученные размеры target будут домножться на scale
this.update();
}
Size.prototype.update = function() {
var f = parseFloat, s = getComputedStyle(this.source);
var wid = this.source.clientWidth - f(s.paddingLeft) - f(s.paddingRight);
var hei = this.source.clientHeight - f(s.paddingTop) - f(s.paddingBottom);
var target = this.target.style;
magic: {
if (wid / hei < this.ratio) {
target.width = wid * this.scale + "px";
target.height = wid * (1 / this.ratio) * this.scale + "px";
} else {
target.width = hei * this.ratio * this.scale + "px";
target.height = hei * this.scale + "px";
}
}
}
/*****/
let parent = document.querySelector("#parent");
let child = document.querySelector("#children");
let size = new Size({ from: parent, applyTo: child, ratio: 1 });
function resize(range, direction) { // -> HTML, oninput
parent.style[direction] = range.value + "px";
size.update();
}
#parent {
width: 300px;
height: 300px;
padding: 10px;
border: 5px solid red;
}
#children {
width: 100%;
height: 100%;
background: green;
}
<p>Изменяем высоту (H) родителя: <input oninput="resize(this, 'height')" id="parent_range_H" type="range" min="10" max="300" value="300" step="1" name="parent_h"></p>
<p>Изменяем ширину (W) родителя: <input oninput="resize(this, 'width')" id="parent_range_W" type="range" min="10" max="300" value="300" step="1" name="parent_w"></p>
<div id="parent">
<div id="children"></div>
</div>
3. Заранее не известно, откуда может происходить resize, но очень нужно контролировать размеры блока. ResizeObserver
var box = document.querySelector("#box");
var observer = new ResizeObserver(function(e) {
console.clear();
console.log(e); // <-- вся необходимая информация о размерах,
// которую можно дальше как-то обрабатывать.
});
observer.observe(box);
/***/
box.style.width = "50vw"; // <-- сработает observer.
#box {
width: 50vw;
height: 50vh;
background: orange;
overflow: hidden;
resize: both;
}
<input type="range" oninput="box.style.width = this.value + 'vw';">
<div id="box"></div>
Поддерживается во всех современных браузерах.
Если нужна поддержка старых, можно написать функцию, которая, например, раз в полсекунды (setInterval(fn, 500)) или 60 раз в секунду (requestAnimationFrame) проверяет размер нужного блока и в случае обнаружения изменений, выполняет функцию. Или гуглить → resize observer polyfill