Pular para o conteúdo
Biblioteca BTN-05

Botão metálico

Botão em três camadas com acabamento metálico, seis variantes de cor, afundamento ao apertar e brilho que varre.

usa o BTN-05
abrir sozinho ↗
Código
BTN-05 · alias botao-metalico
Função
Botão
Fonte
21st.dev · button.tsx — MetalButton
Recebido
06/10/2026
Status
acervo
Licença
não verificada

O código

botao-metalico.css + markup.html 146 linhas · 2 arquivo(s) numa caixa só
/* ===== botao-metalico.css ========================================================== */

/* BTN-05 · Botão metálico — reescrito do original do 21st.dev sem React e sem Tailwind.

   Três camadas que fazem o metal: a MOLDURA (degradê escuro→claro, 1,25 px de espessura), o
   MIOLO (degradê claro→escuro→claro, que é o reflexo) e o BOTÃO por cima. É a sobreposição dos
   três degradês em sentidos opostos que o olho lê como superfície curva.

   **Zero JavaScript.** O original carrega três estados em React (`isPressed`, `isHovered`,
   `isTouchDevice`) e um `useEffect` para detectar toque. Em CSS isso é `:active`, `:hover` e
   `@media (hover: hover)` — o navegador já sabe, e o `@media` acerta onde a detecção por
   `ontouchstart` erra (notebook com tela sensível tem as duas coisas).

   Seis variantes de cor, cada uma com os três degradês e a sombra do texto própria. */
