Buttons
FreePress 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.
npx @galactrex/ui add press-buttonUsage
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.
"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
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Label. |
depth | number | 5 | Key height in px. |
pressed | boolean | — | Stays down, as a toggle (aria-pressed). |
...button props | ButtonHTMLAttributes | — | 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.