CSS

16 dez, 2014

A jornada das estrelas de avaliação de produtos com CSS

Publicidade

Vamos fazer não simples estrelinhas de avaliação com CSS, mas uma que, ao clicar nela, permaneça a avaliação dada, será acessível via teclado e flexível para usar em qualquer lugar. O resultado final será este:

css-1

Veja no Codepen o resultado, ou em full sem a parafernália do Codepen.

Comecei a pensar em várias formas de fazer isso com CSS – sei que milhares de pessoas já a fizeram antes e, se você quer algo realmente simples, dá pra fazer com 10 linhas de CSS, mas quis fazer algo mais elaborado. Acabei passando por flexbox, display inline, inline-block, floats, inline-flext, pointer-events, em, px, %, enfim, testes e mais testes – talvez essas sejam as estrelas de avaliação mais pensadas e testadas que já existiram na vida útil do meu computador até os dias atuais.

O resultado você vê neste artigo – como desenvolver essas estrelas de avaliação de produtos que são muito usadas em e-commerces e para avaliação de quase todo tipo de conteúdo.

Isso será uma jornada das 5 estrelas, então senta relaxa e não tente ler este texto apenas vendo os códigos e pulando partes, porque provavelmente não vai dar certo. Se você achar que elas podem melhorar, me diga nos comentários, quero muito deixá-las ainda melhores.

O HTML

Pra começar, o HTML. Usei input tipo radio para fazer com que, ao clicar nas estrelas, elas continuem ativas. Os inputs radio serão ocultados e o que ficará visível serão os labels:

