Модный переключатель
Большой молодец Рома Комаров прочитал на 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
:
- Первая двухпиксельная тень без размытия: третий параметр равен нулю и четвёртый только добавляет смещения чёрной тени с прозрачностью 10%;
- Вторая простая четырёхпиксельная тень, дающая основное углубление на половину чёрным;
- Третья четырёхписельная тень нависающая сверху на 5 пикселей на треть чёрным;
- Четвёртая тень без размытия, со смещением сверху на половину высоты переключателя всего на 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. С префиксами, конечно.
а почему бы не выкладывать вот такие крутые штуки на github? =) удобно было бы "клонировать" и дополнять
GreLI, я не люблю рисовать градиентами — у них слишком плохое сглаживание, производительность и неоптимальный код, сложный для редактирования. А
ellipse
вместоcircle
по недоразумению, поправлю.В переключателе на iOS движется весь фон, а не просто наплывает серая плашка справа.
Здесь хорошо показано:
http://papermashup.com/demos/ajax-switch/
Вот бы ещё дать потаскать за кругляшок, или хотя бы не реагировать на драг (не выделять элементы), на худой конец написать: "Не тащите, это всего лишь ЦСС!".
Федор, да, синяя плашка не уезжает, но это можно сделать с ещё одним дополнительным элементом. Есть куда развиваться.
Артём, это выходит за рамки эксперимента.
Alex, если бы я делал такой переключатель вот прямо как «компонент», с хорошей кроссбраузерностью, с JS-ом, то это имело бы смысл. А так — это просто игра с CSS.
Вадим, отлично. И даже с радиокнопками есть. Как я и писал, мне нужен был переключатель между двумя состояниями, но многим нужно именно «вкл/выкл», тем более, на айфоне он именно такую роль выполняет.
Хотел было подсмотреть у тебя решение по устранению «дергания» правой подписи в webkit-браузерах, но подписей у тебя как раз и нет =(
Александр, наверное будет вторая версия переключателя, мне стало интересно сделать его безупречным. И тогда предусмотрю подпись, но в той же iOS подпись одна — т.к. это, по сути, чекбокс и его состояние.
У меня в мобильном Safari, кстати, не пашет :(
Дмитрий, кстати да, но вот в Opera Mobile работает без проблем.
А почему Вы не положили
input
вlabel
? Это избавляет нас от необходимости в id и forВообще, не понимаю, почему верстальщики так часто используют эти аттрибуты. Это же просто неудобно. Да и за уникальностью id ещё нужно следить.
https://dl.dropbox.com/u/640912/radio.png
Что я делаю не так?
А, ну да, это Opera Mini. Прошу прощения, затупил.
Егор, поле формы завёрнутое в лейбл — это частный случай, не всегда применимый. Иначе между полем и лейблом будут пустые места, чувствительные к нажатию. Поэтому я предпочитаю организовывать связь полей явно, хотя в некоторых случаях — да, можно вложить.
отличный урок! Спасибо большое
Извиняюсь за комментарий не по теме.
Хотелось бы услышать ваши мысли и рассуждения об использовании двойных или одинарных кавычек в коде html и в стилях css. В чём минусы, и подобное.
А почему ты не использовал этот трюк?
Sadykh, для себя я выбрал систему «основной язык и вложенный». Если HTML является для меня основным, где используются только двойные кавычки (так уж повелось), то во всех остальных, вроде CSS и JavaScript, я использую только одинарные кавычки. Под описание основного языка подходят так же XML-подобные, вроде SVG.
Андрей, этот трюк универсальный, он сделан так, что его удобно применять ко всему, что угодно, с помощью класса. Здесь я решал простую задачу и мне отлично подошёл
text-indent
. Кстати, если ты будешь использовать свой настоящий e-mail, то мне не придётся каждый раз модерировать твои комментарии. Договорились?Как по мне, то правильнее все же использовать не чек бокс, а радио. Опираюсь на то, что так правильнее с точки зрения бекенда. Потому что чекбокс в выключенном состоянии не шлёт вообще ничего, а радио всегда отдаёт одно из состояний. Но это так, не критично. В целом - интересно.
Федор, теперь синий тоже уезжает влево, но это потребовало дополнительного элемента. Спасибо, что спровоцировали меня на улучшение примера!
Чтобы переключатель работал в мобильном сафари нужен хак с JS:
Пример. А все потому что сафари не отрабатывает тап на
label
.Александр, для Mobile Safari наверное имеет смысл сразу закладываться в
touch
, чтобы не смущать десктопные браузеры.Вадим, ну, по-правильному, да. Просто их в jQuery нет и нужно писать самому =)
Вадим, а на основе чего 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 (спасибо пользователю Хабра):
Поэтому, приносим в жертву двойные кавычки, чтобы в остальных языках программирования/разметке использовать одинарные кавычки.
P.S Нет, ну точно подписка на новые комментарии не работает B)
Sadykh, нет, в нотации
this.className='new_name'
кавычки необходимы, так что классnew_name
добавится без кавычек. Что касается подписки, то я посмотрел — в системе есть ваш e-mail, подписанный на этот пост. Даже не знаю, что не так.Рад за ваш выбор, по-моему он самый последовательный из всех возможных.
:)
По случаю решил чуть доработать свой старый эксперимент. Ну ладно, переписал с ноля :) Хоп — http://kizu.github.com/slider/
Основная проблема — нельзя делать полностью скруглённые углы — для слайда фонов используется метод с фоном, который при анимации не идеально отображается. Возможно, в какой-нибудь из следующих итераций поправлю это.
А так — основной идеей было сделать автоматическую ширину по содержимому, во главу этого угла было поставлено всё остальное. Ну и код ещё сильно сырой — половину уже хочу переписать ещё раз )
Да, вопрос чекбокс или 2 радио-баттана остро стоял, когда я делал реализацию для нашего проекта.
В итоге оказалось что не выделенный чекбокс игнорируется при отправке формы (если память не изменяет), и пришлось переделывать на пару радио-баттанов, для совместимости с бекэндом.
Для тех у кого проблемы с бакендом рекомендую проверять на существование соответствуещого POST параметра и, в случае его отсутствия, сохранять в БД соответствующее значение и не использовать mass assignment при обработке формы.