Skip to Content
Piplup
@piplup/react-aclComponents

Can

Render content only when the principal holds the required permissions or roles, with ALL/ANY matching, a fallback and a loading placeholder.

Can renders its children only when the current principal passes a permission and role check. It reads the principal from the nearest AccessControlProvider and throws if there is none.

Use it for markup: buttons, menu items, page sections. When you need the result as a boolean — to filter a list, disable instead of hide, or check access in an event handler — use useAccessControl instead.

Import

import { Can } from "@piplup/react-acl";

Usage

Toggle permissions and the role to see ALL, ANY, a wildcard grant and a combined role-and-permission check respond. Tick invoice:* on its own to watch one wildcard satisfy invoice:line:edit.

"use client";import * as React from "react";import Alert from "@mui/material/Alert";import Checkbox from "@mui/material/Checkbox";import FormControlLabel from "@mui/material/FormControlLabel";import Stack from "@mui/material/Stack";import { AccessControlProvider, Can } from "@piplup/react-acl";const grantable = ["invoice:read", "invoice:write", "invoice:*"];export default function Page() {  const [permissions, setPermissions] = React.useState<string[]>(["invoice:read"]);  const [isAdmin, setIsAdmin] = React.useState(false);  const principal = React.useMemo(    () => ({ permissions, roles: isAdmin ? ["admin"] : ["member"] }),    [permissions, isAdmin],  );  const toggle = (permission: string) =>    setPermissions((current) =>      current.includes(permission)        ? current.filter((p) => p !== permission)        : [...current, permission],    );  return (    <Stack spacing={2}>      <Stack direction="row" sx={{ flexWrap: "wrap" }}>        {grantable.map((permission) => (          <FormControlLabel            key={permission}            label={permission}            control={              <Checkbox                checked={permissions.includes(permission)}                onChange={() => toggle(permission)}              />            }          />        ))}        <FormControlLabel          label="admin role"          control={<Checkbox checked={isAdmin} onChange={(e) => setIsAdmin(e.target.checked)} />}        />      </Stack>      <AccessControlProvider principal={principal}>        <Stack spacing={1}>          {/* Default mode is ALL: every listed permission is required. */}          <Can            permission={["invoice:read", "invoice:write"]}            fallback={<Alert severity="error">ALL: needs read and write</Alert>}          >            <Alert severity="success">ALL: can edit invoices</Alert>          </Can>          {/* ANY: one matching permission is enough. */}          <Can            mode="ANY"            permission={["invoice:read", "invoice:write"]}            fallback={<Alert severity="error">ANY: no invoice permission</Alert>}          >            <Alert severity="success">ANY: can open the invoices page</Alert>          </Can>          {/* Tick `invoice:*` on its own to see one wildcard satisfy a deeper name. */}          <Can            permission="invoice:line:edit"            fallback={<Alert severity="error">invoice:line:edit: denied</Alert>}          >            <Alert severity="success">invoice:line:edit: granted by invoice:*</Alert>          </Can>          {/* Role and permission are checked separately and both must pass. */}          <Can            permission="invoice:write"            role="admin"            fallback={<Alert severity="error">admin + invoice:write: denied</Alert>}          >            <Alert severity="success">admin + invoice:write: can void invoices</Alert>          </Can>        </Stack>      </AccessControlProvider>    </Stack>  );}

What it renders

Can renders exactly one of its three slots, checked in this order:

Provider stateRendersDefault
loading is trueloadingnothing
Check failsfallbacknothing
Check passeschildren—

No wrapper element is added around any of them.

How access is decided

permission and role are checked separately, and both must pass.

RequirementALLANY
Prop omitted or []PassesPasses
"a"Principal holds aPrincipal holds a
["a", "b"]Holds a and bHolds a or b

permission defaults to ALL. role defaults to ANY. mode overrides both:

<Can mode="ANY" permission={["post:publish", "post:schedule"]} role={["editor", "owner"]}>
  {/* editor OR owner, AND post:publish OR post:schedule */}
  <PublishMenu />
</Can>

A few consequences:

  • <Can> with no permission and no role renders its children for every loaded principal.
  • role="admin" with no permission checks only the role.
  • permission="invoice:write" with role="admin" requires both. To allow either one, use two Can blocks or useAccessControl.
  • Permissions resolve wildcards; roles are compared exactly. See Permissions.

Can hides UI. It does not protect data. Anything the principal must not access also needs a check on your server.

withCan

withCan wraps a whole component instead of a piece of markup. It takes the same options as Can, typed as Omit<CanProps, "children">:

import { withCan } from "@piplup/react-acl";

const BillingPanel = withCan(Panel, {
  permission: ["billing:read", "billing:write"],
  mode: "ANY",
  role: "admin",
  loading: <Skeleton />,
  fallback: <NoAccess />,
});

A single permission can be passed directly, with an optional fallback as the third argument:

const AdminPanel = withCan(Panel, "admin:manage");
const Guarded = withCan(Panel, "admin:manage", <NoAccess />);

The returned component forwards its own props through to the wrapped one and is named withCan(Panel) in React DevTools.

Props

Prop

Type

On this page