Accessible buttons whose surface is a small live shader: plasma, embers, a travelling beam, sliding labels, dot-matrix and frosted glass.

  • shader
  • webgl
  • button
  • plasma
  • glass
  • cta
  • hover
Preset

Installation

pnpm dlx shadcn@latest add https://kagadmodyaa.vercel.app/r/shader-buttons.json

Needs a React + Tailwind project set up with shadcn. Copies the effect into your project, along with lib/effect-runtime.ts, the small WebGL layer it runs on. No other dependencies.

Or from npm

pnpm add @kagadmodyaa/effects
import { ShaderButton } from '@kagadmodyaa/effects/shader-buttons'

One package with every free effect, one import path each. No Tailwind or shadcn needed, and no stylesheet to import: the few layout rules an effect needs come with it. The npm package and the command above ship the same source.

Usage

Use it like any other component. Choose a theme with preset; any prop you pass as well overrides the preset.

import { ShaderButton } from '@/components/effects/shader-buttons/shader-buttons'

export function Cta() {
  return (
    <div className="flex items-center gap-3">
      <ShaderButton preset="Plasma" size="lg" onClick={() => signUp()}>
        Get started
      </ShaderButton>
      <ShaderButton variant="glass" label="Book a demo" />
      <ShaderButton variant="beam" loading>Saving</ShaderButton>
    </div>
  )
}

// A normal <button>: type, disabled, ref, aria-* and every event prop work as usual.
// The stage above is <ShaderButtonShowcase />, also exported, if you want the same grid.
<ShaderButton preset="Ember" variant={"plasma"} />

Presets

  • Plasma
  • Ember
  • Beam
  • Sliding
  • Dotted
  • Glass

Props

NameTypeDefaultDescription
preset'Plasma' | 'Ember' | 'Beam' | 'Sliding' | 'Dotted' | 'Glass'none (the defaults below)A named theme. Any prop you pass as well overrides the preset’s value.
variant'plasma' | 'ember' | 'beam' | 'sliding' | 'dotted' | 'glass''plasma'Which shader draws the surface.
size'sm' | 'md' | 'lg''md'Button size: sm 36px, md 44px, lg 56px tall.
label'Get started' | 'Start free' | 'Book a demo' | 'Join waitlist' | 'Continue''Get started'The label, when you do not pass children. Always real text, never part of the shader.
tone'auto' | 'dark' | 'light''auto'Which surface the button sits on. auto follows the page theme.
colorAstring'#ff5fa2'First palette colour. A hex colour.
colorBstring'#5b3dff'Second palette colour. A hex colour.
intensitynumber1How strong the effect is: glow, sparks, highlights. 0 is calm. Range 0 to 1.5.
speednumber1How fast the surface moves. 0 freezes it. Range 0 to 2.
radiusnumber32Corner radius. At or above half the height it is a pill. Range 0 to 32 px.
classNamestring—Extra class on the root element. The effect fills its parent: give the parent a height.
styleCSSProperties—Inline style on the root element.

Files

What the command adds to your project, and where.

  • components/effects/shader-buttons/shader-buttons.tsx

    The component.

  • components/effects/shader-buttons/button.tsx

    The component.

  • components/effects/shader-buttons/surface.tsx

    The component.

  • components/effects/shader-buttons/showcase.tsx

    The component.

  • components/effects/shader-buttons/shaders.ts

    The fragment shader, as a string. Edit it to change the picture.

  • components/effects/shader-buttons/styles.ts

    The component.

  • lib/effect-runtime.ts

    Shared by every shader effect: resizing, pausing, reduced motion, context loss and the no-WebGL fallback. Installed once.

Behaviour

  • A real <button>: Enter and Space activate it, Tab focuses it, and a focus-visible ring shows on keyboard focus. The label is ordinary text above the canvas, never part of the shader.
  • disabled dims it and freezes the shader; loading shows a spinner, sets aria-busy and ignores clicks while keeping focus.
  • The canvas is created only while the button is near the viewport, and at most eight draw at once on a page. Beyond that a button keeps its CSS look.
  • It pauses off screen and in a background tab, lowers its resolution automatically if frames run slow, and draws one still frame under prefers-reduced-motion.
  • Without WebGL, and before the first frame, you see a CSS version of the same variant in the same colours.
  • Tone is 'auto' by default: it follows a dark or light class on <html> (or the OS setting) and re-reads it when the theme changes.

See all effects