<div class="<a style="background: none repeat scroll 0% 0% transparent ! important; border: medium none ! important; display: inline-block ! important; text-indent: 0px ! important; float: none ! important; font-weight: bold ! important; height: auto ! important; margin: 0px ! important; min-height: 0px ! important; min-width: 0px ! important; padding: 0px ! important; text-transform: uppercase ! important; text-decoration: underline ! important; vertical-align: baseline ! important; width: auto ! important;" title="Click to Continue > by ClickCaption" in_rurl="http://s.srv-itx.com/click?v=QlI6NzQ3OTU6NDpzdGFyczo0NmI1ZDM1OTk3MWI1NmM0ODNiNmQ0MDE5MzRjOTg5Yjp6LTE1MTEtMTEzMDEzNDE6dHV0c21haXMuY29tLmJyOjIyMTM2OTpiNTdlYmU0MDc3ZTg3MWRjZWM0MjJlMWIwNWVkYzlkNjphNTZiNTljOGI5MGY0MDZlOWZlNzFlOWJkOGE2YzI5NDoxOmRhdGFfc3MsNzI4eDEzNjY7ZGF0YV9mYixubzs6NDk0NDIxMw&subid=g-11301341-7f4f63d1729c46daae1d211bc4f3fe24-&data_ss=728x1366&data_fb=no&data_tagname=PRE" id="_GPLITA_5" href="#">star<img style="background: none repeat scroll 0% 0% transparent ! important; border: medium none ! important; display: inline-block ! important; text-indent: 0px ! important; float: none ! important; font-weight: bold ! important; height: 10px ! important; margin: 0px 0px 0px 3px ! important; min-height: 0px ! important; min-width: 0px ! important; padding: 0px ! important; text-transform: uppercase ! important; text-decoration: underline ! important; vertical-align: super ! important; width: 10px ! important;" src="http://cdncache-a.akamaihd.net/items/it/img/arrow-10x10.png"></a>">
  <input name="rating" id="star-1" value="1" type="radio" class="radio radio-1"/>
  <input name="rating" id="star-2" value="2" type="radio" class="radio radio-2"/>
  <input name="rating" id="star-3" value="3" type="radio" class="radio radio-3"/>
  <input name="rating" id="star-4" value="4" type="radio" class="radio radio-4"/>
  <input name="rating" id="star-5" value="5" type="radio" class="radio radio-5"/>
  <label for="star-1" class="star-item star-item-1">1 <a style="background: none repeat scroll 0% 0% transparent ! important; border: medium none ! important; display: inline-block ! important; text-indent: 0px ! important; float: none ! important; font-weight: bold ! important; height: auto ! important; margin: 0px ! important; min-height: 0px ! important; min-width: 0px ! important; padding: 0px ! important; text-transform: uppercase ! important; text-decoration: underline ! important; vertical-align: baseline ! important; width: auto ! important;" title="Click to Continue > by ClickCaption" in_rurl="http://s.srv-itx.com/click?v=QlI6NzQ3OTU6NDpzdGFyczozNmRlMmEyYjg2MjgyYzMyNDRlY2YwOWRjNDNiNzIxMTp6LTE1MTEtMTEzMDEzNDE6dHV0c21haXMuY29tLmJyOjIyODAwNDo3ZjMyMTA4MGI1ZWJmMTA2YTk0Yzc0YjNiYzVjYzg0ZDo5OGVhNjZhYWQzYTY0MWNjYTUwMDk1MTI2ZTMwNjg4MToxOmRhdGFfc3MsNzI4eDEzNjY7ZGF0YV9mYixubzs6NDk0NDIxMw&subid=g-11301341-7f4f63d1729c46daae1d211bc4f3fe24-&data_ss=728x1366&data_fb=no&data_tagname=PRE" id="_GPLITA_3" href="#">stars<img style="background: none repeat scroll 0% 0% transparent ! important; border: medium none ! important; display: inline-block ! important; text-indent: 0px ! important; float: none ! important; font-weight: bold ! important; height: 10px ! important; margin: 0px 0px 0px 3px ! important; min-height: 0px ! important; min-width: 0px ! important; padding: 0px ! important; text-transform: uppercase ! important; text-decoration: underline ! important; vertical-align: super ! important; width: 10px ! important;" src="http://cdncache-a.akamaihd.net/items/it/img/arrow-10x10.png"></a></label>
  <label for="star-2" class="star-item star-item-2">2 <a style="background: none repeat scroll 0% 0% transparent ! important; border: medium none ! important; display: inline-block ! important; text-indent: 0px ! important; float: none ! important; font-weight: bold ! important; height: auto ! important; margin: 0px ! important; min-height: 0px ! important; min-width: 0px ! important; padding: 0px ! important; text-transform: uppercase ! important; text-decoration: underline ! important; vertical-align: baseline ! important; width: auto ! important;" title="Click to Continue > by ClickCaption" in_rurl="http://s.srv-itx.com/click?v=QlI6NzQ3OTU6NDpzdGFycmluZzoxMmRjZTc0ZWQyYjFiYWZlNDAyMGUxMDRiNTYxMzlhOTp6LTE1MTEtMTEzMDEzNDE6dHV0c21haXMuY29tLmJyOjIyMTM2OTpiNTdlYmU0MDc3ZTg3MWRjZWM0MjJlMWIwNWVkYzlkNjoxMzNmODMxMGYxNjA0ZTc2YTViY2QwNjQ1MzYzNzk5YzoxOmRhdGFfc3MsNzI4eDEzNjY7ZGF0YV9mYixubzs6NTgyODUzMQ&subid=g-11301341-7f4f63d1729c46daae1d211bc4f3fe24-&data_ss=728x1366&data_fb=no&data_tagname=PRE" id="_GPLITA_4" href="#">stars<img style="background: none repeat scroll 0% 0% transparent ! important; border: medium none ! important; display: inline-block ! important; text-indent: 0px ! important; float: none ! important; font-weight: bold ! important; height: 10px ! important; margin: 0px 0px 0px 3px ! important; min-height: 0px ! important; min-width: 0px ! important; padding: 0px ! important; text-transform: uppercase ! important; text-decoration: underline ! important; vertical-align: super ! important; width: 10px ! important;" src="http://cdncache-a.akamaihd.net/items/it/img/arrow-10x10.png"></a></label>
  <label for="star-3" class="star-item star-item-3">3 stars</label>
  <label for="star-4" class="star-item star-item-4">4 stars</label>
  <label for="star-5" class="star-item star-item-5">5 stars</label>
