24.06.2009

border-radius спешит на помощь

В соавторстве с Вадимом Макишвили

Для реализации скругленных уголков мы искали максимально легкое решение — отсутствие дополнительной разметки, минимум CSS и JS. Конечно, есть очень мощные средства, например, rocon, но нам они показались избыточными и громоздкими.

Идея была в том, чтобы использовать CSS-свойство border-radius, которое поддерживают некоторые прогрессивные браузеры:

  • Firefox начиная с версии 1.0 (~24% пользователей)
  • Safari начиная с 3.0 + Chrome (~2%)
  • Konquerer (~0,05%)

Итого, для ~25% пользователей уголки можно рисовать средствами браузера — уже неплохо.

Смешно, но свойство border-radius, которое есть в CSS3, пока не поддерживается ни одним браузером. Однако, некоторые браузеры поддерживают аналогичные проприетарные свойства, например, -moz-border-radius в Firefox.

.corners {
    /* Возможно какой-нибудь браузер в будущем, будем надеяться IE */
    border-radius: 4px;
    /* WebKit (Safari/Chrome) */
    -webkit-border-radius: 4px;
    /* KHTML (Konquerer) */
    -khtml-border-radius: 4px;
    /* Возможно Opera в будущем */
    -opera-border-radius: 4px;
    /* Gecko (Firefox) */
    -moz-border-radius: 4px;
}

Для остальных браузеров, не поддерживающих border-radius, будем яваскриптом вставлять доп. элементы для реализации уголков.


// Проверяет, поддерживает ли элемент elem CSS-свойство border-radius.
var isBorderRadiusSupported = function (elem) {
    var s = elem.style;
    return typeof s.borderRadius === "string" ||
           typeof s.WebkitBorderRadius === "string" ||
           typeof s.KhtmlBorderRadius === "string" ||
           typeof s.OperaBorderRadius === "string" ||
           typeof s.MozBorderRadius === "string";
};

// Добавляет внутрь элемента elem доп. элементы реализующие уголки.
var wrapInCorners = function (elem) {
    // Реализуем уголки любым известным способом
    $(elem).prepend('<i class="lt"/><i class="rt"/><i class="lb"/><i class="rb"/>');
};

// Реализует закругленные уголки с помощью доп. элементов
// для браузеров не поддерживающих CSS-свойство border-radius.
var checkCorners = function () {
    // Ищем все элементы с классом corners
    $('.corners').each(function () {
        // Если для данного элемента браузер не поддерживает border-radius
        if (!isBorderRadiusSupported(this)) {
            // Добавляем внутрь элемента доп. элементы реализующие уголки
            wrapInCorners(this);
        }
    });
};

// DOM загрузился и готов к обходу и манипуляциям
$(function () {
    checkCorners();
});

Плюсы решения:

  • HTML льется чистый, без доп. элементов, и, соответственно, лишняя разметка не мозолит глаз ни в статической верстке, ни в XSL-шаблонах.
  • В прогрессивных браузерах (примерно у четверти пользователей) уголки рисуются средствами браузера (максимально быстро).
  • Есть задел на будущее — если IE или Opera надумают реализовать border-radius, то наше решение готово к этому радостному событию.

UPD: В комментариях указали на слабые стороны этого решения. Также откликнулись мои коллеги и помогли улучшить решение по производительности. Спасибо всем! Для этого мне и нужен блог — поделиться решением, собрать фидбек и найти более удачный вариант.

1. Не нужно проверять поддержку border-radius для каждого элемента. Сделаем эту проверку один раз в теге <head> до загрузки контента и добавим соответствующий класс у элемента <html>.

// Проверяет, поддерживает ли браузер CSS-свойство border-radius.
var isBorderRadiusSupported = function () {
    var s = document.documentElement.style;
    return typeof s.borderRadius === "string" ||
           typeof s.WebkitBorderRadius === "string" ||
           typeof s.KhtmlBorderRadius === "string" ||
           typeof s.MozBorderRadius === "string";
};

document.documentElement.className += isBorderRadiusSupported() ?
        " border-radius" : " no-border-radius";

2. Создавать из строки ноды под каждый уголок недешево, а зааппендить их в DOM еще дороже. Вставка каждого уголка это рефлоу блока, в который он вставляется. Выгоднее просто отдавать доп. разметку сразу в HTML. Задействовать доп. разметку будем в зависимости от класса, который мы выставили у элемента <html>.

.no-border-radius .corners .lt {
    // Стили для уголков
}

3. На текущий момент в Опере нет поддержки border-radius. Не стоит гонять балластный код ради неизвестного будущего. Поэтому отрываем -opera-border-radius: 4px; и соответствующую проверку в JS.

UPD2: Opera 10.50 pre-alpha поддерживает border-radius, причем без префикса. Ура!

20 комментариев:

Сергей Чикуёнок комментирует...

Главный недостаток этого способа — относительно высокая нагрузка на браузер при загрузке страницы (не говоря о том, что под каждый уголок нужно генерить спрайты). Особенно на больших проектах. Ну и неприятное появление этих уголков.

