Botão de vidro líquido
Botão transparente que DISTORCE o que está atrás dele, com ruído SVG empurrando os pixels do fundo.
usa o BTN-04
O código
/* ===== botao-vidro-liquido.css ===================================================== */
/* BTN-04 · Botão de vidro líquido — reescrito do original do 21st.dev sem React e sem Tailwind.
O truque não é transparência: é DISTORÇÃO. Um filtro SVG (`feTurbulence` + `feDisplacementMap`)
empurra os pixels do que está atrás do botão, como vidro grosso irregular. O `backdrop-filter`
aponta para esse filtro por id.
⚠️ **O `isolation: isolate` no BOTÃO não é enfeite — sem ele o efeito não existe.** Medido em
06/10/2026: uma camada com `z-index: -1` dentro de um pai que não isola tem o backdrop VAZIO, e
o `backdrop-filter` não desenha nada. É assim que o original vem (`-z-10` dentro de um
`relative`), e por isso o vidro dele **não renderiza em navegador nenhum**. Pondo o
`isolation` no pai, o botão vira o backdrop root e a camada passa a enxergar o que está atrás.
Medições: `z-index:-1` sozinho → nada · `z-index:-1` + `isolation` na própria camada → nada ·
`isolation` no pai → renderiza · `z-index:0` → renderiza.
A pilha de sombras é o que desenha a "borda de vidro": duas sombras internas opostas fazem o
bisel, duas mais fechadas fazem o brilho da aresta, e duas largas fazem o corpo do vidro. */
.c-vidro {
--c-liq-tinta: #ebebeb;
--c-liq-raio: 6px;
--c-liq-altura: 56px;
--c-liq-padding: 40px;
position: relative;
/* OBRIGATÓRIO: faz do botão o backdrop root. Sem isto, a lente de z-index -1 tem o backdrop
vazio e o filtro não desenha. Foi o defeito do original. */
isolation: isolate;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: var(--c-liq-altura);
padding: 0 var(--c-liq-padding);
border: 0;
border-radius: var(--c-liq-raio);
background: transparent;
color: var(--c-liq-tinta);
font: 500 14px/1 inherit;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
transition: transform .3s ease;
}
.c-vidro:hover { transform: scale(1.05); }
.c-vidro[disabled] { opacity: .5; pointer-events: none; }
.c-vidro:focus-visible { outline: 3px solid rgba(232, 173, 50, .5); outline-offset: 2px; }
/* a camada que distorce o fundo. Fica ATRÁS do rótulo (z-index -1 com isolation no pai). */
.c-vidro__lente {
position: absolute;
z-index: -1;
inset: 0;
overflow: hidden;
border-radius: var(--c-liq-raio);
/* o isolation NÃO vai aqui: na própria camada ele cria um backdrop root vazio e mata o efeito.
Vai no pai (.c-vidro). Medido. */
-webkit-backdrop-filter: url("#c-vidro-filtro");
backdrop-filter: url("#c-vidro-filtro");
}
/* a pilha de sombras que desenha a borda do vidro. O original usava rounded-full aqui e
rounded-md no botão — era incoerência dele; aqui as duas seguem --c-liq-raio. */
.c-vidro__borda {
position: absolute;
z-index: 0;
inset: 0;
pointer-events: none;
border-radius: var(--c-liq-raio);
box-shadow:
0 0 8px rgba(0, 0, 0, .03),
0 2px 6px rgba(0, 0, 0, .08),
inset 3px 3px 0.5px -3.5px rgba(255, 255, 255, .09),
inset -3px -3px 0.5px -3.5px rgba(255, 255, 255, .85),
inset 1px 1px 1px -0.5px rgba(255, 255, 255, .6),
inset -1px -1px 1px -0.5px rgba(255, 255, 255, .6),
inset 0 0 6px 6px rgba(255, 255, 255, .12),
inset 0 0 2px 2px rgba(255, 255, 255, .06),
0 0 12px rgba(0, 0, 0, .15);
transition: box-shadow .3s ease;
}
.c-vidro__rotulo { position: relative; z-index: 1; pointer-events: none; }
.c-vidro svg { width: 16px; height: 16px; flex: none; }
/* tamanhos do original */
.c-vidro--p { --c-liq-altura: 32px; --c-liq-padding: 16px; font-size: 12px; }
.c-vidro--m { --c-liq-altura: 36px; --c-liq-padding: 16px; }
.c-vidro--g { --c-liq-altura: 40px; --c-liq-padding: 24px; }
.c-vidro--gg { --c-liq-altura: 48px; --c-liq-padding: 32px; }
/* o filtro é caro: com "reduzir movimento" o botão para de crescer, mas a lente fica —
ela não se move, é estática */
@media (prefers-reduced-motion: reduce) {
.c-vidro, .c-vidro__borda { transition: none; }
.c-vidro:hover { transform: none; }
}
/* ===== markup.html ================================================================= */
<!-- BTN-04 · Botão de vidro líquido.
O <svg> do filtro vive UMA VEZ por página, em qualquer lugar do body, e todos os botões o
referenciam pelo id. Sem ele, o backdrop-filter não resolve e o botão fica só transparente.
Precisa de algo ATRÁS para distorcer — sobre fundo liso o efeito não aparece. -->
<button type="button" class="c-vidro">
<span class="c-vidro__lente" aria-hidden="true"></span>
<span class="c-vidro__borda" aria-hidden="true"></span>
<span class="c-vidro__rotulo">Ver valores</span>
</button>
<!-- uma vez por página -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs>
<filter id="c-vidro-filtro" x="0%" y="0%" width="100%" height="100%" color-interpolation-filters="sRGB">
<!-- ruído fractal: é o mapa que diz para onde empurrar cada pixel -->
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.05" numOctaves="1" seed="1" result="ruido"/>
<!-- borrar o ruído tira o granulado e deixa a distorção ondulada, de vidro -->
<feGaussianBlur in="ruido" stdDeviation="2" result="ruidoBorrado"/>
<!-- scale 70 é a força do empurrão: abaixo de ~30 quase não se vê, acima de ~120 vira sopa -->
<feDisplacementMap in="SourceGraphic" in2="ruidoBorrado" scale="70" xChannelSelector="R" yChannelSelector="B" result="deslocado"/>
<feGaussianBlur in="deslocado" stdDeviation="4" result="final"/>
<feComposite in="final" in2="final" operator="over"/>
</filter>
</defs>
</svg>
/* ===== botoes-21st.tsx ============================================================= */
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-primary-foreground hover:bg-destructive/90",
cool: "dark:inset-shadow-2xs dark:inset-shadow-white/10 bg-linear-to-t border border-b-2 border-zinc-950/40 from-primary to-primary/85 shadow-md shadow-primary/20 ring-1 ring-inset ring-white/25 transition-[filter] duration-200 hover:brightness-110 active:brightness-90 dark:border-x-0 text-primary-foreground dark:text-primary-foreground dark:border-t-0 dark:border-primary/50 dark:ring-white/5",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants, liquidbuttonVariants, LiquidButton }
const liquidbuttonVariants = cva(
"inline-flex items-center transition-colors justify-center cursor-pointer gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-transparent hover:scale-105 duration-300 transition text-primary",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 text-xs gap-1.5 px-4 has-[>svg]:px-4",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
xl: "h-12 rounded-md px-8 has-[>svg]:px-6",
xxl: "h-14 rounded-md px-10 has-[>svg]:px-8",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "xxl",
},
}
)
function LiquidButton({
className,
variant,
size,
asChild = false,
children,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof liquidbuttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<>
<Comp
data-slot="button"
className={cn(
"relative",
liquidbuttonVariants({ variant, size, className })
)}
{...props}
>
<div className="absolute top-0 left-0 z-0 h-full w-full rounded-full
shadow-[0_0_6px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3px_rgba(0,0,0,0.9),inset_-3px_-3px_0.5px_-3px_rgba(0,0,0,0.85),inset_1px_1px_1px_-0.5px_rgba(0,0,0,0.6),inset_-1px_-1px_1px_-0.5px_rgba(0,0,0,0.6),inset_0_0_6px_6px_rgba(0,0,0,0.12),inset_0_0_2px_2px_rgba(0,0,0,0.06),0_0_12px_rgba(255,255,255,0.15)]
transition-all
dark:shadow-[0_0_8px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3.5px_rgba(255,255,255,0.09),inset_-3px_-3px_0.5px_-3.5px_rgba(255,255,255,0.85),inset_1px_1px_1px_-0.5px_rgba(255,255,255,0.6),inset_-1px_-1px_1px_-0.5px_rgba(255,255,255,0.6),inset_0_0_6px_6px_rgba(255,255,255,0.12),inset_0_0_2px_2px_rgba(255,255,255,0.06),0_0_12px_rgba(0,0,0,0.15)]" />
<div
className="absolute top-0 left-0 isolate -z-10 h-full w-full overflow-hidden rounded-md"
style={{ backdropFilter: 'url("#container-glass")' }}
/>
<div className="pointer-events-none z-10 ">
{children}
</div>
<GlassFilter />
</Comp>
</>
)
}
function GlassFilter() {
return (
<svg className="hidden">
<defs>
<filter
id="container-glass"
x="0%"
y="0%"
width="100%"
height="100%"
colorInterpolationFilters="sRGB"
>
{/* Generate turbulent noise for distortion */}
<feTurbulence
type="fractalNoise"
baseFrequency="0.05 0.05"
numOctaves="1"
seed="1"
result="turbulence"
/>
{/* Blur the turbulence pattern slightly */}
<feGaussianBlur in="turbulence" stdDeviation="2" result="blurredNoise" />
{/* Displace the source graphic with the noise */}
<feDisplacementMap
in="SourceGraphic"
in2="blurredNoise"
scale="70"
xChannelSelector="R"
yChannelSelector="B"
result="displaced"
/>
{/* Apply overall blur on the final result */}
<feGaussianBlur in="displaced" stdDeviation="4" result="finalBlur" />
{/* Output the result */}
<feComposite in="finalBlur" in2="finalBlur" operator="over" />
</filter>
</defs>
</svg>
);
}
type ColorVariant =
| "default"
| "primary"
| "success"
| "error"
| "gold"
| "bronze";
interface MetalButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ColorVariant;
}
const colorVariants: Record<
ColorVariant,
{
outer: string;
inner: string;
button: string;
textColor: string;
textShadow: string;
}
> = {
default: {
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
textColor: "text-white",
textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
},
primary: {
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
inner: "bg-gradient-to-b from-primary via-secondary to-muted",
button: "bg-gradient-to-b from-primary to-primary/40",
textColor: "text-white",
textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
},
success: {
outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
textColor: "text-[#FFF7F0]",
textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
},
error: {
outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
textColor: "text-[#FFF7F0]",
textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
},
gold: {
outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
textColor: "text-[#FFFDE5]",
textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
},
bronze: {
outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
textColor: "text-[#FFF7F0]",
textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
},
};
const metalButtonVariants = (
variant: ColorVariant = "default",
isPressed: boolean,
isHovered: boolean,
isTouchDevice: boolean,
) => {
const colors = colorVariants[variant];
const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
return {
wrapper: cn(
"relative inline-flex transform-gpu rounded-md p-[1.25px] will-change-transform",
colors.outer,
),
wrapperStyle: {
transform: isPressed
? "translateY(2.5px) scale(0.99)"
: "translateY(0) scale(1)",
boxShadow: isPressed
? "0 1px 2px rgba(0, 0, 0, 0.15)"
: isHovered && !isTouchDevice
? "0 4px 12px rgba(0, 0, 0, 0.12)"
: "0 3px 8px rgba(0, 0, 0, 0.08)",
transition: transitionStyle,
transformOrigin: "center center",
},
inner: cn(
"absolute inset-[1px] transform-gpu rounded-lg will-change-transform",
colors.inner,
),
innerStyle: {
transition: transitionStyle,
transformOrigin: "center center",
filter:
isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
},
button: cn(
"relative z-10 m-[1px] rounded-md inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-md px-6 py-2 text-sm leading-none font-semibold will-change-transform outline-none",
colors.button,
colors.textColor,
colors.textShadow,
),
buttonStyle: {
transform: isPressed ? "scale(0.97)" : "scale(1)",
transition: transitionStyle,
transformOrigin: "center center",
filter:
isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
},
};
};
const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 z-20 overflow-hidden transition-opacity duration-300",
isPressed ? "opacity-20" : "opacity-0",
)}
>
<div className="absolute inset-0 rounded-md bg-gradient-to-r from-transparent via-neutral-100 to-transparent" />
</div>
);
};
export const MetalButton = React.forwardRef<
HTMLButtonElement,
MetalButtonProps
>(({ children, className, variant = "default", ...props }, ref) => {
const [isPressed, setIsPressed] = React.useState(false);
const [isHovered, setIsHovered] = React.useState(false);
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
React.useEffect(() => {
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
}, []);
const buttonText = children || "Button";
const variants = metalButtonVariants(
variant,
isPressed,
isHovered,
isTouchDevice,
);
const handleInternalMouseDown = () => {
setIsPressed(true);
};
const handleInternalMouseUp = () => {
setIsPressed(false);
};
const handleInternalMouseLeave = () => {
setIsPressed(false);
setIsHovered(false);
};
const handleInternalMouseEnter = () => {
if (!isTouchDevice) {
setIsHovered(true);
}
};
const handleInternalTouchStart = () => {
setIsPressed(true);
};
const handleInternalTouchEnd = () => {
setIsPressed(false);
};
const handleInternalTouchCancel = () => {
setIsPressed(false);
};
return (
<div className={variants.wrapper} style={variants.wrapperStyle}>
<div className={variants.inner} style={variants.innerStyle}></div>
<button
ref={ref}
className={cn(variants.button, className)}
style={variants.buttonStyle}
{...props}
onMouseDown={handleInternalMouseDown}
onMouseUp={handleInternalMouseUp}
onMouseLeave={handleInternalMouseLeave}
onMouseEnter={handleInternalMouseEnter}
onTouchStart={handleInternalTouchStart}
onTouchEnd={handleInternalTouchEnd}
onTouchCancel={handleInternalTouchCancel}
>
<ShineEffect isPressed={isPressed} />
{buttonText}
{isHovered && !isPressed && !isTouchDevice && (
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t rounded-lg from-transparent to-white/5" />
)}
</button>
</div>
);
});
MetalButton.displayName = "MetalButton";
Já rodou em
Ainda não rodou em peça publicada.
Exige
- react
- @radix-ui/react-slot
- class-variance-authority (cva)
- cn
- Tailwind (classes arbitrárias longas de
shadow-[...])
Como se configura
--c-liq-tinta--c-liq-raio--c-liq-altura--c-liq-padding
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 deliquidbuttonVariantseLiquidButtonserem declarados. Funciona por hoisting de export em ESM, mas é artefato de cópia, não escolha. - O
MetalButtonnão entra nesse export: tem o próprioexport const. EGlassFilter,ShineEffect,colorVariantsemetalButtonVariantsnão são exportados — quem copiar só oLiquidButtonleva oGlassFilterjunto sem perceber. bg-linear-to-teinset-shadow-2xs(na variantecool) 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, @radix-ui/react-slot, cva, cn, Tailwind.
O truque não é transparência — é distorção. Um filtro SVG empurra os pixels do que está atrás do botão:
feTurbulence(fractalNoise,baseFrequency0.05, 1 oitava) gera um mapa de ruído;feGaussianBlur(σ=2) borra esse ruído — é o que troca granulado por ondulação de vidro;feDisplacementMap(scale=70) usa o canal R para o deslocamento horizontal e o B para o vertical, empurrando cada pixel do fundo;feGaussianBlur(σ=4) suaviza o resultado.
O backdrop-filter: url("#container-glass") é o que liga a camada ao filtro.
A pilha de sombras é a segunda metade do efeito: nove sombras numa box-shadow só. Duas internas opostas de 3 px fazem o bisel; duas de 1 px fazem o brilho da aresta; duas de 6 px de espalhamento fazem o corpo do vidro; três externas assentam o botão.
Tamanho em repouso: o padrão é xxl — 56 px de altura e 40 px de lateral. Hover cresce 5% em 300 ms.
O que a adaptação da OCTA mudou
- ⚠️ O
isolationfoi da camada para o botão — sem isso o efeito não existe, e é assim que o original vem. - O filtro SVG saiu de dentro do componente e virou um bloco único por página. No original cada botão renderiza o próprio
<GlassFilter />— com três botões na tela, são três<filter>com o mesmo id, e o navegador fica com o primeiro. Funcionava por acidente. - O raio ficou coerente: o original usa
rounded-fullna camada de sombra erounded-mdno botão, o que arredondava o brilho mais que a borda. - Os ids ganharam prefixo
c-para não colidirem com outro filtro da página.
Lacunas do original
⚠️ O vidro do original não renderiza — em navegador nenhum
Medido em 06/10/2026, e é o achado que mais importa deste componente. A camada do filtro vem com -z-10 dentro de um pai que não isola. Nessa posição o backdrop fica vazio, e o backdrop-filter não desenha nada — nem com url(), nem com blur(), nem com invert().
| Estrutura | Renderiza? |
|---|---|
z-index: -1 sozinho | não |
z-index: -1 + isolation: isolate na própria camada ← o do original | não |
z-index: -1 + isolation: isolate no pai ← o conserto | sim |
z-index: 0 | sim |
A adaptação põe o isolation no botão. É a diferença entre o componente funcionar e não funcionar.
O que foi medido depois do conserto
- Chromium: renderiza.
- WebKit da Playwright: não renderiza. Mas não dá para concluir daí que o Safari não suporta: num teste mínimo, sem
z-indexnenhum, obackdrop-filternão funcionou em nenhuma forma nesse build — nemblur(10px). Isso aponta para o build headless, não para o motor. Falta medir em Safari de verdade, e até lá o status honesto é não medido.
As outras
- Precisa de algo atrás para distorcer. Sobre fundo liso o componente não tem o que mostrar.
- No original, cada botão emite um
<filter id="container-glass">próprio. Vários na mesma página = ids duplicados, e o navegador fica com o primeiro. Funcionava por acidente. - O rótulo é
pointer-events-nonee a variantedefaultnão declara foco próprio — o anel depende do que vier de fora. feDisplacementMapcomscale=70é caro: numa página com muitos, custa quadro.