Модный переключатель

Большой молодец Рома Комаров прочитал на Web Standards Days в Москве доклад-размышление «Бесчеловечные эксперименты над CSS», суть которого я вам пересказывать не буду — лучше посмотрите сами. В частности, Рома призывал, при виде новой сногсшибательной CSS-демки, не смотреть в код, пытаясь понять как она сделана, а пытаться реализовать её самому. «Отличная идея!» — подумал я тогда. И вот сегодня утром, глядя на «Переключатель в стиле iOS на CSS» Александра Шабуневича, не удержался, чтобы не сделать свой вариант и рассказать вам подробно о том, как это работает.

Хочу предупредить заранее, что я не большой любитель демок, в которых всего 20 000 элементов <b> и 3 000 строк грязного CSS создают что-нибудь невероятно красивое и столь же бесполезное. Я конечно уже был замечен за «Чаем со спецэффектами», но ту демку хотя бы можно было разобрать на полезные каждый день части. С этим переключателем получилось ещё лучше. Поехали!

HTML-скелет

Результат по ссылке.

Прежде чем рваться в бой, вкладывая <div> в <div> и оборачивая это в ещё один <div>, давайте подумаем, на что больше всего похож этот переключатель. Если посмотреть на него внимательно, то видно, что у него всего два положения. И пусть вас не смущает внешний вид этого переключателя, это просто Выкл. и Вкл., а сам он уместно смотрелся бы в составе какой-нибудь формы. Александр, в своей демке, решил, что это скорее две радиокнопки <input type="radio"> с двумя отдельными лейблами и одинаковым атрибутом name, значит при включении одной, тут же выключается вторая. Что ж, может быть. Но мне показалось, что это скорее чекбокс, у которого тоже может быть два состояния.

	<div class="switch">
	    <input type="checkbox" id="switch" class="switch-check">
	    <label for="switch" class="switch-label">Опция</label>
	</div>

Раздадим всем элементам классы в пространстве имён родителя: switch, switch-check и switch-label и не забудем связать лейбл и чекбокс с помощью атрибутов id и for.

Делаем красиво

Пока наш переключатель выглядит просто и удобно: plain.html. И вроде бы: чекбокс и лейбл — жми и поехали, хватит время терять. Но мы здесь делаем красиво, поэтому нам сначала нужно спрятать всё, что некрасиво. Вместо того, чтобы шаманить с нестандартным свойством appearance, мы просто спрячем чекбокс с помощью opacity:0 (чтобы на него всё ещё можно было перейти табом — visibility:hidden это отключает) и уберём текст лейбла с помощью text-indent, само тело лейбла нам ещё пригодится.

	.switch-check {
	    position:absolute;
	    opacity:0;
	    }
	.switch-label {
	    text-indent:-9999px;
	    }

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

Белая и синяя формы переключателя.

Если не заниматься попиксельной имитацией того, что нарисовали дизайнеры Apple для iOS, то кнопка легко разбивается на CSS-примитивы. Если заниматься, то вы просто потратите на день или два больше. Я осознанно отбросил некоторые несущественные или слишком сложные для реализации мелочи, чтобы не усыплять вас их описанием.

Сначала давайте нарисуем форму, которая является основой синей и белой части нашего переключателя. Видно, что отличаются они только фоновым цветом. Задаём скругление в половину высоты с помощью border-radius, базовый синий или белый цвет, а всю остальную красоту рисуем последовательно с помощью четырёх внутренних теней, в порядке следования в блоке box-shadow:

  1. Первая двухпиксельная тень без размытия: третий параметр равен нулю и четвёртый только добавляет смещения чёрной тени с прозрачностью 10%;
  2. Вторая простая четырёхпиксельная тень, дающая основное углубление на половину чёрным;
  3. Третья четырёхписельная тень нависающая сверху на 5 пикселей на треть чёрным;
  4. Четвёртая тень без размытия, со смещением сверху на половину высоты переключателя всего на 7 сотых чёрного поверх основного фона.
	.switch {
	    width:154px;
	    height:54px;
	    border-radius:27px;
	    background:#369AEE;
	    box-shadow:
	        0 0 0 2px rgba(0, 0, 0, 0.1) inset,
	        0 0 4px rgba(0, 0, 0, 0.5) inset,
	        0 5px 4px 1px rgba(0, 0, 0, 0.3) inset,
	        0 27px 0 rgba(0, 0, 0, 0.07) inset;
	    }

