Locked

Free · MIT

The 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.

Markdown

Preview

Install

With the Candor CLI

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

TSX · The example above
"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>

PropTypeDefaultDescription
reasonstring | null | undefined—Why the control can't be used. Without one, the control renders as normal.
inlineboolean | undefinedfalsePrint the reason under the control instead of in a tooltip.
childrenrequiredLockable—What goes inside.
classNamestring | undefined—Extra classes for the outer element.