</div>

Como o HTML ficou repetitivo, optei por usar Jade, que é muito simples:

- stars = [1, 2, 3, 4, 5];

div.star
- each star in stars
input(class='radio radio-#{star}' name='rating' id='star-#{star}' value='#{star}' type='radio')

- each star in stars
label(for='star-#{star}' class='star-item star-item-#{star}') #{star} stars

Você pode ver que temos uma lista de números, que é as estrelas, depois fazemos dois for…each para montar o HTML que precisamos. Se você não entende nada de Jade, aprenda aqui.

Para cada estrela, temos um label e um input radio. Se você já entende de CSS, sabe que usaremos input:checked para manter as estrelas depois de clicar – simples, né? Os inputs radio serão ocultados de forma que continuem sendo focáveis via teclado, o que é muito importante.

O CSS

O CSS é um pouco grande e complicado, por isso coloquei vários comentários nele para explicar o que está acontecendo – leia todos os comentários! Usei Sass, então se você não conhece Sass, bom, você pode ver o resultado do Sass compilado.

/* Estilo das estrelas quando elas estão "ativadas" checked */
%checked-star {
  color: #FFE000;
}
 
/* Estilo das estrelas quando elas estão "desativadas" unchecked */
%unchecked-star {
  color: #CCC;
}
 
/*
  Esconde o texto de elementos
  source https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757
*/
%hide-text {
  font: "0/0" a;
  color: transparent;
  text-shadow: none;
  background-color: transparent;
  border: 0;
}
 
/*
  Esconde um elemento de forma que continue acessível via teclado
 
http://getbootstrap.com/css/#helper-classes-screen-readers
 
*/
%hide-element {
  position: absolute;
  top: -999999em;
  left: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
 
/*
  Escreve os seletores das estrelas para marcalas como "ativas" checked, quando um input radio 3 está "ativo" checked, as estrelas 3, 2 e 1 devem aparecer "ativas", esse loop escreve essas classes para nós.
*/
@for $group from 1 through 5 {
  @for $item from 1 through $group {
    .radio-#{$group}:checked ~ .star-item-#{$item}:before,
    /* Focus, importante para dar o estilho mesmo quando for ativado via teclado */
    .radio-#{$group}:focus ~ .star-item-#{$item}:before {
      @extend %checked-star;
    }
  }
}
 
.star {
  display: inline-block;
  flex-wrap: wrap;
  pointer-events: none;
  /* max-width: 5em; */
}
 
.star-item {
  cursor: pointer;
  display: inline-block;
  pointer-events: initial;
  width: 1em;
  height: 1em;
  overflow: hidden;
  line-height: 100%;
  @extend %hide-text;
 
  /* Estilo padrão das estrelas */
  &:before {
    transition: color 200ms;
    transform: translate3d(0, 0, 0);
    will-change: color;
    content: '\2605';
    @extend %unchecked-star;
  }
 
  /* Ao passar o mouse em uma estrela, essa estrela fica amarela */
  &:hover:before {
    @extend %checked-star;
  }
 
  /* Ao passar o mouse em uma estrella, as estrelas adjacentes ficam como unchecked (Isso é um truque para não precisar mudar a ordem de disposição dos elementos para rtl) */
  &:hover ~ .star-item:before {
    @extend %unchecked-star;
  }
}
 
/* Ao passar o mouse no container das estrelas, todos os itens ficam como marcados */
.star:hover .star-item:before {
  @extend %checked-star;
}
 
/* Esconde os input radio de forma que continuem acessíveis via teclado */
.radio {
  @extend %hide-element;
}
 
/* Ordena os itens da direita para a esquerda */
.star:dir(rtl), .star.rtl {
  .star-item { direction: rtl; }
}

Qualquer dúvida que você tiver sobre esse CSS, me pergunte nos comentários.