.c-metal {
  --c-met-fora: linear-gradient(to bottom, #000, #A0A0A0);
  --c-met-dentro: linear-gradient(to bottom, #FAFAFA, #3E3E3E, #E5E5E5);
  --c-met-botao: linear-gradient(to bottom, #B9B9B9, #969696);
  --c-met-tinta: #fff;
  --c-met-sombra-tinta: 0 -1px 0 rgb(80 80 80);
  --c-met-raio: 6px;
  --c-met-altura: 44px;
  --c-met-curva: cubic-bezier(.1, .4, .2, 1);

  position: relative;
  display: inline-flex;
  padding: 1.25px;
  border-radius: var(--c-met-raio);
  background-image: var(--c-met-fora);
  transform: translateY(0) scale(1);
  box-shadow: 0 3px 8px rgba(0, 0, 0, .08);
  transition: all .25s var(--c-met-curva);
  transform-origin: center;
}
/* o miolo: o reflexo. Fica entre a moldura e o botão. */
.c-metal::before {
  content: "";
  position: absolute;
  inset: 1px;
  border-radius: calc(var(--c-met-raio) - 1px);
  background-image: var(--c-met-dentro);
  transition: filter .25s var(--c-met-curva);
}
.c-metal__botao {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: var(--c-met-altura);
  margin: 1px;
  padding: 0 24px;
  overflow: hidden;
  border: 0;
  border-radius: var(--c-met-raio);
  background-image: var(--c-met-botao);
  color: var(--c-met-tinta);
  text-shadow: var(--c-met-sombra-tinta);
  font: 600 14px/1 inherit;
  white-space: nowrap;
  cursor: pointer;
  outline: none;
  transform: scale(1);
  transition: transform .25s var(--c-met-curva), filter .25s var(--c-met-curva);
}
.c-metal__botao::after {           /* o brilho que varre ao apertar */
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  border-radius: var(--c-met-raio);
  background-image: linear-gradient(to right, transparent, #f5f5f5, transparent);
  transition: opacity .3s ease;
}

/* hover só onde hover existe de verdade — e não onde o JS do original "detectava" toque */
@media (hover: hover) {
  .c-metal:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, .12); }
  .c-metal:hover::before { filter: brightness(1.05); }
  .c-metal:hover .c-metal__botao { filter: brightness(1.02); }
}
/* apertar: o conjunto AFUNDA 2,5 px e o botão encolhe — os dois juntos é que dão o clique físico */
.c-metal:active { transform: translateY(2.5px) scale(.99); box-shadow: 0 1px 2px rgba(0, 0, 0, .15); }
.c-metal:active::before { filter: none; }
.c-metal:active .c-metal__botao { transform: scale(.97); filter: none; }
.c-metal:active .c-metal__botao::after { opacity: .2; }
.c-metal__botao:focus-visible { outline: 2px solid #e8ad32; outline-offset: 3px; }
.c-metal:has(.c-metal__botao[disabled]) { opacity: .5; pointer-events: none; }

/* ---------- as seis variantes de cor ---------- */
.c-metal--primaria {
  --c-met-dentro: linear-gradient(to bottom, #e8ad32, #7a5510, #212121);
  --c-met-botao: linear-gradient(to bottom, #e8ad32, #b98a28);
  --c-met-tinta: #171717;
  --c-met-sombra-tinta: 0 -1px 0 rgb(232 173 50);
}
.c-metal--ganho {
  --c-met-fora: linear-gradient(to bottom, #005A43, #7CCB9B);
  --c-met-dentro: linear-gradient(to bottom, #E5F8F0, #00352F, #D1F0E6);
  --c-met-botao: linear-gradient(to bottom, #9ADBC8, #3E8F7C);
  --c-met-tinta: #FFF7F0;
  --c-met-sombra-tinta: 0 -1px 0 rgb(6 78 59);
}
.c-metal--perda {
  --c-met-fora: linear-gradient(to bottom, #5A0000, #FFAEB0);
  --c-met-dentro: linear-gradient(to bottom, #FFDEDE, #680002, #FFE9E9);
  --c-met-botao: linear-gradient(to bottom, #F08D8F, #A45253);
  --c-met-tinta: #FFF7F0;
  --c-met-sombra-tinta: 0 -1px 0 rgb(146 64 14);
}
.c-metal--ouro {
  --c-met-fora: linear-gradient(to bottom, #917100, #EAD98F);
  --c-met-dentro: linear-gradient(to bottom, #FFFDDD, #856807, #FFF1B3);
  --c-met-botao: linear-gradient(to bottom, #FFEBA1, #9B873F);
  --c-met-tinta: #FFFDE5;
  --c-met-sombra-tinta: 0 -1px 0 rgb(178 140 2);
}
.c-metal--bronze {
  --c-met-fora: linear-gradient(to bottom, #864813, #E9B486);
  --c-met-dentro: linear-gradient(to bottom, #EDC5A1, #5F2D01, #FFDEC1);
  --c-met-botao: linear-gradient(to bottom, #FFE3C9, #A36F3D);
  --c-met-tinta: #FFF7F0;
  --c-met-sombra-tinta: 0 -1px 0 rgb(124 45 18);
}

@media (prefers-reduced-motion: reduce) {
  .c-metal, .c-metal::before, .c-metal__botao, .c-metal__botao::after { transition: none; }
}


/* ===== markup.html ================================================================= */

<!-- BTN-05 · Botão metálico. Duas camadas no markup (o invólucro e o botão); a terceira, o
     reflexo, é o ::before do invólucro. Sem JavaScript: :hover, :active e @media (hover: hover)
     fazem tudo o que o original fazia com três estados de React.
     Variantes: --primaria --ganho --perda --ouro --bronze (sem classe = aço). -->
<span class="c-metal">
  <button type="button" class="c-metal__botao">Reservar</button>
</span>

<span class="c-metal c-metal--ouro">
  <button type="button" class="c-metal__botao">Assinar</button>
</span>

Já rodou em

Ainda não rodou em peça publicada.

Exige

  • react (useState ×3, useEffect)
  • cn (clsx + tailwind-merge)
  • Tailwind

Como se configura

  • --c-met-fora
  • --c-met-dentro
  • --c-met-botao
  • --c-met-tinta
  • --c-met-sombra-tinta
  • --c-met-raio
  • --c-met-altura

A ficha

O que o 21st.dev entregou

Veio num arquivo com TRÊS componentes. O mesmo .tsx traz o Button (shadcn padrão, com a variante cool), o LiquidButton e o MetalButton. O original/ de cada um dos três é esse arquivo inteiro, igual — é o que foi colado, e cortá-lo seria editar o original.

Três defeitos do arquivo como veio, nenhum corrigido em original/:

  • O export { Button, buttonVariants, liquidbuttonVariants, LiquidButton } aparece antes de liquidbuttonVariants e LiquidButton serem declarados. Funciona por hoisting de export em ESM, mas é artefato de cópia, não escolha.
  • O MetalButton não entra nesse export: tem o próprio export const. E GlassFilter, ShineEffect, colorVariants e metalButtonVariants não são exportados — quem copiar só o LiquidButton leva o GlassFilter junto sem perceber.
  • bg-linear-to-t e inset-shadow-2xs (na variante cool) são classes do Tailwind v4. Em v3 elas não existem, e o degradê do botão simplesmente não aparece.

O componente

Dependências: react (três useState e um useEffect), cn, Tailwind. Nenhuma biblioteca de animação.

Três camadas, e é a sobreposição delas que o olho lê como metal:

CamadaDegradêPapel
Moldura (outer)escuro → claro, de cima para baixoa borda de 1,25 px
Miolo (inner)claro → escuro → claroo reflexo: o escuro no meio é o que curva a superfície
Botãoclaro → escuroa face onde o texto fica

Os três correm no mesmo sentido mas com pontos de virada diferentes — é a defasagem entre eles que faz o volume.

Seis variantes de cor: default (aço), primary, success, error, gold, bronze. Cada uma traz os três degradês e a sombra do texto, que é sempre 0 -1px 0 numa cor mais escura — o texto parece gravado, não impresso.

Ao apertar: o conjunto desce 2,5 px e encolhe para 99%, o botão de dentro encolhe para 97%, a sombra fecha de 8 px para 2 px e um brilho horizontal varre a 20% de opacidade. São quatro coisas ao mesmo tempo, em 250 ms com cubic-bezier(0.1, 0.4, 0.2, 1) — é o conjunto que dá a sensação de botão físico, não uma delas.

Tamanho em repouso: 44 px de altura, 24 px de lateral, texto de 14 px peso 600.


O que a adaptação da OCTA mudou

  • Zero JavaScript. Os três estados de React (isPressed, isHovered, isTouchDevice) e o useEffect de detecção viraram :active, :hover e @media (hover: hover).
  • O @media (hover: hover) acerta onde a detecção do original erra: "ontouchstart" in window || navigator.maxTouchPoints > 0 dá verdadeiro num notebook com tela sensível, e ali o hover existe — o original desligava o realce em máquina que o tem.
  • O miolo (a camada de reflexo) virou ::before do invólucro: um elemento a menos no markup.
  • A variante primary do original depende dos tokens do shadcn e não pinta fora dele; virou --primaria, no âmbar do IDV.

Lacunas do original

  • O texto cai para "Button" quando não há filho — rótulo em inglês embutido no componente.
  • MetalButton não tem variante de tamanho: a altura de 44 px é fixa no código.
  • A detecção de toque por ontouchstart dá falso positivo em notebook com tela sensível, e ali o realce de hover fica desligado sem motivo.
  • Sem focus-visible próprio: o outline-none do botão tira o anel padrão e nada o repõe. A adaptação repõe.
  • O disabled não é tratado: o componente repassa a prop, mas nenhuma camada reage a ela.