Главная » Статьи » Срезанные углы блока в CSS 2018

Срезанные углы блока в CSS 2018

Срезанные углы блока в CSS 2018

От автора: когда я увидел статью Криса по notched boxes, я вспомнил, что не так давно у меня была задача по CSS дизайну в кроссбраузерной форме, как показано ниже.

 

Очень похоже на notched boxes, только здесь углы вырезаны по дуге, и нам нужно обрезать только один угол в блоке. Давайте разберемся, как сделать с помощью CSS срезанные углы блока, как можно применить технику к нескольким углам, с какими проблемами столкнемся, и как их решить с или без кроссбраузерности.

Первоначальная идея: box-shadow!

Начинаем с блока:

<div class='box'></div>

Блоку можно указать размеры или позволить определять размер, исходя из его контента – неважно. Для простоты поставим max-width и min-height. Также добавим outline, чтобы видеть рамки.

.box { outline: solid 2px; max-width: 15em; min-height: 10em;
}

Затем с помощью абсолютного позиционирования располагаем квадрат из псевдоэлемента ::before, чья длина края равна диаметру (или двум радиусам $r) выреза в углу. Зададим псевдоэлементу также красноватую box-shadow и какой-нибудь background (потом удалим), чтобы лучше видеть:

$r: 2em; .box { position: relative; /* same styles as before */ &:before { position: absolute; padding: $r; box-shadow: 0 0 7px #b53; background: #95a; content: '' }
}

Что мы получаем:

Смотрится не очень круто… пока что! Превратим квадрат в диск, указав на нем border-radius: 50% и отрицательный радиус $r, чтобы центральная точка круга совпадала с точкой (0, 0) (верхний левый угол) родительского блока. На родительском блоке мы указали overflow: hidden, чтобы часть псевдоэлемента, вылезающая за пределы .box, обрезалась.

$r: 2em; .box { overflow: hidden; /* same styles as before */ &:before { /* same styles as before */ margin: -$r; border-radius: 50% }
}

Мы получили нужную фигуру:

Но это все еще не то, что мы хотим. Для достижения цели мы используем четвертое значение свойства box-shadow: spread radius. Можете освежить память по работе четырех значений свойства box-shadow:

Вы могли догадаться, что мы сделаем дальше. Мы удалим наш фейковый background, обнулим первые три значения box-shadow (смещения по Х и У и радиус размытия), а в последнее значение укажем большое число (радиус распространения):

box-shadow: 0 0 0 300px;

Интерактивное демо ниже показывает, как увеличение радиуса распространения захватывает все больше своего родителя .box:

Трюк заключается в том, чтобы радиус распространения точно покрывал остальную часть родителя. Что хорошо, мы можем сделать box-shadow полупрозрачной или скруглить углы на родителе .box:

.box { /* same styles as before */ border-radius: 1em; &:before { /* same styles as before */ box-shadow: 0 0 0 300px rgba(#95a, .75); }
}

Как Крис заметил в статье по notched boxes, радиус среза можно положить в переменную и легко изменять его через JS. Затем все красиво обновляется, даже с текстом в нашем блоке:

:root { --r: 50px } .box { /* same styles as before */ padding: var(--r); &:before { /* same styles as before */ margin: calc(-1*var(--r)); padding: inherit;
}

С текстовым контентом необходимо задать отрицательный z-index на псевдоэлементе ::before и явно поместить его в угол, так как теперь у нас есть padding на .box для компенсации среза.

.box { /* same styles as before */ &:before { /* same styles as before */ z-index: -1; top: 0; left: 0
}

Применение этой техники

Теперь давайте узнаем, как применять эту концепцию, чтобы воспроизвести дизайн, показанный в начале статьи. В этом примере центральные точки дисков из псевдоэлементов не совпадают с углами блока и выходят за его пределы в середину пространства между блоками.

Используемая структура довольно простая. Просто

и следом теги article, сгенерированные в цикле Pug:

while n-- article h3 #{data[n].name} section p #{data[n].quote} a(href='#') go

К body применяем Flexbox макет с переносом элементов, очень широкий header и 1 или 2 элемента article в строке (зависит от вьюпорта).

Если в каждой строке всего один article, у нас не будет срезанных углов, поэтому их радиус равен 0px. В противном случае этот радиус –r задается в ненулевое значение.

$min-w: 15rem; /* min width of an article element */
$m: 1rem; /* margin of such an element */ html { --r: 0px; } article { margin: $m; min-width: $min-w; width: 21em;
} @media (min-width: 2*($min-w + 2*$m) /* enough for 2 per row */) { html { --r: 4rem; } article { width: 40%; }
}

Разберем ситуацию, когда в строке 2 тега article (и, конечно, у них срезанные углы, нам ведь это и нужно).

У первого тега самая левая точка диска должна находится на одном уровне с правой гранью родителя (left: 100%). Чтобы сдвинуть координату Х центральной точки диска к правой грани родителя, необходимо вычесть радиус диска, что дает нам left: calc(100% — var(—r)). Но центр не должен располагаться вдоль правой границы, он должен быть свещен с помощью margin $m тега

. Так мы получаем финальное значение:

left: calc(100% - var(--r) + #{$m});

По оси У начнем с того, что передвинем самую верхнюю точку диска на пересечение а нижней границей родителя — top: 100%. Чтобы переместить центральную точку диска на нижнюю грань родительского блока, необходимо передвинуть фигуру на 1 радиус, что даст нам top: calc(100% — var(—r)). Нам также нужно сместить центральную точку на $m вниз от нижней грани родителя. Так мы получаем финальный сдвиг:

top: calc(100% - var(--r) + #{$m});

Для второго article (второй на той же строке) значение вертикального сдвига не изменится.

По горизонтали же необходимо сдвинуть самую левую точку диска на пересечение с левой гранью родителя — left: 0%. Чтобы переместить центральную точку диска на пересечение с левой гранью родителя, необходимо сдвинуть фигуру влево на один радиус —r (left: calc(0% — var(—r))). Финальная позиция – смещение $m влево от левого родительского края:

left: calc(0% - var(--r) - #{$m});

Для третьего article (первый во второй строке) значение сдвига по оси Х совпадает с первым тегом.

По вертикали необходимо сдвинуть фигуру так, чтобы самая верхняя точка диска оказалась на пересечении с верхней гранью родителя — top: 0%. Чтобы сдвинуть центральную точку диска на пересечение с верхним краем родителя, необходимо сдвинуть фигуру на один радиус —r (top: calc(0% — var(—r))). Но нам нужно добавить еще $m, поэтому финальный верхний сдвиг:

top: calc(0% - var(--r) - #{$m});

У последнего тега (второй во второй строке) значение горизонтального сдвига совпадает с тем, который расположен над ним, а значение вертикального сдвига – с тем, который слева от него на той же строке.

Сдвиги можно записать следующим образом:

article:nth-of-type(1) { /* 1st */ left: calc(100%/* 2*50% = (1 + 1)*50% = (1 + i)*50% */ - var(--r) + /* i=+1 */#{$m}); top: calc(100%/* 2*50% = (1 + 1)*50% = (1 + j)*50% */ - var(--r) + /* j=+1 */#{$m});
} article:nth-of-type(2) { /* 2nd */ left: calc( 0%/* 0*50% = (1 - 1)*50% = (1 + i)*50% */ - var(--r) - /* i=-1 */#{$m}); top: calc(100%/* 2*50% = (1 + 1)*50% = (1 + j)*50% */ - var(--r) + /* j=+1 */#{$m});
} article:nth-of-type(3) { /* 3rd */ left: calc(100%/* 2*50% = (1 + 1)*50% = (1 + i)*50% */ - var(--r) + /* i=+1 */#{$m}); top: calc( 0%/* 0*50% = (1 - 1)*50% = (1 + j)*50% */ - var(--r) - /* j=-1 */#{$m});
} article:nth-of-type(4) { /* 4th */ left: calc( 0%/* 0*50% = (1 - 1)*50% = (1 + i)*50% */ - var(--r) - /* i=-1 */#{$m}); top: calc( 0%/* 0*50% = (1 - 1)*50% = (1 + j)*50% */ - var(--r) - /* j=-1 */#{$m});
}

Это значит, что позиции центральных точек дисков зависят от расстояния между article (это расстояние равно двойному margin: $m, заданному для article), радиусом дисков r и нескольких горизонтальных и вертикальных множителей (—I и —j). Изначально оба множителя равны -1.

Для первых двух article (на первой строке сетки 2х2) изменим вертикальный множитель —j на 1б так как координата У центральных точек дисков должна быть ниже нижней грани, а для нечетных тегов (в первой колонке) мы меняем горизонтальный множитель –i на 1, так как координата Х должна быть правее правого края.

html { --i: -1; --j: -1 } /* multipliers initially set to -1 */ h3, section { &:before { /* set generic offsets */ top: calc((1 + var(--j))*50% - var(--r) + var(--j)*#{$m}); left: calc((1 + var(--i))*50% - var(--r) + var(--i)*#{$m}); }
} @media (min-width: 2*($min-w + 2*$m)) { article { /* change vertical multiplier for first two (on 1st row of 2x2 grid) */ &:nth-of-type(-n + 2) { --j: 1 } /* change horizontal multiplier for odd ones (on 1st column) */ &:nth-of-type(odd) { --i: 1 }
}

Для первых двух article мы имеем только видимые вырезы в виде дисков на теге section. Для оставшихся – вырезы на h3. Поэтому для первых двух article радиус —r псевдоэлемента ::before заголовка равен 0. Для последних двух радиус равен 0 для псевдоэлемента ::before секции:

@media (min-width: 2*($min-w + 2*$m)) { article { &:nth-of-type(-n + 2) h3, &:nth-of-type(n + 3) section { &:before { --r: 0 ; } } }
}

Точно так же добавляем разные padding дочерним тегам article:

$p: .5rem; h3, section { padding: $p; } @media (min-width: 2*($min-w + 2*$m)) { article { &:nth-of-type(-n + 2) section, &:nth-of-type(n + 3) h3 { padding-right: calc(.5*(1 + var(--i))*(var(--r) - #{$m}) + #{$p}); padding-left: calc(.5*(1 - var(--i))*(var(--r) - #{$m}) + #{$p}); } }
}

Мы получаем, что хотели:

Демо сверху работает во всех текущих версиях основных браузеров. Если подобрать значения вместо CSS переменных, можно добавить поддержку вплоть до IE9.

Потенциальные проблемы описанного метода

Это был быстрый и легкий кроссбраузерный способ получить желаемый результат в этом определенном случае. Однако этот подход может быть не так успешен.

Во-первых, для всех обрезаемых углов нужны псевдоэлементы. Поэтому если нужно срезать все углы, придется вводить дополнительный элемент. Грустно.

Во-вторых, мы не всегда хотим иметь однотонный background. Нас может заинтересовать полупрозрачный фон (что сделать очень сложно, если срезанных углов более одного), градиентный (можно эмулировать радиальный градиент с помощью box-shadow, но решение далеко от идеального) или вообще background с изображением (сложно реализуемо, единственный выход — mix-blend-mode – но он отрезает поддержку Edge безе элегантного фолбека).

А что делать с очень большими блоками, которым недостаточно радиуса распространения? Эмм. Рассмотрим другие, более надежные подходы с разной степенью поддержки в браузерах.

Гибкость и хорошая поддержка в браузерах? SVG!

Наверное, не удивительно, но полное решение SVG лучше всего подходит, если нам нужно что-то гибкое и надежно кроссбраузерное. Решение заключается в том, что нужно использовать SVG элемент перед контентом блока. Этот SVG содержит circle с заданным нами атрибутом радиуса r.

<div class='box'> <svg> <circle r='50'/> </svg> TEXT CONTENT OF BOX GOES HERE
</div>

Абсолютно позиционируем этот SVG внутри блока и делаем так, чтобы он полностью закрывал родителя:

.box { position: relative; } svg { position: absolute; width: 100%; height: 100%;
}

Ничего интересного. Давайте присвоим circle id и скопируем его в другие углы:

<circle id='c' r='50'/> <use xlink:href='#c' x='100%'/> <use xlink:href='#c' y='100%'/> <use xlink:href='#c' x='100%' y='100%'/> 

Если нужно исключить один или более углов, мы просто не копируем их.

Мы создали круги в углах, а мы хотели… совершенно противоположное! Далее мы поместим эти круги в mask поверх white, полноразмерного прямоугольника (закрывающего весь SVG. Далее используем эту mask на другом полноразмерном прямоугольнике:

<mask id='m' fill='#fff'> <rect id='r' width='100%' height='100%'/> <circle id='c' r='50' fill='#000'/> <use xlink:href='#c' x='100%'/> <use xlink:href='#c' y='100%'/> <use xlink:href='#c' x='100%' y='100%'/>
</mask>
<use xlink:href='#r' fill='#f90' mask='url(#m)'/>

Результат ниже:

Если у нас будет текст, нам нужно адаптировать padding блока к радиусу наших углов, установив его в значение радиуса SVG круга с помощью JS для поддержания синхронизации:

Fill прямоугольного фона может быть не сплошной. Заливка может быть полупрозрачной (как в демо сверху), можно использовать SVG градиент или шаблон. Последний вариант позволяет использовать одно и более фоновых изображений.

Но я пришел сюда за CSS!

Хорошо, что спросили! Можно предпринять ряд шагов, чтобы перенести метод маскирования из SVG в CSS.

К сожалению, все они не кроссбраузерные. Однако они упрощают код, поэтому на них стоит обратить внимание в ближайшем или отдаленном будущем.

Использование CSS маскирования HTML элементов

Нам нужно удалить все из mask из SVG. Затем через CSS установить background (он может быть полупрозрачным, CSS градиентом, изображением, комбинацией нескольких фонов… чем угодно, что может предложить CSS) и свойство mask на элементе .box:

.box { /* any kind of background we wish */ mask: url(#m);
}

Установка инлайн SVG маски на HTML элементах работает только в Firefox на данный момент!

Установка радиуса круга через CSS

Необходимо удалить атрибут r с тега circle и задать его в CSS той же переменной, что и padding блока:

.box { padding: var(--r); } [id='c'] { r: var(--r); }

Таким образом при изменении значения —r обновятся радиус выреза и padding вокруг контента .box! Установка геометрических свойств для SVG элементов через CSS работает только в Blink браузерах на данный момент!

Комбинация двух предыдущих методов

Это круто, но, к сожалению, на данный момент невозможно на практике ни в одном браузере. Но можно сделать еще лучше!

Использование CSS градиентов для маскирования

CSS маскирование HTML элементов полностью не работает в Edge на данный момент, хотя статус у него для этого браузера In Development, и в about:flags добавлен соответствующий флаг (который сейчас ничего не делает).

Мы полностью вырываем SVG часть и начинаем строить нашу mask для CSS градиента. Создадим круги в углах с помощью радиальных градиентов. Следующий CSS создает круг радиуса —r в левом вернем углу блока:

.box { background: radial-gradient(circle at 0 0, #000 var(--r, 50px), transparent 0);
}

Смотрите в демо ниже, мы также указали блоку красную рамку, чтобы были видны границы:

Этот же градиент используем для нашей mask:

.box { /* same as before */ /* any CSS background we wish */ mask: radial-gradient(circle at 0 0, #000 var(--r, 50px), transparent 0);
}

В Webkit браузерах все еще необходим префикс –webkit- для свойств mask. Затем добавляем круги в другие углы:

$grad-list: radial-gradient(circle at 0 0 , #000 var(--r, 50px), transparent 0), radial-gradient(circle at 100% 0 , #000 var(--r, 50px), transparent 0), radial-gradient(circle at 0 100%, #000 var(--r, 50px), transparent 0), radial-gradient(circle at 100% 100%, #000 var(--r, 50px), transparent 0);
.box { /* same as before */ /* any CSS background we wish */ mask: $grad-list
}

Слишком много повторений. Давайте посмотрим, что можно сделать. Во-первых, используем CSS переменную для стоп-листа. Это удалит повторения в сгенерированном CSS.

$grad-list: radial-gradient(circle at 0 0 , var(--stop-list)), radial-gradient(circle at 100% 0 , var(--stop-list)), radial-gradient(circle at 0 100%, var(--stop-list)), radial-gradient(circle at 100% 100%, var(--stop-list)); .box { /* same as before */ /* any CSS background we wish */ --stop-list: #000 var(--r, 50px), transparent 0; mask: $grad-list;
}

Немногим лучше. Давайте генерировать углы в цикле:

$grad-list: (); @for $i from 0 to 4 { $grad-list: $grad-list, radial-gradient(circle at ($i%2)*100% floor($i/2)*100%, var(--stop-list));
} .box { /* same as before */ /* any CSS background we wish */ --stop-list: #000 var(--r, 50px), transparent 0; mask: $grad-list;
}

Намного лучше. Теперь нам не нужно писать один код несколько раз, тем самым рискуя не обновить что-то в будущем. Но результат все еще не тот, что мы ожидали:

Вырезаем все кроме углов – это противоположность того, что нам нужно. Можно перевернуть градиенты, сделать круги в углах transparent, а остальное – black:

--stop-list: transparent var(--r, 50px), #000 0;

Это работает, когда используется один градиент для одного угла:

Однако если добавить все 4 (или даже 2), мы получаем black прямоугольник размера нашего блока для mask. То есть ничего не маскируется.

Поэтому ограничиваем все эти градиенты на четверть блока – 50% по width и 50% по height или 25% (четверть) от области:

Это значит, что нужно задать mask-size 50% 50%, mask-repeat no-repeat и поместить все mask-image в нужные углы:

$grad-list: (); @for $i from 0 to 4 { $x: ($i%2)*100%; $y: floor($i/2)*100%; $grad-list: $grad-list radial-gradient(circle at $x $y, var(--stop-list)) /* mask image */ $x $y; /* mask position */
} .box { /* same as before */ /* any CSS background we wish */ --stop-list: transparent var(--r, 50px), #000 0; mask: $grad-list; mask-size: 50% 50%; mask-repeat: no-repeat;
}

Webkit браузерам все еще необходим префикс –webkit- для свойств mask. Но главная проблема (общая проблема с разделением и скруглением) заключается в том, что 4 четверти, собранные вместе, не всегда составляют всю область, из-за чего образуются пропуски между ними.

ОК, мы можем закрыть эти пропуски тонкими полосками linear-gradient() или увеличить mask-size до 51%:

Но, может, есть более элегантный способ?

Есть свойство mask-composite, которое может нам помочь, если задать его в значение intersect и вернуться обратно к полноразмерным слоям градиентов.

$grad-list: (); @for $i from 0 to 4 { $grad-list: $grad-list, radial-gradient(circle at ($i%2)*100% floor($i/2)*100%, var(--stop-list));
} .box { /* same as before */ /* any CSS background we wish */ --stop-list: transparent var(--r, 50px), #000 0; mask: $grad-list; mask-composite: exclude;
}

Очень круто, это же чистый CSS, не SVG. Минус в том, что поддержка только в Firefox 53+.

Тем не менее, это все же лучше, чем поддержка финального нашего варианта, когда дело доходит до вырезанных углов.

Способ corner-shape

Lea Verou додумалась до этой идеи примерно 5 лет назад и даже создала превью страницу. К сожалению, этот способ не реализован ни в одном браузере, а спецификация почти не продвинулась с того времени. Но не стоит забывать об этом решении в будущем – оно дает большую гибкость и маленький код. Мы могли бы воссоздать наш эффект так:

padding: var(--r);
corner-shape: scoop;
border-radius: var(--r);

Никакого избыточного HTML, никаких простыней из градиентов, только этот очень простой CSS. Т.е…. когда он наконец будет поддерживаться в браузерах!

Автор: Ana Tudor

Источник: https://css-tricks.com/

Редакция: Команда webformyself.