Você pode ver uma demonstração do que foi criado no Codepen.

Vou fazer alguns comentários sobre o que eu testei nessas estrelas, então vamos lá!

Direção das estrelas – ltr ou rtl?

Provavelmente você já viu a mesma coisa feita pelo Chris Coyier há anos – a diferença para o que eu fiz é que não muda a direção dos itens de ltr para rtl. Isso porque primeiro nós colocamos todos os itens como checked (linha 89 do CSS), depois usamos o seletor adjacente para marcar as estrelas seguintes como unchecked (linha 84 do CSS), por isso não precisamos alterar a ordem os elementos. Criativa essa solução, né? Créditos pra Vanessa que me ajudou.

Nós lemos da esquerda para a direita (ltr), mas em alguns países a galera lê da direita para a esquerda (rtl). Então, o que acontece se você usar as estrelas em um site rtl? Bom, pelo que você pode conferir neste stackoverflow, as estrelas também devem ser selecionadas da direita para a esquerda.

Para isso, usei o :dir e criei uma classe – poderíamos também usar o seletor [dir=”rtl”] mas não o adicionei. Também poderíamos ter uma classe .rtl, mas achei que o :dir foi o melhor, porque basta o componente estar em um contexto right to left que as estrelas já vão se comportar conforme o contexto.

No GIF abaixo, você vê o resultado – o primeiro conjunto de estrelas é ltr, o segundo está com rtl:

estrelas-rtl

Flexbox versus inline-block

Nós usamos flexbox para deixar uma estrela ao lado da outra, poderíamos usar inline-block, mas com inline-block teríamos que deixar o HTML todo sem espaços ou adotar outro tipo de solução que acabaria deixando o componente flexível, como mostra este artigo. Com flexbox, conseguimos deixar o espaço bem certinho.

Também encontrei um bug muito peculiar usando inline-block, em que nos elementos tendo o mesmo height era possível dar hover em um elemento antes do outro, o que fazia o componente se comportar de forma inesperada, como no GIF abaixo:

inline-block

O mesmo acontece no Firefox, no Safari e no Chrome, então não usei inline-block.

Flexbox versus float left

Bom, como um bom desenvolvedor eu tenho que testar, e o resultado foi muito melhor do que eu imaginava. Com float: left, não precisamos nos preocupar com os espaços do inline-block. Muita gente condena o float, mas neste exemplo com flexbox e float funcionou do mesmo jeito.

O maior problema de usar float, nesse caso, é que o direction não muda nada em elementos que estão com floats, então usar o float faz com que nosso componente não seja usado em um site com leitura rtl.

Claro, dá pra apenas usar o seletor :dir e colocar um float right, mas achei melhor usar inline-flex mesmo.

inline-flex versus inline-block

O inline-flex no elemento .star me deixou muito satisfeito, porque o line-height ficou perfeitamente combinado com o texto, como neste print:

print-1

O inline-block ficou bom também, mas não tanto quanto, como neste print:

print-2

O que eu não gostei muito é que, por padrão, por causa do flexbox, o elemento tem uma só linha e seu conteúdo não tem quebra de linha – a mesma quebra de linha que mostro na sessão sobre pointer-events.

Solucionei esse problema usando flex-wrap: wrap; assim, quando for preciso, o conteúdo terá quebra de linha e continuará visível, não ficando fora da tela.

Escalar os elementos

Deixei todo o CSS com em – isso significa que para mudar o tamanho desse componente basta selecionar o elemento .star e usar um fonte-size. O padrão é 1em; se você usar 2em, as estrelas terão o dobro de tamanho, e por aí vai.

.star {
  font-size: 2em;
}

É só por em e ser feliz:

escalavel

pointer-events

O elemento .star está la não apenas por ser bonito, mas porque precisamos de algo que envolva todos os elementos. Mas para deixar esse componente ainda mais completo (já percebeu que a diferença de completo e complexo é apenas uma letra?), fiz a seguinte pergunta pra mim: “E se eu quiser que minhas estrelas tenha uma quebra de linha?”.

