Account

Buttons

Free

Press Button

It stands on its own edge and sinks when pressed. Works as a toggle with `pressed`. CSS transitions, so it reacts on the first frame.

Install

No key needed.

Terminal
npx @galactrex/ui add press-button

Usage

Example
import { PressButton } from "@/components/galactrex/press-button";

<PressButton pressed={muted} onClick={() => setMuted((m) => !m)}>Mute</PressButton>

Source

Installs to components/galactrex/press-button.tsx, with the shared motion primitives beside it.

components/galactrex/press-button.tsx
"use client";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { cx } from "@/components/galactrex/motion/cx";

export type PressButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "style"> & {
  children: ReactNode;
  /** how tall the key stands, in px */
  depth?: number;
  /** a pressed-in state that stays down, for toggles */
  pressed?: boolean;
};

/** A button with real travel, like a mechanical key: it stands on its own edge and sinks when pressed, the
    edge collapsing beneath it. Works as a toggle with `pressed` (aria-pressed). Pure CSS transitions on
    transform, so it responds on the first frame and never delays the click. */
export function PressButton({ children, depth = 5, pressed, className, type = "button", ...rest }: PressButtonProps) {
  return (
    <button
      type={type}
      aria-pressed={pressed}
      {...rest}
      className={cx("group/key relative inline-flex select-none rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-gxui-ink focus-visible:ring-offset-2 focus-visible:ring-offset-gxui-bg disabled:opacity-50", className)}
    >
      {/* the key's travel lives on an inner span: a host's reset may strip a button's own padding */}
      <span className="relative block" style={{ paddingBottom: depth }}>
        <span aria-hidden className="absolute inset-x-0 bottom-0 rounded-lg bg-gxui-line" style={{ top: depth }} />
        <span
          className={cx(
            "relative inline-flex items-center gap-2 rounded-lg border border-gxui-line bg-gxui-surface px-5 py-2.5 text-sm font-medium text-gxui-ink transition-transform duration-75 ease-out group-active/key:translate-y-[var(--gxui-depth)] motion-reduce:transition-none",
            pressed && "translate-y-[var(--gxui-depth)] bg-gxui-raised",
          )}
          style={{ ["--gxui-depth" as string]: `${depth - 1}px` }}
        >
          {children}
        </span>
      </span>
    </button>
  );
}

Props

PropTypeDefaultDescription
childrenReactNode—Label.
depthnumber5Key height in px.
pressedboolean—Stays down, as a toggle (aria-pressed).
...button propsButtonHTMLAttributes—onClick, disabled, type and the rest.

Variants

  • Key. A single satisfying action.
  • Toggle. Bold, italic, mute: `pressed` keeps it down.

Accessibility

  • aria-pressed when used as a toggle.

Motion and reduced motion

  • A 75 ms transform on press.
  • Reduced motion: no transition, still moves.