Skip to content

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/react

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

Reference

Propsresource, link, people, suggestions, organization, access, readOnly, loading, handlers…

Props

ShareDialog props
PropType
openboolean–
defaultOpenboolean–
onOpenChange(open: boolean) => void–
triggerReactElement

The element that opens the dialog, pressed to open it.

–
resourceRequiredstring

The name of what is shared; it's the dialog's title.

–
linkRequiredstring

The address Copy link puts on the clipboard.

–
rolesShareRole[]

Default: Viewer, Commenter and Editor.

–
defaultInviteRolestring

The role the invite starts on. Default: the first role.

–
peopleRequiredSharePerson[]

Everyone with access, the owner and pending invitations included.

–
loadingboolean

The list is on its way: skeleton rows instead of people.

–
suggestionsShareSuggestion[]

People and groups the invite field offers. Those already in people are left out.

default []

[]
organizationstring

The organization's name; it adds Anyone at … to general access.

–
previewSharePreview

Shows the thing being shared as a card above the invite field.

–
accessLayout'select' | 'switches'

How general access is laid out. select (default): one menu for who can open the link, with a role beside it. switches: compact rows with a Switch each, the invite role on the "Only people invited" row, and no message field.

default 'select'

'select'
accessShareAccess–
defaultAccessShareAccess

default { scope: 'restricted', role: 'viewer' }

{ scope: 'restricted', role: 'viewer' }
onAccessChange(access: ShareAccess) => Promise<unknown> | unknown

Called when someone changes general access; resolve to apply it. Rejecting keeps the old value and shows the message.

–
readOnlyboolean

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

Sends the invitation; resolve when it's out and add the people to people. Reject to show the message and keep the chips.

–
onRoleChange(person: SharePerson, role: string) => Promise<unknown> | unknown–
onRemove(person: SharePerson) => Promise<unknown> | unknown

Removes someone's access.

–
onResend(person: SharePerson) => Promise<unknown> | unknown–
onCancelInvite(person: SharePerson) => Promise<unknown> | unknown

Cancels a pending invitation.

default `onRemove`

`onRemove`
onCopyLink() => void

Called after the link was copied.

–
classNamestring–

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

Motion tokens
MomentTokens
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.standard

140ms · 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. In readOnly, 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-describedby and aria-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-busy and says Loading 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.