---
id: "packages/active-ui/guarded-action"
title: "GuardedAction"
description: "A button that can be blocked with a reason, which the reader can reach and read with the mouse and the keyboard."
url: "/packages/active-ui/guarded-action"
locale: "en"
revision: "cf1c6ca45e028309"
---

# GuardedAction

Use `GuardedAction` 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](/packages/active-ui/tooltip) on hover and focus;
pressing it does nothing. Without a reason it is an ordinary [Button](/packages/active-ui/button).

**Example: Actions blocked with a reason**

Client: Runs entirely in your browser. Source: `examples/ui/guarded-action.tsx`.

```tsx
// 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
`GuardedAction` with a `disabledReason`:

- is `aria-disabled="true"` instead of `disabled`, so it stays focusable and receives the pointer;
- blocks the click, `Enter` and `Space` before 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

```tsx
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; `reasonSide` places it. Your own
  `aria-describedby` is kept after the reason.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | The visible action text. Required: a button without a label is not a button. |
| `disabledReason` | `string \| null` | no | 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. |
| `reasonSide` | `"bottom" \| "left" \| "right" \| "top"` | no | Side where the reason appears. Default `top`, just like the Tooltip itself. |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | 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-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**GuardedActionProps**

```ts
interface GuardedActionProps extends React.ComponentProps<
  typeof Button
> {
  disabledReason?: string | null;
  children: React.ReactNode;
  reasonSide?: 'top' | 'bottom' | 'left' | 'right';
}
```
