ToggleButton
A button that stays pressed, with a check mark in its corner while it is on.
import { ToggleButton } from "@cynco-labs/ui"; Example
import { ToggleButton } from "@cynco-labs/ui";
import { ChartBarIcon, FunnelSimpleIcon, UsersIcon } from "@phosphor-icons/react";
import { useState } from "react";
export default function ToggleButtonDemo() {
const [chart, setChart] = useState(true);
return (
<div className="flex flex-wrap items-center gap-2">
<ToggleButton iconOrientation="left" icon={<ChartBarIcon />} pressed={chart} onPressedChange={setChart}>
Show chart
</ToggleButton>
<ToggleButton iconOrientation="left" icon={<UsersIcon />} defaultPressed={false}>
Group by customer
</ToggleButton>
<ToggleButton icon={<FunnelSimpleIcon />} defaultPressed tooltip="Show filters" />
</div>
);
} packages/ui/src/base/components/general/toggle-button.tsx:17-76 /**
* A button that stays pressed: a filter, a view mode, a panel toggle. It reports `aria-pressed`
* rather than posing as a checkbox, and the corner mark means the state never rests on colour.
*/
function ToggleButton({
className,
variant = "secondary",
size = "sm",
iconOrientation = "center",
pressed: pressedProp,
defaultPressed = false,
onPressedChange,
onClick,
onKeyDown,
children,
tooltip,
"aria-label": ariaLabel,
...props
}: ToggleButtonProps) {
const [pressed = false, setPressed] = useControllableState({
prop: pressedProp,
defaultProp: defaultPressed,
onChange: onPressedChange,
});
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setPressed(!pressed);
onClick?.(event);
// Drop focus after a pointer click so the active state doesn't stick. Keyboard activation
// (detail 0) keeps focus, so keyboard users don't lose their place.
if (event.detail > 0) event.currentTarget.blur();
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
// Suppress the native button click without toggling, so cmd/ctrl+enter
// only triggers sheet-level submit shortcuts.
if (event.key === "Enter" && hasSubmitShortcutModifier(event)) event.preventDefault();
onKeyDown?.(event);
};
return (
<IconButton
data-slot="toggle-button"
variant={variant}
size={size}
iconOrientation={iconOrientation}
className={cn("relative", pressed ? "border-primary text-foreground" : "text-tertiary-foreground", className)}
tooltip={tooltip}
// The corner mark is decoration, so the button is still icon-only without a label of its own.
aria-label={ariaLabel ?? (children == null && typeof tooltip === "string" ? tooltip : undefined)}
onClick={handleClick}
onKeyDown={handleKeyDown}
aria-pressed={pressed}
{...props}
>
{children}
{pressed && <SelectedMark />}
</IconButton>
);
} Use
Toggling views and options in toolbars: show a chart, group rows, reveal filters.
Props
IconButton props plus pressed / defaultPressed, onPressedChange, icon and iconOrientation (left with a label, center icon-only, which needs tooltip or aria-label).
| Prop | Type | Default |
|---|---|---|
pressed | boolean Controlled pressed state. | – |
defaultPressed | boolean Initial pressed state for uncontrolled use. | false |
onPressedChange | (pressed: boolean) => void | – |
variant | "primary" | "secondary" | "skeleton" | "muted" | "destructive" | "dotted" | "secondary" |
size | "sm" | "md" | "icon" | "sm" |
icon | ReactNode | – |
iconOrientation | "center" | "left" | "right" | "center" |
isLoading | boolean Swaps the label for a spinner and disables the button, while the label stays its accessible name. | – |
asChild | boolean Radix compatibility: render the single child element instead of a `<button>`. | – |
render | ReactElement The element to render instead of a `<button>` (Base UI). | – |
focusableWhenDisabled | boolean Keep the button in the tab order while disabled (Base UI). Set automatically while `isLoading`. | – |
tooltip | ReactNode A hint on hover and focus. On an icon-only button it also names the button when no `aria-label` is given. | – |
rightIcon | ReactNode | – |
tooltipSide | "top" | "bottom" | "left" | "right" Which side the tooltip opens on. | – |
Also accepts ComponentProps<"button">, and className.
Notes
It reports aria-pressed rather than posing as a checkbox; for a real checkbox use Checkbox. The corner mark means the state never rests on colour, and it sits slightly outside the button, so a clipping parent hides part of it.