Размеры 154×54 пикселей я взял прямо из iOS — именно такие размеры имеет переключатель в версии для ретины. В итоге получается уже что-то похожее на правду: shape.html.

Придумываем слайдер

Сейчас нам нужно решить, как именно будет ездить наш слайдер. В оригинальном переключателе iOS к круглой ручке переключателя как будто прикреплены два блока: синий слева и белый справа. Передвижение слайдера как будто уводит каждый из них за пределы отверстия, которым является переключатель.

К сожалению, из-за особенностей работы overflow:hidden и border-radius, у нас не получится кроссбраузерно повторить то, что говорит нам понимание происходящего. Браузеры очень по-разному трактуют скрытие за радиусом скругления. Но мы можем сделать так, чтобы это выглядело в точности так, как нам нужно.

Но для этого нам понадобятся два дополнительных элемента. Сам лейбл мы, к сожалению, использовать не сможем — он должен стоять на месте и передавать клики на чекбокс, который и будет контролировать поведение нашего слайдера. Поэтому вложим в слайдер два элемента switch-slider с подклассами -on и -off:

	<div class="switch">
	    <input type="checkbox" id="switch" class="switch-check">
	    <label for="switch" class="switch-label">
	        Опция
	        <span class="switch-slider switch-slider-on"></span>
	        <span class="switch-slider switch-slider-off"></span>
	    </label>
	</div>

