Skip to content
txbrendan/ui

Access List

beta

Manage who has access and what they can do.

Preview

Change levels and answer requests

  • Rob KerrAsks to edit · Client, guest
  • Nina ShahAsks to comment · Contractor · Support
  • Sam CarterDesign lead · DesignOwner
  • Kate ClarkeEngineer · Engineering
  • Tom HaleClient, guest
  • ella.moss@example.comNo account · commented as Ella Moss
  • +44 7700 900123Text message link
  • Anyone with the linkView only · 3 visitors

Guests allowed to edit

  • Rob KerrAsks to edit · Client, guest
  • Tom HaleClient, guest

Read only, with levels as words

  • Rob KerrAsks to edit · Client, guest
  • Nina ShahAsks to comment · Contractor · Support
  • Sam CarterDesign lead · DesignOwner
  • Kate ClarkeEngineer · EngineeringCan edit

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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>
  );
}