Access List
betaManage who has access and what they can do.
Preview
Change levels and answer requests
- Rob Kerr
- Nina Shah
- Sam CarterOwner
- Kate Clarke
- Tom Hale
- ella.moss@example.com
- +44 7700 900123
- Anyone with the link
Guests allowed to edit
- Rob Kerr
- Tom Hale
Read only, with levels as words
- Rob Kerr
- Nina Shah
- Sam CarterOwner
- Kate ClarkeCan edit
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/access-list
Use it for
- The people part of a share sheet: who can open a page, a plan, or a file, and whether they view, comment, or edit.
- Each entry is a person, an email with no account, a phone number, or the link. Give each an id.
- Each row's level is a menu: Can view, Can comment, Can edit, then Remove access. A change applies at once, and ⌘Z takes it back for 8 seconds.
- Remove access shows Removed with Undo on the row. onRemove runs once the window ends.
- Set asked on an entry to show a request. Requests come first, with Approve and Decline in the row.
- Guests view or comment. Set guestsCanEdit when the owner lets them edit. The link and a phone number only view.
- Put it in a Card with className="gap-0 py-0". Its rows bring their own padding and lines.
Avoid it for
- Picking people to share with. Use a token field or a search above the list.
- Roles in a team, like admin or member. This is about access to one thing.
- Setting manage on someone who cannot change access. Pass manage={false} and the levels are plain words.
Props
- entries
Required. { id, person?, photo?, email?, phone?, link?, meta?, guest?, owner?, level?, asked? }. level and asked are "view" | "comment" | "edit".
AccessEntry[]
- guestsCanEdit
The owner lets guests edit. Off, guests view or comment.
boolean · default false
- manage
You can change access. Off, levels are plain words and requests have no buttons.
boolean · default true
- onLevel
A level changed, at once. It runs again with the old level if the change is undone.
(id: string, level: "view" | "comment" | "edit") => void
- onRemove
Removed, once the 8-second undo window ends.
(id: string) => void
- onApprove
A request approved, once the undo window ends, with the level given.
(id: string, level: "view" | "comment" | "edit") => void
- onDecline
A request declined, once the undo window ends.
(id: string) => void
- className
Extra classes on the list. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| entries | AccessEntry[] | - | Required. { id, person?, photo?, email?, phone?, link?, meta?, guest?, owner?, level?, asked? }. level and asked are "view" | "comment" | "edit". |
| guestsCanEdit | boolean | false | The owner lets guests edit. Off, guests view or comment. |
| manage | boolean | true | You can change access. Off, levels are plain words and requests have no buttons. |
| onLevel | (id: string, level: "view" | "comment" | "edit") => void | - | A level changed, at once. It runs again with the old level if the change is undone. |
| onRemove | (id: string) => void | - | Removed, once the 8-second undo window ends. |
| onApprove | (id: string, level: "view" | "comment" | "edit") => void | - | A request approved, once the undo window ends, with the level given. |
| onDecline | (id: string) => void | - | A request declined, once the undo window ends. |
| className | string | - | Extra classes on the list. Later classes win. |
Accessibility
- A list of rows. Each level is a menu button named by the person and the level, like "Kate Clarke can edit".
- In the menu, the arrow keys move without wrapping, Enter chooses, and Esc closes it and puts focus back on the button. The current level is a checked menuitemradio.
- After Remove access, the same button says "Kate Clarke removed" and is the Undo, armed 1.2 seconds after a key press. It no longer says it opens a menu.
- Decline is an icon named "Decline" plus the name, and its tooltip says Decline. Approve's name carries the name too.
- Each result is said once in the page's polite live region. ⌘Z or Ctrl+Z takes back the newest change, outside a text field.
Examples
In a share sheet
"use client";
import { useState } from "react";
import { AccessList, type AccessEntry } from "@/components/ui/access-list";
import { Card } from "@/components/ui/card";
export function Access() {
const [entries, setEntries] = useState<AccessEntry[]>([
{ id: "sam", person: "Sam Carter", meta: "Design lead", owner: true, level: "edit" },
{ id: "rob", person: "Rob Kerr", meta: "Client, guest", guest: true, asked: "comment" },
{ id: "kate", person: "Kate Clarke", level: "edit" },
{ id: "link", link: true, meta: "View only · 3 visitors", level: "view" },
]);
const set = (id: string, change: Partial<AccessEntry>) =>
setEntries((all) => all.map((entry) => (entry.id === id ? { ...entry, ...change } : entry)));
const drop = (id: string) => setEntries((all) => all.filter((entry) => entry.id !== id));
return (
<Card className="gap-0 py-0">
<AccessList
entries={entries}
onLevel={(id, level) => set(id, { level })}
onRemove={drop}
onApprove={(id, level) => set(id, { asked: undefined, level })}
onDecline={drop}
/>
</Card>
);
}