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.
LTLý Thu Trang
280 leftCtrl+↵to post
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.
TMTrần Thị Mai
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
NTNguyễn Đức Thành
Nguyễn Đức Thành
Column two is the one I keep thinking about.
VHVũ Thanh Hà
Vũ Thanh Hà
Putting the actions on the board this afternoon.
LHLê Văn Hùng
Lê Văn Hùng
@hungle··Weekend 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
NTNguyễn Đức Thành
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
VHVũ Thanh Hà
Vũ Thanh Hà
@thanhha··Design Circle
Space
Type specimen from the archive print — the one we keep arguing about.
Liked by Trần Thị Mai and 1 other
PAPhạm Minh Anh
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.
ĐHĐỗ Quang Huy
Đỗ 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.
NTNguyễn Đức Thành
Nguyễn Đức Thành
@ducthanh · Product lead · joined
3 mutual
TMTrần Thị Mai
Trần Thị Mai
@maitran · Design · joined
2 mutual
LHLê Văn Hùng
Lê Văn Hùng
@hungle · Engineering · joined
+1
4 mutual
PAPhạm Minh Anh
Phạm Minh Anh
@minhanh · Engineering · joined
2 mutual
ĐHĐỗ Quang Huy
Đỗ Quang Huy
@quanghuy · Quality · joined
1 mutual
VHVũ Thanh Hà
Vũ Thanh Hà
@thanhha · Support · joined
+2
5 mutual
ĐKĐặng Tuấn Kiệt
Đặng Tuấn Kiệt
@tuankiet · Finance · joined
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.
DCDesign CirclePrivate
Design Circle
Critique, specimens and the long-running serif argument.
+1
4 members
Last post
WHWeekend HikersInvite only
Weekend Hikers
Trips within three hours of the city. Photos welcome.
3 members
Last post
RNRelease NotesOpen
Release Notes
What shipped, what broke, what is next.
+3
6 members
Last post
KTKitchen TableOpen
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.
Requests still need your approval — this only decides who may ask.
Capped by “Private account”: members outside your circle never see the list.
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.