Элемент -on будет отвечать за синий блок слева от ручки, а -off за белый справа. В синий мы перенесём тот фон, который с самого начала был на самом переключателе. В элемент -off мы как раз и впишем нашу ручку с градиентом и парой теней для объёма, поскольку он следует последним и оказывается сверху:

	.switch-slider-off:after {
	    position:absolute;
	    top:1px;
	    left:1px;
	    width:52px;
	    height:52px;
	    border-radius:50%;
	    background:#E5E5E5;
	    background:linear-gradient(#D0D0D0, #FDFDFD);
	    box-shadow:
	        0 0 2px 2px #FFF inset,
	        0 0 4px 1px rgba(0, 0, 0, 0.6);
	    content:'';
	    }

Остаётся только добавить иконки, обозначающие состояния переключателя. Сначала я сверстал их с помощью ещё одних псевдоэлементов, чтобы похвастать потом «Смотрите, только CSS!» Но это снова привело к проблемам со скрытием объектов за border-radius и я позволил себе вставить эти картинки графикой. Но графикой не простой, а векторной. В итоге к добавились два файла: off.svg и on.svg, которые прекрасно масштабируются и выглядят шикарно на новых дисплеях. При желании, для этих векторых иконок легко делается фолбек в растр, подробнее об этом можно прочитать в начале прошлой заметки «Непростая простая кнопка».

Иконка для белого блока указывается фоновой картинкой с отступом слева, а вот иконка для синего блока должна прикрепляться к правому краю (ниже вы поймёте почему), но отступать от него на фиксированное количество пикселов. Поэтому мы задаём смещение фона для синей иконки 100% 12px и добавляем нужный отступ прямо в SVG-файле, меняя его ширину. Не очень изящно, зато вполне кроссбраузерно.

Анимация слайдера

Схематичная анимация слайдера. Осторожно, не залипните.

Теперь у нас готовы все элементы и пора бы уже сделать так, чтобы переключатель заработал. Работать он будет с помощью псевдокласса :checked, который будет срабатывать при включённом чекбоксе и передавать это состояние с помощью сестринского селектора E + E и дальше прямо к слайдерам.

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

	.switch-check:checked + .switch-label .switch-slider-on {
	    width:154px;
	    }
	.switch-check:checked + .switch-label .switch-slider-off {
	    width:54px;
	    }

А чтобы изменение ширины слайдеров не происходило одним прыжком, опишем плавный переход от одной ширины к другой с помощью transition и не забудем, что у активной ручки слайдера фон и градиент немного темнее, и что по слайдеру можно не просто кликнуть, но ещё и выделить его табом и нажать пробел для переключения:

	.switch-slider {
	    transition:width 0.2s linear;
	    }
	.switch-label:active .switch-slider-off:after,
	.switch-check:focus + .switch-label .switch-slider-off:after {
	    background:#D5D5D5;
	    background:linear-gradient(#C8C8C8, #E4E4E4);
	    }

Ура, переключатель заработал! Смотрите сами: check.html. И вроде бы всё, но зуд всё не унимался и я сделал ещё одну версию с помощью двух радиокнопок и двух лейблов, что в итоге несколько усложнило код, ведь пришлось на ходу менять z-index лейблов в каждом из состояний: radio.html. Правда, это делает слайдер недоступным для фокусировки табом, ведь радиокнопки две, а он один.

А теперь давайте посмотрим на то, ради чего была вся эта возня с CSS и SVG вместо того, чтобы сделать всё двумя-тремя обычными картинками. Зайдём в браузер и сильно увеличим наш переключатель:

Переключатель, сильно увеличенный средствами браузера. Красиво, да?

И вот теперь точно всё. Можно было бы конечно вытащить текст лейбла влево в виде подписи, добить совместимость с IE7 и может быть поработать над универсальным способом менять размер переключателя. Но я оставлю эти эксперименты вам, моим внимательным и пытливым читателям.

Комментарии

35

А почему в on.svg используется ellipse, а не circle?
А вообще состояния можно было сделать градиентами :-). В случае с палочкой с bacgkround-size.

элементу .switch можно еще добавить свойство -webkit-user-select: none, чтоб при быстрых кликах не было выделения

Мне кажется, для полного счастья можно добавить еще user-select: none; на label. С префиксами, конечно.

GreLI, я не люблю рисовать градиентами — у них слишком плохое сглаживание, производительность и неоптимальный код, сложный для редактирования. А ellipse вместо circle по недоразумению, поправлю.

Вот бы ещё дать потаскать за кругляшок, или хотя бы не реагировать на драг (не выделять элементы), на худой конец написать: "Не тащите, это всего лишь ЦСС!".

Артём, это выходит за рамки эксперимента.

Alex, если бы я делал такой переключатель вот прямо как «компонент», с хорошей кроссбраузерностью, с JS-ом, то это имело бы смысл. А так — это просто игра с CSS.

Вадим, отлично. И даже с радиокнопками есть. Как я и писал, мне нужен был переключатель между двумя состояниями, но многим нужно именно «вкл/выкл», тем более, на айфоне он именно такую роль выполняет.

Хотел было подсмотреть у тебя решение по устранению «дергания» правой подписи в webkit-браузерах, но подписей у тебя как раз и нет =(

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

А почему Вы не положили input в label? Это избавляет нас от необходимости в id и for


<div class="switch">
    <label class="switch-label">
        <input type="checkbox" checked id="switch-1" class="switch-check" />
        Опция<span class="switch-slider"></span>
    </label>
</div>
<style>
/*...*/
.switch-check:checked + .switch-slider { /*...*/ }
.switch-check:checked + .switch-slider:after { /*...*/ }
</style>

Вообще, не понимаю, почему верстальщики так часто используют эти аттрибуты. Это же просто неудобно. Да и за уникальностью id ещё нужно следить.

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

Извиняюсь за комментарий не по теме.

Хотелось бы услышать ваши мысли и рассуждения об использовании двойных или одинарных кавычек в коде html и в стилях css. В чём минусы, и подобное.

Sadykh, для себя я выбрал систему «основной язык и вложенный». Если HTML является для меня основным, где используются только двойные кавычки (так уж повелось), то во всех остальных, вроде CSS и JavaScript, я использую только одинарные кавычки. Под описание основного языка подходят так же XML-подобные, вроде SVG.

Андрей, этот трюк универсальный, он сделан так, что его удобно применять ко всему, что угодно, с помощью класса. Здесь я решал простую задачу и мне отлично подошёл text-indent. Кстати, если ты будешь использовать свой настоящий e-mail, то мне не придётся каждый раз модерировать твои комментарии. Договорились?

Как по мне, то правильнее все же использовать не чек бокс, а радио. Опираюсь на то, что так правильнее с точки зрения бекенда. Потому что чекбокс в выключенном состоянии не шлёт вообще ничего, а радио всегда отдаёт одно из состояний. Но это так, не критично. В целом - интересно.

Федор, теперь синий тоже уезжает влево, но это потребовало дополнительного элемента. Спасибо, что спровоцировали меня на улучшение примера!

Вадим, а на основе чего html становится для вас «основным»? Правильно ли я понимаю:
html — у вас двойными кавычками, css/js — одинарными кавычками?

Но по сути, есть ли какая-нибудь специфичная разница?

p.s «подписаться на комментарии» видимо не работает.

С касательными событиями не всё так просто. Если начинать их ловить, то по-хорошему придётся поддерживать перетаскивание и прочие эффекты, что плохо, так как практически невозможно поддержать таким образом все возможности различных браузеров. Для работы на iOS можно вместо onclick добавить стиль cursor:pointer.

Sadykh, HTML основной, как я уже сказал, по историческим причинам (двойные кавычки в HTML — самая распространённая практика), и потому, что с HTML начинается жизнь любой страницы, как документа, как основы. Я забыл упомянуть, почему во вложенных одиночные. Это нужно для того, чтобы любые включения происходили безболезненно: <div style="background:url('…')" onclick="this.className='…'"> и наоборот body.innerHTML = '<span class="…">'.

Вадим, о, спасибо! Теперь я для себя буду иметь несколько причин, писать html код двойными кавычками (если вам интересно):

1. Подключения, как вы уже выше привели пример. Хотя, в данном случае, при срабатывании onclick добавиться класс с одинарными кавычками, так?
2. Пример с php (спасибо пользователю Хабра):


var div = '<div id="div"></div>' // с двойными


var div = "<div id='div'></div>" // не люблю такой HTML-код
var div = "<div id=div></div>"; // такой тоже
var div = "<div id=\"div\"></div>"; // плохая читаемость

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

P.S Нет, ну точно подписка на новые комментарии не работает B)

Sadykh, нет, в нотации this.className='new_name' кавычки необходимы, так что класс new_name добавится без кавычек. Что касается подписки, то я посмотрел — в системе есть ваш e-mail, подписанный на этот пост. Даже не знаю, что не так.

Рад за ваш выбор, по-моему он самый последовательный из всех возможных.

:)

По случаю решил чуть доработать свой старый эксперимент. Ну ладно, переписал с ноля :) Хоп — http://kizu.github.com/slider/

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

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

Да, вопрос чекбокс или 2 радио-баттана остро стоял, когда я делал реализацию для нашего проекта.

В итоге оказалось что не выделенный чекбокс игнорируется при отправке формы (если память не изменяет), и пришлось переделывать на пару радио-баттанов, для совместимости с бекэндом.

Для тех у кого проблемы с бакендом рекомендую проверять на существование соответствуещого POST параметра и, в случае его отсутствия, сохранять в БД соответствующее значение и не использовать mass assignment при обработке формы.