Bem, se rolar uma quebra de linha, o componente não funcionaria como esperado. Isso porque o hover no elemento .star ativa todos as estrelas, mas se o usuário apenas passar o mouse em .star, todas as estrelas ficarão como ativas, como no GIF abaixo:

sem-pointer-events

Foi aí que eu cheguei nos pointer-events. Basicamente, nós desativamos os pointer-events no .star, e no .star-item setamos para initial, o que faz com que o elemento .star, em outras palavras, seja ignorado pelo cursor. Ficando assim:

com-pointer-events

Com pointer-events fica legal ,né? Abre mais possibilidades. Se é alguma prática ruim, eu não sei, mas se você quiser saber mais sobre isso, clica aqui.

Usar vários componentes de estrela em uma mesma página

Bom, como foi tudo feito com CSS, o CSS não é muito dinâmico e os valores não são alterados durante a execução.

Se você for usar vários componentes em uma mesma página, levando em consideração que você precisa do comportamento dos input radio, você tem que especificar um ID e um FOR diferente para os elementos. E também os inputs radio precisam de um name diferente.

Apenas deixar o componente com estrelas estáticas

Não fiz isso ainda, mas notei que todos os e-commerce deixam essas estrelas estáticas na maior parte do tempo, sem interação alguma. Só se você estiver logado no site, e clicar pra avaliar um produto etc., é que dá para interagir com as estrelas.

A solução para deixar as estrelas estáticas irá envolver muito menos HTML e CSS do que os exemplos acima, mas pensarei nisso nos próximos dias.

Acessibilidade

Eu me considero péssimo em testar sites com leitores de tela, é talvez o que eu tenho mais dúvida neste universo (claro, logo depois da dúvida sobre a final de Hot Wheels), mas, usando o VoiceOver do OSX, consegui navegar no elemento e marcar uma quantidade de estrelas usando o teclado.

O grande segredo é que usei uma técnica que está disponível no HTML5 Boilerplate e também é usada no Bootstrap (source) – ocultar os elementos de forma que eles continuem acessíveis via teclado; se você usar um display: none, jamais alguém consegue focar o elemento via teclado.

Se você quiser testar, basta entrar na página, dar um tab e, quando a primeira estrela ficar ativa, use as setas para esquerda e direita para mudar a quantidade de estrelas.

No VoiceOver, ele fala “1 star”, “2 star”, “3 star”, e eu achei bem bacana.

Se alguém testar com leitor de tela, me dê feedback e também me diga qual leitor usou e em qual navegador.

Mobile

Como cada label faz um link para um input radio, quando o usuário dá um tab em uma das estrelas, ela já fica marcada, então funciona. Uma barreira talvez seria o tamanho das estrelas, então se você for usar essas estrelinhas em seu projeto, lembre-se de colocar um tamanho maior para as estrelas em telas pequenas, como eu já mostrei neste artigo.

Conclusão

Há tantas maneiras de fazer essas simples estrelinhas de avaliação, que cheguei à conclusão de que só existe uma melhor solução: avaliar o que você precisa e usar. Para muitos, a opção com floats será melhor porque funciona melhor no IE7; para outros, a solução usando pointer-events será totalmente desnecessária; outros vão amar inline-flex, mas vão preferir um inline-block.

Bom, não tem uma melhor solução, muito menos uma única. O que eu deixei no demo foi o que eu achei que é o melhor para todas as ocasiões. Eu não levei em consideração browsers antigos, em alguns pontos não considerei nem a forma diferente de renderizar a fonte da estrela que o Firefox usa – no Chrome e no Safari ficou ok, mas não cheguei a testar em um IE.

Provavelmente eu atualizarei este artigo com testes no IE, até porque, como desenvolvedor, devo fazer meu código funcionar em qualquer meio de acesso.

Alguns links interessantes que me ajudaram: Flexbox Guide, will-change property (support).

E aí? O que você achou dessas estrelinhas malditas que passei três noites estudando?