Locked
Free · MITThe honest-state rule: a control that cannot be used says why.
Wrap any single control. With a reason, the control stays focusable but is marked aria-disabled, its activation is blocked (click, Enter, Space), and it is described by the reason — so keyboard and screen-reader users reach the explanation on the control itself. The reason shows as a tooltip, or printed under the control with inline. Pressing the control reveals the reason (the tooltip is held open, the printed reason is emphasised) and, under data-motion="expressive" only, shakes it briefly; with reduced motion or the default subtle preset, only the reason is shown.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/locked
Brings in: tooltip, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { Button } from "@/components/ui/button"
import { Locked } from "@/components/locked"
export default function Example() {
return (
<div className="flex flex-wrap items-start gap-6">
<Locked reason="Only an admin can publish this.">
<Button>Publish</Button>
</Locked>
<Locked inline reason="You wrote this change, so someone else must approve it.">
<Button variant="outline">Approve</Button>
</Locked>
</div>
)
}<Locked>
| Prop | Type | Default | Description |
|---|---|---|---|
| reason | string | null | undefined | — | Why the control can't be used. Without one, the control renders as normal. |
| inline | boolean | undefined | false | Print the reason under the control instead of in a tooltip. |
| childrenrequired | Lockable | — | What goes inside. |
| className | string | undefined | — | Extra classes for the outer element. |