GuardedAction
A button that can be blocked with a reason, which the reader can reach and read with the mouse and the keyboard.
Use Guarded for an action the reader may not take right now, when they should learn why:
a missing role, a record that is locked, a step that comes first. Blocked, the button stays in the
tab order and shows the reason in a tooltip on hover and focus;
pressing it does nothing. Without a reason it is an ordinary Button.
Approved 0 times.
// apps/docs/examples/ui/guarded-action.tsx'use client';
import {useState} from 'react';
import {GuardedAction} from '@workspace/active-ui/components/composed/guarded-action';import { Field, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Switch} from '@workspace/active-ui/components/primitives/switch';
/** * An action with the reason it is blocked. Without a reason it is an * ordinary button. Blocked, it stays focusable: hover or focus it to read * why, and pressing it does nothing. The permission is a switch here; in an * application it comes from your own rules. */export default function GuardedActionExample() { const [mayApprove, setMayApprove] = useState(false); const [approved, setApproved] = useState(0); return ( <div className="grid w-full max-w-sm justify-items-start gap-6"> <Field orientation="horizontal"> <Switch id="may-approve" checked={mayApprove} onCheckedChange={setMayApprove} /> <FieldLabel htmlFor="may-approve">I may approve statements</FieldLabel> </Field> <div className="flex gap-3"> <GuardedAction variant="outline" onClick={() => setApproved((count) => count + 1)} disabledReason={ mayApprove ? null : 'You need the approver role to approve statements.' } > Approve </GuardedAction> <GuardedAction variant="outline" disabledReason="Statements that were sent cannot be changed." reasonSide="bottom" > Edit </GuardedAction> </div> <p className="text-muted-foreground text-sm" aria-live="polite"> Approved {approved} {approved === 1 ? 'time' : 'times'}. </p> </div> );}What the composition adds#
A natively disabled button gets no hover and no focus, so a tooltip on it never opens. A
Guarded with a disabled:
is
aria-instead ofdisabled="true" disabled, so it stays focusable and receives the pointer;blocks the click,
EnterandSpacebefore your handler runs;makes the reason the button's accessible description, so a screen reader announces it with the button (the tooltip itself is not linked to its trigger);
looks disabled, through classes on
aria-;disabled has one tab stop, with no wrapper element.
disabled without a reason keeps its ordinary meaning, for a button that is briefly off while a
request runs.
Usage#
import {GuardedAction} from '@workspace/active-ui/components/composed/guarded-action';
<GuardedAction onClick={approve} disabledReason={mayApprove ? null : 'You need the approver role.'}> Approve</GuardedAction>;What your application provides#
Whether the action is allowed, and the reason in the reader's language. The component knows no permissions or rules.
The check on the server as well: a blocked button is presentation, not protection.
Accessibility and keyboard#
The blocked button is announced as dimmed or unavailable, with the reason as its description, and stays reachable with
Tab.The reason appears on keyboard focus and on hover;
reasonplaces it. Your ownSide aria-is kept after the reason.describedby
API#
Import from @workspace/
Props marked Base UI are behaviour of the Base UI part this component is built on.
Props
- Type
ReactNode
The visible action text. Required: a button without a label is not a button.
- Type
string | null
The translated reason why this action is not available. null or undefined means: no block, and then this is an ordinary button.
The caller decides the reason. This component knows no permissions, business rules, workflow states or eligibility: it only knows there is a reason and shows it.
- Type
boolean- Default
false
Whether the button should be focusable when disabled.
- Type
"bottom""left""right""top"- Default
'top'
Side where the reason appears. Default top, just like the Tooltip itself.
- Type
"default""icon""icon-lg" "icon-sm" "icon-xs" "lg""sm""xs"null
The height and padding. xs, sm, default and lg for a button with text; icon-, icon-, icon and icon- for a square button with only an icon, which then needs an accessible name (aria-).
- Type
"default""destructive""ghost""link""outline""secondary"null
The visual weight of the button. default for the main action of a screen or form; outline and secondary for other actions; ghost for a quiet action in a toolbar or a list; destructive for an action that deletes or cannot be undone; link for an action that reads as a link. Every variant but link sinks 1px while pressed; a link-styled action keeps its text and icon in place (button-).
- Type
ComponentRender Fn<…> ReactElement<…>
Allows you to replace the component's HTML element with a different tag, or compose it with another component.
Accepts a React or a function that returns the element to render.
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
Types
The props of Guarded, each described in its prop list on this page.
interface GuardedActionProps extends React.ComponentProps<
typeof Button
> {
disabledReason?: string | null;
children: React.ReactNode;
reasonSide?: 'top' | 'bottom' | 'left' | 'right';
}