Я, когда начинал проектировать rocon, хотел сделать один трюк, который, к сожалению, работает только в IE. Суть его заключается в том, что если IE встречает незнакомое еиу CSS-правило в таблице стилей, то он все равно хранит его в описании (остальные браузеры просто игнорируют его). Соответственно, алгоритм такой: сразу после загрузки всех CSS запускается скрипт, который сканирует CSS-правила, и если находит в них свойство borderRadius, то вешает на них одноразовый expression, который моментально отрабатывается при появлении элемента, подходящего под правило. Но, как я уже сказал, работает это только в IE.

Степан Резников комментирует...

>> не говоря о том, что под каждый уголок нужно генерить спрайты

Ну не обязательно ведь использовать картинки для уголков. Мы вот сделали для ИЕ и Оперы однопиксельные уголки без картинок. Я просто не стал в этой заметке рассказывать об этом, чтобы не перегружать.


>> Ну и неприятное появление этих уголков.

А мне, кстати, очень даже нравится :)


>> вешает на них одноразовый expression

Трабл еще и в том, что в IE8 нет экспрешеннов.

Vii комментирует...

А для чего проверять поддержку border-radius для каждого элемента (он же по-идее или есть, или нет)? Или хотя бы префикс (Moz, Webkit и др.) можно вычислить один раз. (http://thinkweb2.com/projects/prototype/feature-testing-css-properties/)

Есть еще вот такой вариант: http://community.livejournal.com/ru_webdev/2718122.html (правда он вроде не совсем универсальный) там JavaScript кажется только для IE.

DenisX комментирует...

хороши комменты те, которые хорошо читать. люблю ваши блоги, ребята

Vii комментирует...

>> Трабл еще и в том, что в IE8 нет экспрешеннов.

Зато есть -ms-behavior (http://blogs.msdn.com/ie/archive/2008/09/08/microsoft-css-vendor-extensions.aspx) В принципе, думаю, можно с ним покудесничать :))

nop комментирует...

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

Степан Резников комментирует...

2nop: Суть заметки была в использовании бордер-радиуса (самого быстрого и правильного способа). А дальше, для браузеров не поддерживающих бордер-радиус, каждый волен использовать любой способ, который ему нравится - хочешь вставляй дом-элементы, хочешь заворачивай в обертки, хочешь класс навешивай и от него пляши. Я просто проиллюстрировал (может, не очень удачно) вставкой дом-элементов, но нисколько за это не агитирую.

Вегед! комментирует...

а кстати, то что border-radius самый быстрый способ подтверждено тестами? можно представить что из-за произвольного цвета и полупрозрачности оно проиграет какойнибудь реализации частного случая с конкретными цветами (например)

alpha комментирует...

Плюсы решения: HTML льется чистый, без доп. элементов, и, соответственно, лишняя разметка не мозолит глаз ни в статической верстке, ни в XSL-шаблонах.

А почему тебе это мозолит глаза в xsl-шаблонах? Там это должно быть написано ровно один раз для любых блоков.

Степан Резников комментирует...

2Вегед: В целом, да, ты прав, надо провести тесты и померить, но интуитивно кажется, что border-radius должен быть самый быстрый способ.

2alpha: Ну а вызовы шаблона рисующего эти уголки?

alpha комментирует...

Ну а вызовы шаблона рисующего эти уголки?

Вызов шаблона мозолит глаза? По-моему, мозолить глаза в даннном случае может только копипаст.

Сергей Чикуёнок комментирует...

Раз уж пошла такая пьянка, то хочу заметить, что результат работы border-radius принципиально отличается от того, как работает эмуляция (добавление элементов-уголков). Посмотрите на пример: в первом случае используется нативное свойство border-radius, во втором — картинка. Как видно, border-radius — это всего лишь фоновая декорация блока, а не маска, как многие могут подумать. Именно по этой причине в следующей версии rocon появится параметр force, который принудительно будет рисовать картинки независимо от того, поддерживает браузер border-radius или нет.

Pavel Volodko комментирует...

Насчет "минимум JS" - вы забыли про jQuery :)

Степан Резников комментирует...

jQuery у нас так и так есть в проекте :)

do комментирует...

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

Я тут ещё подумал... В свое время боролись за ортогональность содержания и оформления. А теперь ответвляется ещё 3-я ось координат - graphic engine для отрисовки ентого оформления :)

DenisX комментирует...

Стёпа, а почем тебе обошлась реклама? :)

B@rmaley.exe комментирует...

> -opera-border-radius: 4px
Оперы используют префикс -o-. Т.е. свойство скорей всего будет
-o-border-radius: 4px

Unknown комментирует...

Наверное, стоит border-radius поместить после хаков.

Swart комментирует...

-o-border-radius не работает,
за то Opera 10.61 поддерживает border-radius

так что писать теперь можно как-т так
/* Возможно какой-нибудь браузер в будущем, будем надеяться IE */
border-radius: 0 7px 0 7px;
/* WebKit (Safari/Chrome) */
-webkit-border-radius: 0 7px 0 7px;
/* KHTML (Konquerer) */
-khtml-border-radius: 0 7px 0 7px;
/* Возможно Opera в будущем */
-0-border-radius: 0 7px 0 7px;
/* Gecko (Firefox) */
-moz-border-radius: 0 7px 0 7px;

Unknown комментирует...

-webkit-border-radius: 0px;
-moz-border-radius: 0px;
-o-border-radius: 0px;
-ms-border-radius: 0px;
border-radius: 0px;
-khtml-border-radius: 0px;