Patterns

Private Social Network

An invite-only social app, assembled from the package: a composer that names its audience, a filterable feed, a searchable member directory, private spaces, an activity inbox, and the privacy controls that keep a circle small and trusted.

A feed is lighter on composites than a table-shaped screen, and that is correct — there is no DataTable anywhere on this page. What the package does carry here is the identity layer: LetterAvatar and AvatarGroup for people, RelativeTime on every timestamp, FilterChips for the feed and inbox filters, StatusPill for who can see a post, and EmptyState for every view a filter can empty. Each demo below is live — the filters filter, the follow and save buttons hold state, and the empty states are reachable.

Post composer

The audience selector is the whole screen. It is a Select backed by a StatusConfig table, mirrored into a StatusPill and into a sentence below the box, so the answer to “who can see this?” is in text before anyone presses Post. Type to watch the counter, the disabled state and the audience line respond; ⌘↵ posts, and KeyboardHint renders that as Ctrl off macOS.

Lý Thu Trang
280 left

Circle onlyVisible to the 24 members you have approved. Not indexed, not resharable outside the circle.

Feed

FilterChips across the top with live counts; LetterAvatar, RelativeTime and a StatusPill audience chip on every post; AvatarGroup for who liked it. Like, comment and save all hold real state — save a post and the Saved chip counts it, which is how you reach the EmptyState and come back out of it.

Trần Thị Mai

@maitran

Circle

Retro wall from the offsite, photographed before someone wiped the board. Three themes, eleven actions, zero arguments.

Liked by Nguyễn Đức Thành and 2 others

Nguyễn Đức Thành

Column two is the one I keep thinking about.

Vũ Thanh Hà

Putting the actions on the board this afternoon.

Lê Văn Hùng

@hungleWeekend Hikers

Space

Anyone up for Ba Vì on Saturday? Leaving at six, back before dark. Keeping this to the space — I would rather the group stayed small.

Liked by Đỗ Quang Huy

Nguyễn Đức Thành

@ducthanh

Circle

Shipped the new invite flow. Invites expire after seven days now, which should keep the member list honest.

+2
Liked by Trần Thị Mai and 4 others

Vũ Thanh Hà

@thanhhaDesign Circle

Space

Type specimen from the archive print — the one we keep arguing about.

Liked by Trần Thị Mai and 1 other

Phạm Minh Anh

@minhanh

Circle

What is everyone using to keep photo albums out of the public index? Asking before I upload the retreat set.

Đỗ Quang Huy

@quanghuy

Public

We are opening five seats in the network next month. This one is public, so pass it on if you know someone worth inviting.

Liked by Nguyễn Đức Thành and 1 other

Member directory

SearchFilterBar over an ItemGroup, with matchesSearch doing the matching — it strips diacritics on both sides, so duc finds Đức. Mutual connections are an AvatarGroup rather than a number, and the follow button is a real toggle that the relationship filter reads back.

Nguyễn Đức Thành
Trần Thị Mai
Lê Văn Hùng
Phạm Minh Anh
Đỗ Quang Huy
Vũ Thanh Hà
Đặng Tuấn Kiệt

Private spaces

Topic spaces scope a conversation to a subset of the network. The square LetterAvatar is the cue that a space is a thing and not a person — the same shape rule the whole ecosystem uses for workspaces and apps — and its colour is derived from the name, so a space looks the same in every view. Privacy is a StatusPill from a status table, not a hand-picked badge colour.

Private

Design Circle

Critique, specimens and the long-running serif argument.

+1
4 members

Last post

Invite only

Weekend Hikers

Trips within three hours of the city. Photos welcome.

3 members

Last post

Open

Release Notes

What shipped, what broke, what is next.

+3
6 members

Last post

Open

Kitchen Table

Recipes, lunch plans, and the coffee order spreadsheet.

2 members

Last post

Activity inbox

Rows are Item inside an ItemGroup, the same list machinery as the directory, with asChild making each row the button that marks it read. Each pairs a LetterAvatar with the icon for the event kind, taken from a static map — never a class name built at runtime, which Tailwind cannot see. Unread is a word plus a dot, not a tinted row; and Mark all read empties the Unread filter into an EmptyState.

Privacy controls

FormSection with a primary accent stripe, Switch rows that carry their own label and description, and two FormFields on the 12-column grid. The line underneath restates the effective settings in plain language, because a column of toggles does not tell anyone what their account currently looks like.

Privacy

Who can see you, and who can reach you. Defaults are the strict end of each choice.

Only members you approve can see your posts, your circle and your spaces.

Your profile stops appearing in the member directory. People who already follow you keep their link.

Members of your circle can reshare a post to their own circle. Never outside the network.

Other members can see when you were last active. Turning this off also hides read receipts in spaces.

Right now: Only approved members see your posts. Your profile appears in member search. Reshares stay inside the network. Your activity status and read receipts are hidden.

Best practices

Do

  • State the audience in words before a member publishes, and repeat it on the post.
  • Default new accounts to private; make wider visibility an explicit opt-in.
  • Drive audience and space privacy from one StatusConfig table so the chip means the same thing everywhere.
  • Seed every avatar from the display name — one person, one colour, in every view.
  • Give each filter an empty state with a way back out of it.
  • Let members scope conversations to spaces instead of one global feed.
  • Pair every privacy toggle with a plain-language description of its effect.
  • Confirm irreversible actions — leaving a space, deleting a post — with ConfirmDialog.

Don't

  • Do not make posts public by default in an invite-only network.
  • Do not express the audience with a padlock icon alone.
  • Do not rely on a tinted row to mark unread, or on colour alone for anything.
  • Do not expose member profiles to search without an opt-in.
  • Do not leave AvatarGroup, SearchFilterBar or FilterChips on their Vietnamese default strings in an English app.
  • Do not build tone classes at runtime — text-{kind}-ink is purged from the bundle.
  • Do not bury unfollow or leave-space behind several taps.
  • Do not put more than one primary action on a card.

Accessibility

  • LetterAvatar already carries the member's name in an sr-only span on both the image and the initials branch. In a row that already prints the name — a post header, a directory row, a notification — the avatar is decoration, so give it aria-hidden; leave it announced only where nothing else names the person, as in the composer. The one place it must never be hidden is inside AvatarGroup, where that span is the accessible name of the avatar's own button.
  • Every AvatarGroup needs its own label: the default is "Thành viên", and each avatar inside it is a focusable button so keyboard users reach the tooltip.
  • Reaction and save buttons are toggles: give them aria-pressed and an sr-only verb, since the icon and count alone do not say whether the action is on.
  • The audience must be conveyed in text — the padlock is a reminder, not the message.
  • Unread needs a text or aria cue in addition to the dot; this page prints the word New.
  • RelativeTime renders a <time> with a real dateTime and an absolute timestamp in title; pass locale, or an English UI reads “3 phút trước”.
  • Switch renders its own <label htmlFor> when given label — take it rather than wrapping your own.
  • A composer textarea and a search field both need a label; SearchFilterBar uses its placeholder as the aria-label, so write it as a sentence.
  • Announce publish confirmations and follow changes through role="status".
  • FilterChips uses aria-pressed, not role="tab" — it toggles a condition rather than swapping a panel.