CSS-live.ru

Свойство background-clip и его применения

Перевод статьи The `background-clip` Property and its Use Cases с сайта css-tricks.com, переведено для css-live.ru с разрешения автора — Аны Тюдор.

background-clip — одно из тех свойств, о которых я давно знала, но почти не использовала. Может, лишь пару раз в ответах на Stack Overflow. До прошлого года, когда я начала создавать эту огромную коллекцию ползунковых регуляторов (слайдеров). Некоторые дизайны, что я решила воспроизвести, были довольно-таки сложны, а мне на каждый ползунок отводился один-единственный элемент, и им как назло оказался input, к которому даже псевдоэлементов толком не добавить. Хоть в некоторых браузерах такое и работает, то, что оно работает — на самом деле баг, и мне не хотелось на это полагаться. Так что в итоге мне пришлось использовать фоны, рамки и тени в изобилии. И я многому научилась при этом, и в этой статье делюсь некоторыми их тех уроков.

Первым делом давайте посмотрим, что такое background-clip и что оно делает.

На следующей картинке у нас блочная (или всё-таки боксовая? — прим. перев.) модель элемента.

блочная модель

Если padding равен 0, то padding-box по размеру точно равен content-box, и край контента совпадает с краем внутреннего отступа.

при padding: 0

Если border-width равен 0, то border-box по размеру точно равен padding-box, и край рамки совпадает с краем внутреннего отступа.

при border-width: 0

Если и padding, и border-width оба равны 0, то все три области (content-box, padding-box и border-box) равны по размеру, и края контента, внутреннего отступа и рамки все совпадают друг с другом.

при padding: 0 и border-width: 0

По умолчанию фоны покрывают всю border-box (под рамкой они тоже накладываются), но их background-position (а также проценты для background-size) отсчитываются от padding-box.

Чтобы лучше это понять, рассмотрим пример. Возьмем блок с наугад выбранными размерами, зададим ему простой градиентный фон с background-size: 50% 50% и заштрихованный border (с помощью border-image), так  что сквозь штрихи нам будет видно, что там под рамкой:

See the Pen default background-origin and background-clip (basic version) by CSS-Tricks (@css-tricks) on CodePen.

В этом примере видно, что градиентный фон целиком покрывает border-box (его видно под штрихованной рамкой). Мы не указывали ему background-position, так что у него значение по умолчанию — 0 0. Видно, что оно отсчитывается от padding-box, потому что начинается от левого верхнего угла (точки 0 0) этой области. Видно также, что background-size, указанный в процентах, отсчитывается от padding-box.

по умолчанию фон полностью покрывает border-box, но начинается в левой верхней точке padding-box

Задавая background-size для градиентов (но не для настоящих картинок), мы обычно вынуждены указывать два значения ради единообразия результата в браузерах. Если указать лишь одно значение, то второе в Firefox станет равным 100% (по спецификации), тогда как все остальные браузеры ошибочно делают второе значение равным первому. Пропущенное значение background-size принимается за auto, и так как у градиентов нет внутренних размеров и пропорций, значение auto никак из них не высчитать, так что оно должно восприниматься как 100%. Поэтому, если только мы не собираемся растягивать background-size на 100% по обоим измерениям, нам понадобятся два значения.

Задание background-size с одним значением не дает кроссбраузерного результата (можно проверить); слева: Firefox (по спецификации, второе значение считается равным 100%); справа: Chrome/ Opera, Safari, IE/ Edge (ошибочно делают второе значение равным первому)

Можно заставить фон покрывать только padding-box или только content-box с помощью background-clip. Обрезка (clipping) означает, что всё, что выступает за обрезаемую область, отсекается и не показывается. Обрезаемой областью на иллюстрации ниже будет то, что лежит внутри пунктирной линии.

иллюстрация, что такое обрезка

При background-clip: border-box (по умолчанию) обрезаемой областью будет border-box, так что у нас будет фон и под рамкой.

background-clip: border-box

Если задать background-clip: padding-box, обрезаемой областью будет padding-box, так что фон будет отображаться только в пределах padding-box (под рамкой его уже не будет).

background-clip: padding-box

И наконец, при background-clip: content-box обрезаемой областью будет content-box, так что фон будет показан только в пределах content-box.

background-clip: content-box

Эти три случая показаны в следующем живом примере:

See the Pen backgrounds — helper demo #2 by CSS-Tricks (@css-tricks) on CodePen.

У нас есть и еще одно свойство под названием background-origin, которое указывает, от какой из трех областей отсчитывается background-positionbackground-size, если выражен в процентах).

Допустим, у нас опять есть элемент с заштрихованным border, но, на этот раз, с видимым padding. Положим на фон обычную картинку и градиент. У обоих background-size: 50% 50%, и оба они не повторяются. В добавок к этому, у картинки будет background-position: 100% 100% (а для градиента оставим 0 0 по умолчанию):

