Share Dialog
Share a document, board or file with people and by link: invite, see who has access and set what the link allows.
import { ShareAccess, ShareDialog, ShareInvite, SharePerson, SharePreview, ShareRecipient, ShareRole, ShareSuggestion } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a Share Dialog where someone shares one thing with others: a document, a board, a file or a folder. For inviting people to a whole workspace, use the Team members block; for an ordinary short task in a modal, a Dialog.
It is a component with a data contract, not a page: you own the list of people, so each handler updates your data and the dialog shows what you pass back.
'use client'
import { useState } from 'react'
import { Button, ShareDialog, type SharePerson } from '@youj/react'
export function ShareRoadmap({ initial }: { initial: SharePerson[] }) {
const [people, setPeople] = useState(initial)
return (
<ShareDialog
trigger={<Button variant="outline">Share</Button>}
resource="Q3 roadmap"
link="https://acme.test/docs/q3-roadmap"
organization="Acme"
people={people}
suggestions={directory}
onInvite={async ({ recipients, role, message }) => {
const invited = await api.invite(recipients, role, message)
setPeople((current) => [...current, ...invited])
}}
onRoleChange={async (person, role) => {
await api.setRole(person.id, role)
setPeople((current) => current.map((p) => (p.id === person.id ? { ...p, role } : p)))
}}
onRemove={async (person) => {
await api.remove(person.id)
setPeople((current) => current.filter((p) => p.id !== person.id))
}}
onAccessChange={(access) => api.setAccess(access)}
/>
)
}Every handler may return a promise. Resolve and update people; reject with an Error and its message shows next to the action, with the dialog still open. Roles default to Viewer, Commenter and Editor; pass roles for your own ({ value, label }). Pass loading while the list is on its way and the people area shows skeleton rows in the same space.
Description
A roadmap shared with an editor, a commenter and a pending invitation. Type an address or pick a person or group, choose a role and send.
The dialog is Google-Drive-style sharing in one window. Everyone's role can change from their row, access can be removed, and a pending invitation is marked and can be resent or cancelled. Each action waits for your handler and fails on its own row, so nothing closes while something is wrong.
A document anyone at the company can comment on, and no one outside it can open.
General access offers Restricted, Anyone at <organization> (only when you pass organization) and Anyone with the link. The two open ones take a role for the people who arrive by the link. A change is applied once your onAccessChange resolves; if it rejects, the old value stays and its message shows under the setting.
A viewer who may see who has access but can change nothing.
readOnly removes the invite field, turns every role select into plain text and drops the remove buttons. Copy link stays: the viewer can still pass the link on, within what general access allows.
A file shared from its own screen: a card with its name and one line of detail sits above the invite field, and general access is a short list of switches.
preview takes { title, description?, thumbnail? }; without a thumbnail the card shows a document icon. accessLayout="switches" is the compact general access for a narrower dialog: no message field, and one row per way in, each a title, a line and a Switch on the same edge. Only people invited carries the role invitations start on (so the invite field loses its own role menu) and its switch stays on until you turn another row on. Anyone at <organization> and Anyone with the link open the document when on, with their own role menu beside the switch; turning one off goes back to invited people only. onAccessChange and the rest work as in the default layout.
Code
share-dialog-demo.tsx, the Share a document story. The component itself comes from the package.
Reference
Propsresource, link, people, suggestions, organization, access, readOnly, loading, handlers…
Props
| Prop | Type | |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | – |
onOpenChange | (open: boolean) => void | – |
trigger | ReactElementThe element that opens the dialog, pressed to open it. | – |
resourceRequired | stringThe name of what is shared; it's the dialog's title. | – |
linkRequired | stringThe address | – |
roles | ShareRole[]Default: Viewer, Commenter and Editor. | – |
defaultInviteRole | stringThe role the invite starts on. Default: the first role. | – |
peopleRequired | SharePerson[]Everyone with access, the owner and pending invitations included. | – |
loading | booleanThe list is on its way: skeleton rows instead of people. | – |
suggestions | ShareSuggestion[]People and groups the invite field offers. Those already in default [] | [] |
organization | stringThe organization's name; it adds | – |
preview | SharePreviewShows the thing being shared as a card above the invite field. | – |
accessLayout | 'select' | 'switches'How general access is laid out. default 'select' | 'select' |
access | ShareAccess | – |
defaultAccess | ShareAccessdefault { scope: 'restricted', role: 'viewer' } | { scope: 'restricted', role: 'viewer' } |
onAccessChange | (access: ShareAccess) => Promise<unknown> | unknownCalled when someone changes general access; resolve to apply it. Rejecting keeps the old value and shows the message. | – |
readOnly | booleanThe viewer may look but not change anything: no invite field, no role or access selects, no remove. Copy link stays. | – |
onInvite | (invite: ShareInvite) => Promise<unknown> | unknownSends the invitation; resolve when it's out and add the people to | – |
onRoleChange | (person: SharePerson, role: string) => Promise<unknown> | unknown | – |
onRemove | (person: SharePerson) => Promise<unknown> | unknownRemoves someone's access. | – |
onResend | (person: SharePerson) => Promise<unknown> | unknown | – |
onCancelInvite | (person: SharePerson) => Promise<unknown> | unknownCancels a pending invitation. default `onRemove` | `onRemove` |
onCopyLink | () => voidCalled after the link was copied. | – |
className | string | – |
Invite field
Type an address and press Enter or a comma (a semicolon works too); a pasted list is split into one chip each. Suggestions open on focus and filter as you type; people already in people are left out, and a group suggestion (one with members) shows its size. An address that isn't valid, or belongs to someone who already has access, stays as a danger chip with its reason and blocks Send until it is removed. Backspace on an empty field takes the last chip back. Send is an Async Button: onInvite receives { recipients, role, message }, and on success the chips and message clear.
Data attributes
The preview card is share-dialog-preview. The invite, people and general access areas carry data-slot values share-dialog-invite, share-dialog-people and share-dialog-access; the chip field is share-dialog-recipients, and an invalid chip carries data-invalid.
LabelsEvery string, in every built-in language
Labels
Every string is a core label (shareDialog…) in every built-in language, so the provider's locale or labels (see Internationalization) rewrites them. The roles' names are labels too (shareDialogViewer, shareDialogCommenter, shareDialogEditor); your own roles bring their own names. The organization, the resource and people's names are yours and arrive inside the sentences.
MotionRows enter and leave · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Open and closethe dialog’s own rise and scrim, as any Dialog | no token | ||
| A person added or removedtheir row fades into its slot and the rows below glide | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Send and Copy linkthe Async Button’s pending, success and error states | no token |
Reduced motion
Rows appear and leave at once and the buttons change without morphing; every message is still shown and announced.
AccessibilityFocus, combobox, live regions
Accessibility
- The dialog is named by its title (
Share “<resource>”) and focus opens in the invite field, so people can type straight away. InreadOnly, where there's no field, focus starts on the first control as in any dialog. - The invite field is an ARIA combobox over a listbox of suggestions: ArrowDown and ArrowUp move through them, Enter chooses, and Escape closes the list before a second Escape closes the dialog. Each chip's remove button is named after its person (
Remove Ada). - An invalid chip says why in text (read with the chip) and in an alert under the field, which the field points to with
aria-describedbyandaria-invalid. - The people list is a named list inside a fixed-height scroll region; each role select is named after its person (
Role for Grace Hopper) and each icon button says what it does and to whom (Cancel invitation for Alan Turing). - While loading, the area is
aria-busyand saysLoading people…. - A polite live region announces invitations sent, access removed, a role changed, general access changed and the link copied. A failure is an alert on the row, setting or button it belongs to.