background: linear-gradient(to right bottom,
     #e18728, #be4c39, #9351a6, #4472b9),
 url(tiger_lily.jpg) 100% 100%;
background-repeat: no-repeat;
background-size: 50% 50%;

Следующий пример показывает, что происходит при каждом из трех возможных значений background-origin — border-box, padding-box и content-box:

See the Pen backgrounds — helper demo #3 by CSS-Tricks (@css-tricks) on CodePen.

Значение 100% 100%, указанное для background-position настоящей картинки — это 100% 100% от области, указанной в background-origin. В то же время, 50% 50%, указанные для background-size, означают половину ширины и половину высоты области, указанной в background-origin.

В сокращенной записи background свойства background-origin и background-clip можно указывать именно в таком порядке в конце слоя. Поскольку они оба принимают значение вида *-box, то если указать лишь одно значение такого типа, оно будет присвоено сразу им обоим. Если указать два значения, background-origin получит первое, а background-clip — второе. Если не указывать значений вида *-box, оба свойства останутся со своими значениями по умолчанию (padding-box для background-origin и border-box для background-clip).

Чудесно! Посмотрим теперь, как использовать это в своих интересах!

Прозрачный зазор между рамкой и фоном

Кто-то наверняка вспомнит, что с background-clip можно делать полупрозрачные рамки. Но можно еще и добавить зазор между рамкой и областью, покрытой фоном, без дополнительного элемента. Простейший способ этого добиться — задать padding в дополнение к border и указать для background-clip значение content-box. Если сделать это сокращенной записью с единственным значением content-box, мы заодно установим значение content-box и для background-origin, но в данном случае это нормально, никаких побочных эффектов от него нет.

border: solid .5em #be4c39;
padding: .5em;
background: #e18728 content-box;

Обрезка фона по content-box значит, что он не распространится за края контента. Дальше их фона не будет, так что будет видно всё, что находится под нашим элементов. Добавка border значит, что мы увидим этот border в между краем внутреннего отступа и краем рамки. Но, поскольку padding ненулевой, у нас остается прозрачная область между краями контента и внутреннего отступа.

подсветка рамки, внутреннего отступа и области контента в отладчике

Можно проверить это вживую в этом примере на CodePen:

See the Pen space between border and background by Ana Tudor (@thebabydino) on CodePen.

Можно сделать результат еще интереснее, добавив фильтр drop-shadow(), который добавит всей конструкции желтоватое свечение:

See the Pen space between border and background v2 by Ana Tudor (@thebabydino) on CodePen.

Напоминание насчет префиксов: я видела множество ресурсов, добавляющих префиксы -moz- и -ms- для CSS-фильтров. Пожалуйста, не делайте этого! CSS-фильтры работают в Firefox без префикса с тех самых пор, как их там реализовали (Firefox 34, осенью 2014-го), а сейчас они появились в Edge за флагом — и тоже без префикса! Так что CSS-фильтрам никогда не были нужны префиксы -moz- и -ms-, добавлять их абсолютно бессмысленно, они будут лишь загромождать стили мертвым грузом.

Можно также получить клёвый визуальный эффект, применив градиент и для background-image, и для border-image. Сделаем градиент, начинающийся со сплошного оранжево-красного цвета вверху и переходящий в полную прозрачность внизу. Поскольку различаются лишь оттенки, а в остальном наши градиенты идентичны, создадим функцию Sass.

@function fade($c) {
 return linear-gradient($c, rgba($c, 0) 80%);
}

div {
 border: solid 0.125em;
 border-image: fade(#be4c39) 1;
 padding: 0.125em;
 background: fade(#e18728) content-box;
}

Можно проверить это в действии в этом примере на CodePen:

See the Pen space between border and background v3 by Ana Tudor (@thebabydino) on CodePen.

Этот подход, когда в качестве пустого пространства между фоном и рамкой служит внутренний отступ, трудно назвать лучшим, если только внутри не одна коротенькая надпись. Если текста больше… да уж, смотрится это паршиво.

See the Pen space between border and background v4 by Ana Tudor (@thebabydino) on CodePen.

Загвоздка в том, что текст начинается сразу от края оранжевого фона, а мы не можем добавить внутренний отступ, ведь он уже занят под прозрачный зазор. Можно было бы добавить лишний элемент… а можно воспользоваться box-shadow!

box-shadow может принимать 2, 3 либо 4 значения длины. Первое — смещение по x (определяющее, насколько сдвинуть тень вправо), второе — смещение по y (насколько сдвинуть тень вниз), третье — радиус размытия (определяет, насколько будет размыт край тени), и четвертое — радиус распространения (определяет, как далеко тень будет простираться во все стороны).

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

See the Pen how `box-shadow` works v2 by CSS-Tricks (@css-tricks) on CodePen.

Заметьте, что если радиус размытия не может быть меньше нуля, смещения и радиус распространения могут быть отрицательными. Отрицательное смещение просто сдвигает тень в обратную сторону по своей оси (влево или вверх). Отрицательный радиус распространения значит, что тень не расширяется, а сжимается.

Еще одна важная вещь, которую здесь надо отметить — поскольку она удобна для нашей задачи — то, что box-shadow никогда не отображается под border-box, даже когда эта область (полу)прозрачна.

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

имитация рамки с помощью box-shadow

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

Возвращаясь к нашему примеру, можно имитировать рамку с помощью box-shadow с распространением, реальный border превратить в прозрачный зазор, задать для background-clip значение padding-box, и применить  padding по назначению:

border: solid 1em transparent;
padding: 1em;
box-shadow: 0 0 0 1em #be4c39;
background: #e18728 padding-box;

подсветка областей рамки, внутреннего отступа и контента в отладчике

Это можно посмотреть в действии в следующем примере на CodePen:

Можно было бы имитировать рамку и внутренней тенью (с параметром inset). В этом случае она начинается от края внутреннего отступа (границы между областями padding и border), и распространяется на указанный радиус внутрь.

эмуляция второй рамки с помощью внутренней box-shadow

Поскольку теней может быть много, так можно имитировать несколько рамок. Пусть, к примеру, их у нас две, причем одна из них — inset. Если реальный border элемента ненулевой толщины и при этом transparent, а у background-clip стоит значение padding-box, то получится имитация двойной рамки с прозрачной областью (на месте реальной рамки) между внешней и внутренней частями. Обратите внимание, что ради этого понадобилось увеличить padding для компенсации места, занятого внутренней box-shadow.

border: solid 1em transparent;
padding: 2em; // увеличен с 1em до 2em для компенсации внутреннего "border"
box-shadow:
 0 0 0 0.25em #be4c39 /* внешний "border" */,
 inset 0 0 0 1em #be4c39 /* внутренний "border" */;
background: #e18728 padding-box;

Можно испытать это в действии в этом примере на CodePen, где добавлен еще фильтр drop-shadow() для «светящегося» эффекта.

See the Pen space between border and background v6 by Ana Tudor (@thebabydino) on CodePen.

Мишень из одного элемента (без псевдоэлементов) с гладкими краями

Допустим, нам нужно получить вот такую мишень, и при этом нам позволено использовать только один элемент без псевдоэлементов.

мишень, которую нам надо построить силами CSS

Первая мысль — воспользоваться repeating-radial-gradient. Структура нашей мишени примерно такая:

иллюстрация структуры мишени

Итак, половина мишени составит 9 единиц, т.е. размеры мишени по вертикали и горизонтали будут по 18 единиц. Наш повторяющийся радиальный градиент всю первую единицу черный, потом до третьей единицы (включительно — прим. перев.) прозрачный, потом опять черный и опять прозрачный… звучит как повторение. Вот только первый раз у нас черная область от 0 до 1, т.е. одна единица, а следующая черная область уже от 3 до 5 — две единицы! Так что… нам тут надо начать отсчет не от 0, а от -1, верно? Что ж, это должно работать, согласно спецификации.

$unit: 1em;

background: repeating-radial-gradient(
 #000 (-$unit), #000 $unit,
 transparent $unit, transparent 3*$unit
);

Этот пример на CodePen иллюстрирует принцип:

See the Pen repeating-radial-gradient for target by Ana Tudor (@thebabydino) on CodePen.

Первая загвоздка здесь в том, что у IE другое мнение, как это должно работать.

repeating-radial-gradient с отрицательной первой контрольной точкой: сравните ожидаемый результат (слева) и IE (справа)

К счастью, это исправили в Edge, но если нужна поддержка IE, проблема остается. Ее можно решить, используя обычный радиальный градиент, нам ведь всё равно не так уж много кругов и нужно. Кода больше, но всё не так уж плохо…

background: radial-gradient(
 #000 $unit, transparent 0,
 transparent 3*$unit, #000 0,
 #000 5*$unit, transparent 0,
 transparent 7*$unit, #000 0,
 #000 9*$unit, transparent 0
);

Можно увидеть его в действии в этом примере на CodePen:

See the Pen radial-gradient for target by Ana Tudor (@thebabydino) on CodePen.

Теперь окружности распределены во всех браузерах одинаково, но у нас осталась другая проблема: пусть края не слишком отличаются по гладкости от оригинальной картинки в IE/Edge, но в Firefox и Chrome они выглядят просто ужасно!

сравните оригинал (вверху слева), IE (вверху справа), Firefox (внизу слева) и Chrome (внизу справа)

Можно было бы применить трюк с нерезкой границей цветов:

background: radial-gradient(
 #000 calc(#{$unit} - 1px),
 transparent $unit,
 transparent calc(#{3*$unit} - 1px),
 #000 3*$unit,
 #000 calc(#{5*$unit} - 1px),
 transparent 5*$unit,
 transparent calc(#{7*$unit} - 1px),
 #000 7*$unit,
 #000 calc(#{9*$unit} - 1px),
 transparent 9*$unit
);

Посмотреть вживую:

See the Pen radial-gradient for target v2 by Ana Tudor (@thebabydino) on CodePen.

Что ж, в IE (где и так было неплохо) и Firefox стало лучше, но в Chrome края по-прежнему безобразные.

сравните оригинал (вверху слева), IE (вверху справа), Firefox (внизу слева) и Chrome (внизу справа)

Может быть, радиальные градиенты вообще не лучшее решение, в конце концов. А почему бы нам не приспособить сюда решение на background-clip и box-shadow из предыдущего раздела? С помощью внешней box-shadow мы можем сделать внешнее кольцо, а тенью с inset — внутреннее. Пространство между ними займет прозрачная рамка. Мы также установим для background-clip значение content-box и дадим элементу достаточный внутренний отступ, так что у нас получится прозрачная область между центральным кружком и внутренним кольцом.

border: solid 2*$unit transparent;
padding: 4*$unit;
width: 2*$unit; height: 2*$unit;
border-radius: 50%;
box-shadow:
 0 0 0 2*$unit #000,
 inset 0 0 0 2*$unit #000;
background: #000 content-box;

Посмотрите, как это работает, в следующем примере на CodePen, никаких «рваных» краев и прочих неприятностей:

See the Pen CSS target with smooth edges by CSS-Tricks (@css-tricks) on CodePen.

Реалистичные элементы управления

Я впервые это придумала, когда пыталась оформлять ручку ползунка от <input type=»range»>, дорожку ползунка и, для «невебкитных» браузеров, заполненную часть дорожки. Для этих деталей в браузерах предусмотрены псевдоэлементы.

Для полосы у нас есть -webkit-slider-runnable-track, -moz-range-track и -ms-track. Для ручки — -webkit-slider-thumb, -moz-range-thumb и -ms-thumb. И для заполненной части есть -moz-range-progress, -ms-fill-lower (оба для части слева от ручки) и -ms-fill-upper (справа от ручки). У WebKit-браузеров не предусмотрено псевдоэлемента, которым можно было бы оформить части ползунка до и после ручки по-разному.

Разнобой между этими псевдоэлементами выглядит ужасно, но еще ужаснее то, что мы не можем перечислить все разнобраузерные версии для одной детали, чтобы оформить их все сразу. Что-то вроде такого не заработает:

input[type='range']::-webkit-slider-thumb,
input[type='range']::-moz-range-thumb,
input[type='range']::-ms-thumb { /* styles here */ }

Нам приходится постоянно писать их так:

input[type='range']::-webkit-slider-thumb { /* styles here */ }
input[type='range']::-moz-range-thumb { /* styles here */ }
input[type='range']::-ms-thumb { /* styles here */ }

Что выглядит как механическое «переливание» в коде одной и той же «воды», и часто это так и есть — хотя при таком обилии разночтений между браузерами, как с ползунками, такие дубликаты полезны для сглаживания этих различий. Моим решением было использовать миксин thumb() и при необходимости передавать ему аргументы для обработки несоответствий. Например, как-то так:

@mixin thumb($flag: false) {
 /* styles */
       
 @if $flag { /* more styles */ }
}

input[type='range'] {
 &::-webkit-slider-thumb { @include thumb(true); }
 &::-moz-range-thumb { @include thumb(); }
 &::-ms-thumb { @include thumb(); }
}

Но вернемся к самим возможностям оформления. Добавлять псевдоэлементы к этим деталям можно только в Chrome и Opera, а это значит, что нам нужно будет максимально точно, как только возможно, воспроизвести их внешний вид, не полагаясь на псевдоэлементы. Так что у нас остаются только фоны, рамки, тени и фильтры для самого «элемента» ручки.

Давайте взглянем на несколько примеров!

Мягкий пластик

В качестве визуального примера представим себе что-то вроде ручки ползунка ниже:

ползунок с ручкой из мягкого пластика

На первый взгляд всё решение сводится к градиентному background и и градиенту для border-image, потом еще box-shadow для красоты — и такой элемент готов:

border: solid 0.375em;
border-image: linear-gradient(#fdfdfd, #c4c4c4) 1;
box-shadow: 0 0.375em 0.5em -0.125em #808080;
background: linear-gradient(#c5c5c5, #efefef);

И это даже работает (на примере элемента button вместо ручки ползунка ради простоты):

See the Pen soft plastic button (square) by Ana Tudor (@thebabydino) on CodePen.

За исключением того, что наша ручка круглая, а не квадратная. Нужно просто добавить ей border-radius: 50%, так? Так вот… это не сработает, потому что мы используем border-image, с которым border-radius самого элемента игнорируется, хотя, как это ни забавно, он по-прежнему действует на box-shadow, если она есть.

Что же делать? Точно, использовать background-clip! Сначала мы зададим элементу ненулевой padding, уберем рамку и скруглим его с помощью border-radius: 50%. Затем наложим два градиентных фона, обрезав верхний по content-box (обратите внимание, что обрезка входит в сокращенную запись background). Наконец, добавим две тени, одну темную, создающую собственно тень под ручкой, а вторую с inset, которая будет слегка затемнять низ и бока внешней части ручки.

border: none; /* makes border-box ≡ padding-box */
padding: .375em;
border-radius: 50%;
box-shadow: 0 .375em .5em -.125em #808080,
     inset 0 -.25em .5em -.125em #bbb;
background:
 linear-gradient(#c5c5c5, #efefef) content-box,
 linear-gradient(#fdfdfd, #c4c4c4);

Полюбоваться конечным результатом можно в этом примере на CodePen:

See the Pen soft plastic button (really round!) by Ana Tudor (@thebabydino) on CodePen.

Матовая ручка

Что-то вроде ручки ползунка со следующей картинки:

ползунок с матовой ручкой

Этот случай похож на предыдущий, только теперь у нас светлая линия вверху и внутренняя тень для средней части. Если использовать внешнюю box-shadow для создания светлой линии, вся конструкция потеряет круглую форму, если только заодно не уменьшить высоту для компенсации тени. А тогда потребуется больше расчетов для определения положения внутренней части. Если же сделать ее тенью с inset, то мы уже не сможем использовать ее для затемнения внутренней части. Однако, мы можем эмулировать ее с помощью radial-gradient, которому заданы подходящий размер, положение и обрезка по content-box. То есть делаем всё аналогично предыдущему примеру, но теперь у нас поверх остальных фонов будет наложен дополнительный radial-gradient. Фактический background-size этого радиального градиента больше, чем наш content-box, так что мы можем сдвинуть его вниз так, что его верхний край останется за пределами области контента.

border: none; /* теперь border-box ≡ padding-box */
padding: .625em;
width: 1.75em; height: 1.75em;
border-radius: 50%;
box-shadow:
 0 1px .125em #444 /* темная тень внизу */,
 inset 0 1px .125em #fff /* светлый блик наверху */;
background:
 /* эффект внутренней тени */
 radial-gradient(transparent 35%, #444)
    50% calc(50% + .125em) content-box,

 /* внутренний фон */
 linear-gradient(#bbb, #bbb) content-box,

 /* внешний фон */
 linear-gradient(#d0d3d5, #d2d5d7);
background-size:
 175% 175% /* размер с запасом для радиального градиента */,
 100% 100%, 100% 100%;

Можно посмотреть на это вживую в этом примере на Code Pen:

See the Pen matte button by Ana Tudor (@thebabydino) on CodePen.

Объемный элемент

Например, ручка следующего ползунка:

ползунок с объемной ручкой

Это уже посложнее, для этого все три области (content-box, padding-box и border-box) должны быть разными, чтобы мы могли накладывать фоны друг на друга и добиваться нужного эффекта с помощью background-clip.

Итак, для основной части ползунка у нас есть градиентный background, обрезанный по content-box, наложенный поверх другого, обрезанного по padding-box, и оба они поверх третьего linear-gradient, обрезанного по border-box. Мы таже задействовали внутреннюю box-shadow, чтобы подчеркнуть край внутреннего отступа (между padding и border):

 

border: solid .25em transparent;
padding: .25em;
border-radius: 1.375em;
box-shadow:
 inset 0 1px 1px rgba(#f7f7f7, .875) /* сверху */,
 inset 0 -1px 1px rgba(#bbb, .75) /* снизу */;
background:
 linear-gradient(#9ea1a6, #fdfdfe) content-box,
 linear-gradient(#fff, #9c9fa4) padding-box,
 linear-gradient(#eee, #a4a7ab) border-box;

Результат можно посмотреть на CodePen здесь:

See the Pen 3D button by Ana Tudor (@thebabydino) on CodePen.

Теперь остается маленькая круглая деталька. Это тот случай, когда я по-настоящему почувствовала потребность в псевдоэлементе. В итоге я и пошла по этому пути для Blink, но мне удалось сделать так, что запасной вариант для других браузеров — из наложенных друг на друга двух радиальных градиентов поверх линейных — стал выглядеть прилично:

See the Pen 3D button #2 by Ana Tudor (@thebabydino) on CodePen.

Можно было бы еще приблизиться к нашей цели, лучше подобрав оттенки, добавив еще радиальных градиентов или даже воспользовавшись background-blend-mode, но на подобное мне не хватает художественного чутья.

С псевдэлементом добиться нужного результата намного проще — сначала нужно задать ему правильные размер и положение, закруглить его с помощью border-radius: 50%. Затем задать ему padding, никакой рамки, и использовать два градиента в background, верхний из которых радиальный и обрезан по content-box:

padding: .125em;
background:
 radial-gradient(circle at 50% 10%,
     #f7f8fa, #9a9b9f) content-box,
 linear-gradient(#ddd, #bbb);

Посмотреть, что получилось, можно в следующем примере:

See the Pen 3D button #3 by Ana Tudor (@thebabydino) on CodePen.

Для ручки реального ползунка я использовала тот же самый background с радиальными градиентами сверху для всех браузеров, а для Blink добавила псевдоэлемент прямо поверх этих радиальных градиентов. Это потому, что стили ручки ползунка в Safari применяются к ::-webkit-slider-thumb, но Safari не поддерживает псевдоэлементов для ручки (или дорожки). Так что если бы я убрала запасной вариант из стилей для ::-webkit-slider-thumb, то Safari не отобразил бы круглую детальку вообще.

Иллюзия глубины

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

ползунок с дорожкой, уходящей в глубину

Как и раньше, для этого мы задаем элементу ненулевой прозрачный border, padding, и накладываем друг на друга фоны с разными значениями background-clip (помните, что слои со значением content-box должны лежать поверх слоев со значением padding-box, а те — поверх слоев со значением border-box). В данном случае у нас есть более светлый linear-gradient для заливки области border, более темный — плюс пара уменьшенных неповторяющихся радиальных, чтобы добавить еще темноты по краям — для области padding, и совсем тёмный для области контента. Затем мы задаем этой конструкции border-radius, равный как минимум половине от суммы height области контента, удвоенного padding и удвоенного border. И еще добавим внутреннюю box-shadow, чтоб слегка подчеркнуть край внутреннего отступа.

border: solid .375em transparent;
padding: 1em 3em;
width: 15.75em; height: 1.75em;
border-radius: 2.25em;
background:
 linear-gradient(#090909, #090909) content-box,
 radial-gradient(at 5% 40%, #0b0b0b, transparent 70%)
    no-repeat 0 35% padding-box /* левый край */,
 radial-gradient(at 95% 40%, #111, transparent 70%)
    no-repeat 100% 35% padding-box /* правый край */,
 linear-gradient(90deg, #3a3a3a, #161616) padding-box,
 linear-gradient(90deg, #2b2d2c, #2a2c2b) border-box;
background-size: 100%, 9em 4.5em, 4.5em 4.5em, 100%, 100%;

Но тут есть проблема, и ее видно в следующем примере на CodePen:

See the Pen illusion of depth by Ana Tudor (@thebabydino) on CodePen.

Из-за того, как работает border-radius — радиус области контента получается из указанного вычитанием border-width и padding, что дает в итоге отрицательное число — углы области контента не закругляются. Что ж, мы можем это исправить! Можно эмулировать нужную нам форму, сочетая линейные, и радиальные градиенты для области контента.

Делается это так: сначала убедимся, что width нашей области контента кратна ее height (в нашем случае, 15.75em = 9*1.75em). Нижним слоем положим  linear-gradient по центру так, чтобы по вертикали он покрывал всю height области контента, но оставляет зазоры по половине ее height от обоих боковых краев. Поверх него накладываем radial-gradient, background-size, равным height области контента и по горизонтали, и по вертикали.

See the Pen illusion of depth #2 by Ana Tudor (@thebabydino) on CodePen.

Металлические элементы управления

Например, что-то вроде кнопки, изображенной ниже:

металлический элемент управления

Это уже чуть сложнее, так что давайте разберем его по частям. Прежде всего, мы делаем кнопку круглой, задав ей равные width и height и установив border-radius: 50%. Затем убеждаемся, что ей задан box-sizing: border-box, чтобы border-width и padding отсчитывались внутрь (вычитались из размеров, что мы указали). Теперь зададим ей прозрачную рамку и внутренний отступ. И вот что у нас получилось на данный момент:

$d-btn: 27em; /* диаметр кнопки */
$bw: 1.5em; /* border-width */

button {
 box-sizing: border-box;
 border: solid $bw transparent;
 padding: 1.5em;
 width: $d-btn; height: $d-btn;
 border-radius: 50%;
}

Пока еще это ни на что не похоже, потому что весь общий вид достигается с помощью двух свойств, которые мы еще не добавили: box-shadow и background.

See the Pen metallic control — step 0 by Ana Tudor (@thebabydino) on CodePen.

Прежде чем двигаться куда-то дальше, давайте немного разберем, из чего состоит элемент:

составные части металлического элемента управления

В порядке от самой внешней детали к центру, у нас есть:

  • широкое внешнее кольцо со светодиодами
  • тонкое внутреннее кольцо
  • решетчатая область (она же включает в себя голубое свечение вокруг следующей внутренней части)
  • большая центральная деталь

Широкое внешнее кольцо — это область рамки, центральная часть — область контента, а всё, что между ними (тонкое внутреннее кольцо и решетчатая часть) — область внутреннего отступа. Тонкое внутреннее кольцо мы сделаем внутренней тенью:

box-shadow:
 /* прерывистая темная тень, отделяющая от внешнего кольца */
 inset 0 0 1px #666,

 /* темная область вверху */
 inset 0 1px .125em #8b8b8b,
 inset 0 2px .25em #a4a2a3,

 /* темная область внизу */
 inset 0 -1px .125em #8b8b8b,
 inset 0 -2px .25em #a4a2a3,

 /* основная полоса по окружности внутреннего кольца */
 inset 0 0 0 .375em #cdcdcd;

Добавим еще две внешних тени, одну для светлого верхнего блика на внешнем кольце и другую для кусочка темной тени под элементом, итого у нас уже есть:

box-shadow:
 0 -1px 1px #eee,
 0 2px 2px #1d1d1d,
 inset 0 0 1px #666,
 inset 0 1px .125em #8b8b8b,
 inset 0 2px .25em #a4a2a3,
 inset 0 -1px .125em #8b8b8b,
 inset 0 -2px .25em #a4a2a3,
 inset 0 0 0 .375em #cdcdcd;

До цели еще далековато, но уже хоть что-то.

See the Pen metallic control — step 1 by Ana Tudor (@thebabydino) on CodePen.

Теперь нам нужно наложить друг на друга три типа фонов, сверху вниз: ограниченный по content-box (образующий центральную область), ограниченный по padding-box (образующий решетчатую область и голубое свечение) и ограниченный по border-box (образующий широкое внешнее кольцо и светодиоды).

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

background:
 /* ======= content-box ======= */
 /* окружности - используем простые числа 13, 19, 23 */
 repeating-radial-gradient(
     rgba(#e4e4e4, 0) 0,
     rgba(#e4e4e4, 0) 23px,
     rgba(#e4e4e4, .05) 25px,
     rgba(#e4e4e4, 0) 27px) content-box,
 repeating-radial-gradient(
     rgba(#a6a6a6, 0) 0,
     rgba(#a6a6a6, 0) 13px,
     rgba(#a6a6a6, .05) 15px,
     rgba(#a6a6a6, 0) 17px) content-box,
 repeating-radial-gradient(
     rgba(#8b8b8b, 0) 0,
     rgba(#8b8b8b, 0) 19px,
     rgba(#8b8b8b, .05) 21px,
     rgba(#8b8b8b, 0) 23px) content-box,
 /* конические отражения */
 conic-gradient(/* случайные варианты нескольких оттенков серого */
     #cdcdcd, #9d9d9d, #808080,
     #bcbcbc, #c4c4c4, #e6e6e6,
     #dddddd, #a1a1a1, #7f7f7f,
     #8b8b8b, #bfbfbf, #e3e3e3,
     #d2d2d2, #a6a6a6, #858585,
     #8d8d8d, #c0c0c0, #e5e5e5,
     #d6d6d6, #9e9e9e, #828282,
     #8f8f8f, #bdbdbd, #e3e3e3, #cdcdcd)
    content-box;

И наконец это становится на что-то похоже!

See the Pen metallic control — step 2 by Ana Tudor (@thebabydino) on CodePen.

Переходим к решетчатой области. Голубое свечение — просто радиальный градиент, сходящий к краям в прозрачность, а вот в основе отверстий лежит паттерн «Карбоновое волокно» из галереи, которую Лиа Веру сделала целых пять лет назад — но всё еще чертовски полезной для людей с ограниченными художественными способностями вроде меня.

$d-hole: 1.25em; /* диаметр отверстий */
$r-hole: .5*$d-hole; /* радиус отверстий */

background:
 /* ======= padding-box ======= */
 /* голубое свечение */
 radial-gradient(
     #00d7ff 53%, transparent 65%) padding-box,
 /* отверстия */
 radial-gradient(
     #272727 20%, transparent 25%)
    0 0 / #{$d-hole} #{$d-hole}
    padding-box,
 radial-gradient(
     #272727 20%, transparent 25%)
    $r-hole $r-hole / #{$d-hole} #{$d-hole}
    padding-box,
 radial-gradient(#444 20%, transparent 28%)
    0 .125em / #{$d-hole} #{$d-hole}
    padding-box,
 radial-gradient(#444 20%, #3d3d3d 28%)
    #{$r-hole} #{$r-hole + .125em} / #{$d-hole} #{$d-hole}
    padding-box

Кажется, у нас начинает вырисовываться что-то приличное:

See the Pen metallic control — step 3 by Ana Tudor (@thebabydino) on CodePen.

Основа широкого внешнего кольца (без светодиодов) делается одним коническим градиентом:

conic-gradient(
 #b5b5b5, #8d8d8d, #838383,
 #ababab, #d7d7d7, #e3e3e3,
 #aeaeae, #8f8f8f, #878787,
 #acacac, #d7d7d7, #dddddd,
 #b8b8b8, #8e8e8e, #848484,
 #a6a6a6, #d8d8d8, #e3e3e3,
 #8e8e8e, #868686, #a8a8a8,
 #d5d5d5, #dedede, #b5b5b5) border-box;

И вот у нас получился металлический элемент!

See the Pen metallic control — step 4 by Ana Tudor (@thebabydino) on CodePen.

Но на нем пока нет светодиодов, исправим же это поскорее!

Каждый светодиод сделан из двух неповторяющихся радиальных градиентов, наложенных один на другой. Верхний — сам светодиод, а нижний, слегка сдвинутый по вертикали, создает светлый отблеск в нижней части светодиода. Это практически тот же эффект, что применен для отверстий в решетчатой части. Нижний градиент всегда одинаков, а вот верхний меняется в зависимости от того, включен светодиод или выключен.

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

У нас 24 светодиода, размещенных по окружности, проходящей по середине области рамки. Так что ее радиус — это радиус элемента минус половина толщины рамки.

Будем генерировать все эти градиенты с помощью Sass. Сначала создадим пустой список градиентов, затем пройдемся по нему циклом и на каждой итерации будем добавлять в список два градиента. Их координаты рассчитаны так, что они окажутся на вышеупомянутой окружности. Первый градиент зависит от счетчика цикла, а второй всегда одинаков (разве что с другими координатами на окружности).

$d-btn: 27em;
$bw: 1.5em;
$r-pos: .5*($d-btn - $bw);
$n-leds: 24;
$ba-led: 360deg/$n-leds;
$d-led: 1em;
$r-led: .5*$d-led;
$k: 7;
$leds: ();

@for $i from 0 to $n-leds {
 $a: $i*$ba-led - 90deg;
 $x: .5*$d-btn + $r-pos*cos($a) - $r-led;
 $y: .5*$d-btn + $r-pos*sin($a) - $r-led;
 $leds: $leds,
    if($i < $k,
     (radial-gradient(circle, #01d6ff,
         #178b98 .5*$r-led,
         rgba(#01d6ff, .35) .7*$r-led,
         rgba(#01d6ff, 0) 1.3*$r-led) no-repeat
       #{$x - $r-led} #{$y - $r-led} /
       #{2*$d-led} #{2*$d-led} border-box),
     (radial-gradient(circle, #898989,
         #4d4d4d .5*$r-led, #999 .65*$r-led,
         rgba(#999, 0) .7*$r-led) no-repeat
       $x $y / #{$d-led} #{$d-led} border-box)
    ),
    radial-gradient(circle,
       rgba(#e8e8e8, .5) .5*$r-led,
       rgba(#e8e8e8, 0) .7*$r-led) no-repeat
     $x ($y + .125em) / #{$d-led} #{$d-led}
     border-box;
}

Окончательным результатом можно полюбоваться в этом примере:

See the Pen 1 element (no pseudos) metallic control by Ana Tudor (@thebabydino) on CodePen.

Тени на перпендикулярной плоскости

Рассмотрим пример, когда есть элементы управления в вертикальной плоскости, и нам нужна тень на горизонтальной плоскости под ними. Что-то наподобие следующей картинки:

элементы с тенью на горизонтальной плоскости под ними

Нам нужно воспроизвести этот эффект с помощью всего одного элемента, без псевдоэлементов.

Наложение фонов с разными значениями background-clip и background-origin выручает и в этом случае. Саму кнопку мы строим двумя фонами, верхний из которых обрезан по content-box, а тот, что под ним — по padding-box, и используем фоновый radial-gradient() со значением border-box для background-clip и background-origin, чтобы сделать тень.

Базовое оформление очень похоже на оформление металлического элемента из прошлого раздела:

$l: 6.25em;
$bw: .1*$l;

border: solid $bw transparent;
padding: 3px;
width: $l; height: $l;
border-radius: 1.75*$bw;

Мы задаем довольно широкую прозрачную рамку вокруг кнопки, чтобы нам хватило места для создания этой тени в нижней части рамки. Делаем это для всех сторон рамки, а не только для нижней, потому что нам нужно, чтобы все углы у padding-box закруглялись одинаково и симметрично (если вам нужно освежить в памяти, как это работает, посмотрите потрясающий доклад о border-radius Лии Веру).

See the Pen border-width & padding-box corner rounding by Ana Tudor (@thebabydino) on CodePen.

Первый background наверху — это conic-gradient() для создания металлического отлива. Он обрезан по content-box. Прямо под ним у нас linear-gradient(), обрезанный по padding-box. Мы используем три внутренние тени, чтобы сделать этот второй фон менее плоским — добавляем еще один оттенок вокруг него с нулевым размытием, осветляем его вверху полупрозрачной белой тенью и затемняем внизу полпрозрачной черной тенью.

box-shadow:
 inset 0 0 0 1px #eedc00,
 inset 0  1px 2px rgba(#fff, .5),
 inset 0 -1px 2px rgba(#000, .5);
background:
 conic-gradient(
     #edc800, #e3b600, #f3cf00, #ffe800,
     #ffe900, #ffeb00, #ffe000, #ebc500,
     #e0b100, #f1cc00, #fcdc00, #ffe500,
     #fad900, #eec200, #e7b900, #f7d300,
     #ffe800, #ffe300, #f5d100, #e6b900,
     #e3b600, #f4d000, #ffe400, #ebc600,
     #e3b600, #f6d500, #ffe900, #ffe90a,
     #edc800) content-box,
 linear-gradient(#f6d600, #f6d600) padding-box

Это дает нам металлическую кнопку (пока без тени):

See the Pen metallic button — no shadow by Ana Tudor (@thebabydino) on CodePen.

Для тени добавим третий слой фона, для которого мы зададим обоим свойствам background-clip и background-origin значение border-box. Этим фоном будет неповторяющийся radial-gradient(), положение которого мы привяжем к bottom (а по горизонтали к середине) и который мы сожмем по вертикали так, что он поместится внутри нижней стороны рамки и даже еще немного места останется — пусть он будет размером, скажем, где-то .75 от значения border-width.

radial-gradient(rgba(#787878, .9), rgba(#787878, 0) 70%)
 50% bottom / 80% .75*$bw no-repeat border-box

И всё! Можете поиграть с этими кнопками в следующем примере на CodePen:

See the Pen metallic buttons with shadow (no pseudos) by Ana Tudor (@thebabydino) on CodePen.

 

Для свойства background-clip наверняка найдётся множество применений! Особенно при наложении разных эффектов по краям элементов.

P.S. Это тоже может быть интересно:

14 комментариев

  1. Спасибо за перевод!
    Интересная статья, а количество кода для кастомизации слайдера совсем не радует. В связи с этим возник вопрос.
    Илья, а Вы не в курсе, есть ли какие-то попытки «стандартизации» input’ов ? У каждого браузера свой путь. И стилизация input’ов дело трудоемкое, а местами практически невозможное (.

  2. Интересная тема… Возможно, тянет на цикл статей «исследуем возможности градиента в CSS»…

  3. Вроде бы такая мелочь, а тут…

    P.S. добавьте уже авторизацию через соцсети для комментирования.

  4. Разбираюсь с иллюзией глубины, вариант, предложенный автором, в Хроме даёт сбой.
    Мне вообще интересно, почему ни так, ни так, ни даже так — не получается нормального результата, тестирую в Хроме и Лисе, особенно как-то криво обрезает Хром…
    PS: немного жаль что синтаксис at point в радиальном градиенте не даёт возможности работать с calc, а также позиционируется только от левого верхнего угла…

  5. Вот, в частности, в таком варианте в Хроме «выровнено» по верхней границе(причём низ у кружочков, как буд-то обрезан), в Лисе — по нижней (там просто «не дотягивается»).

    1. Да, округление при отрисовке градиентов (и теней), строящихся по разным алгоритмам — это давняя проблема кроссбраузерности. Вот так вроде удается состыковать в Хроме (под виндой), но подобранная методом тыка поправка в четверть пикселя удручает (а в Фоксе она лишняя — там центрирование через background-position справляется, но остается небольшая нестыковочка радиуса). Всё-таки это больше «проверка идеи», на практике SVG-фон будет надежнее.

      1. Вот-вот — в Фоксе получается немного кривовато.
        Если на глаз, то в Фоксе вот так вроде работает, тут два в одном: левый и правый шарик подпорот разными костылями :-)

  6. А если пойти по пути автора, но немного видоизменить код то, в принципе, более-менее всё работает. Но если мы закомментируем с 11 по 16 строчки, то увидим, что всё равно работает как-то не сглажено… Да и вообще не совсем понятно откуда и почему она берёт эти цифры: 1.75em как общий размер градиента и 70% от него, как цветная зона, получается — радиус. 1.75em x 70% = 1.225em, а поскольку высота 1.75em равна диаметру, радиус должен быть 1.75/2 = .875em. Почему тогда 1.225em?
    Автор почему-то подробно разжёвывает в начале статьи очевидные вещи, а то, откуда и почему у неё берутся те или иные цифры, каким образом и что отражается в её синтаксисе почему-то не считает нужным подробно пояснять, хотя в этом случае нужно намного больше разбираться, чтоб не соскочить с нити изложения…

    1. Вот ещё https://jsfiddle.net/Launder/d36wL7g2/2/«>чуть-чуть видоизменённый код и опять-таки непонятно, какого Хром там что-то вырезает. Размер «рисунка» высоте, она же — диаметр = двум радиусам. Радиус задан как размер градиента. Что тут ещё нужно добавить, чтоб браузер адекватно это проинтерпретировал?

    2. По умолчанию, если явно не указаны ни позиция, ни радиус, ни ключевое слово типа closest-side, радиус градиента считается от центра до угла (позиция по умолчанию — центр, а радиус по умолчанию — farthest-corner). Поэтому эти 70% берутся от 0.875em*√2 (от диагонали квадрата со стороной в половину высоты градиента), т.е. от 1.237em — получается 0.866em, как раз чуточку меньше стороны. Вообще варианты синтаксиса радиального градиента очень головоломные. Спасибо за хороший повод заново проштудировать их спецификацию!

  7. Каким образом вот тут:

    repeating-radial-gradient(transparent 23px, rgba(228, 228, 228, 0.5) 25px, transparent 27px)
    , repeating-radial-gradient(transparent 13px, rgba(166, 166, 166, 0.5) 15px, transparent 17px)
    , repeating-radial-gradient(transparent 19px, rgba(139, 139, 139, 0.5) 21px, transparent 23px)

    создаётся эффект тоже не ясно. Я как ни пытался разглядеть больше двух цветов — не получилось. Если последовательно отключать каждый из этих градиентов — немного меняется цвет колец. Создаётся впечатление, что если подобрать правильный цвет почитав внимательно например вот это то можно обойтись одним градиентом — да и это вполне по логике: ведь все градиенты тут два пикселя растут в нужный цвет, два пикселя падают обратно в прозрачность (интересно, кстати, как тут раскрашиваются конкретные пиксели для обычных экранов и HD, интересно, кстати, для простоты, как бы в этом случае, вёл бы себя линейный градиент), итого, вроде как на кольцо уходят 4px. Поскольку градиент повторяющийся во все стороны, и полупрозрачный, очевидно, что они накладываются друг на друга.
    Отсюда вопрос: зачем так усложнять, почему нельзя было использовать один цвет?
    И второе: откуда берётся вот этот эффект? В Хроме и Лисе он присутствует.

    1. Принцип Цикад, изложенный в этой статье, на которую ссылается автор, довольно интересен. Честно говоря, более-менее понял лишь первый пример, где автор играясь с простыми числами делает так, чтоб три фона накладывались друг на друга неповторяющимся образом.
      Берётся три полоски, где часть составляет, собственно цвет, часть прозрачная. За счёт того, что используются простые числа (то есть делятся на самих себя и на единицу, в простоте — неделимые), не получится подобрать целое сочетание двух любых полосок, чтоб получилась третья. для этого придётся перемножить их, а это получится очень большая текстура. в иных случаях всегда будет оставаться небольшой зазор, в который влезет третья полосочка и ширина полосочек всё время разная. Получается рисунок из полосочек не повторяется для любого доступного широкого экрана. (кстати, полоски там, похоже, тоже полупрозрачные, в результате образуются ещё различные сочетания начальных трёх цветов ну и когда попадает «прозрачная» полоса — получается ещё белая полосочка, цвет фона браузера).
      Второй и третий пример я не очень понял и буду благодарен, если кто-нибудь его подробно объяснит :-) Что же касается радиального градиента, то пример в котором радиальный градиент четырёх составный:

      repeating-radial-gradient(
      rgba(#e4e4e4, 0) 0,
      rgba(#e4e4e4, 0) 23px,
      rgba(#e4e4e4, .05) 25px,
      rgba(#e4e4e4, 0) 27px)

      (код я так понимаю, не CSS, а SCSS, впрочем он легко читается), а в песочнице градиент трёхсоставный:

      rgba(#e4e4e4, 0) 23px,
      rgba(#e4e4e4, .05) 25px,
      rgba(#e4e4e4, 0) 27px)

      из-за отсутствия прозрачного участка между нулём и 23px смешения с соседними слоями не получится…

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.