@amauryuh/components
Ready-made React components for whole features, built on
@amauryuh/ui's layout and
styled with Tailwind CSS v4. Starting with a chat and its list of
conversations.
Install
npm install @amauryuh/components @amauryuh/ui i18next react-i18next
Peer dependencies: react and react-dom (18+), tailwindcss (v4),
@amauryuh/ui (0.6.1+), i18next (23+) and react-i18next (14+).
Tailwind skips node_modules, so import both packages' stylesheets after
Tailwind:
@import "tailwindcss";
@import "@amauryuh/ui/styles.css";
@import "@amauryuh/components/styles.css";
Chat
A whole messaging app in the style of Apple Messages: the list of chats
in the sidebar, and the open chat beside it. It looks like Messages on the
iPhone on small screens, and like Messages on the Mac from md up.
- List: the user's avatar and name, search by title, folder tabs and a row per chat with its newest message and an unread dot (see Chat.List).
- Title bar: a back button on mobile (it opens the list), the avatar and title, and a search that narrows the log down.
- Log: day lines (with the year for older messages), runs of messages per sender with a tail on the last bubble, quoted replies, tapbacks, view counts and "Delivered".
- Composer: a reply preview above the field. Replying with a single emoji reacts to the message instead.
- Types: direct chats, groups and channels (see Types).
- Reply: a reply button beside the bubble on hover or keyboard focus, or on touch screens a swipe to the left. Tapping a quote jumps to the original.
- Motion: new messages spring in and push the log up. The log stays at the newest message unless the user has scrolled up.
Chat takes one backend with every chat, and keeps which one is open
itself: a row of the list opens its chat, so the backend doesn't depend
on the app's address or routes. Rendered without children, as below, it
is ready to use: the whole Layout of @amauryuh/ui, with the list in
its sidebar and the open chat beside it. With children, it only provides
the state and you lay out the parts (see Parts).
import { Chat, type IChat } from "@amauryuh/components";
export default function App() {
const { conversations, post, react, mute, read } = useMyChats(); // your data
const backend: IChat = {
user: { name: "Alex Kim", avatar: "/alex.jpg" },
chats: conversations.map((c) => ({
id: c.id,
info: { type: c.type, title: c.title, muted: c.muted },
messages: c.messages,
folders: c.labels, // e.g. ["Work"]
unread: c.unread,
})),
post,
react,
mute,
read,
};
return <Chat backend={backend} />;
}
The backend: IChat
Plain data and callbacks, the same whichever chat is open. Build it on every render from your state, e.g. from a socket or a store. Every action gets the id of the chat it's for.
| Field | |
|---|---|
user | The person using the app, { name, avatar? }: shown above the list's search. avatar is a picture's URL; without it, their initials. |
chats | ChatConversation[], below, in the order the list shows them. |
post(chatId, { content, replyTo }) | Sends a message. Without it every chat is read-only: no composer, no reply. Channels are always read-only. |
react(chatId, messageId, emoji) | Toggles the user's reaction. With it, a reply that is one emoji reacts instead of being sent. |
mute(chatId, muted) | Turns a chat's notifications off or on. Without it, channels show no mute button. |
pin(chatId, pinned) | Pins a chat to the top of the list's All tab, or unpins it. With it, the title bar has a pin button left of search. |
read(chatId) | Called when an unread chat is on screen (beside the list, or on phones once the list is closed), and again as messages arrive while it is. Mark it read, so its dot goes away. |
setDraft(chatId, draft) | Keeps a chat's unsent reply ({ replyTo?: ChatMessage }), e.g. across visits; give it back as the chat's draft. Without it the drafts are kept in memory. |
ChatConversation:
| Field | |
|---|---|
id | Unique among the chats. |
info | About the chat: ChatInfo, below. |
messages | ChatMessage[], oldest first. The newest is the list's preview. |
folders | The folders it's in, by name, e.g. ["Work"]: the list's tabs. |
unread | Has messages the user hasn't seen: a dot in the list. |
draft | Its unsent reply, kept with setDraft. |
ChatInfo:
| Field | |
|---|---|
type | "direct", "group" or "channel". See Types. |
title | Shown in the list and the title bar, with an avatar of its initials. |
muted | Notifications are off: a bell in the list, and the mute button offers to turn them on. |
pinned | At the top of the list's All tab, with a pin on its row; the pin button offers to unpin it. |
ChatMessage:
| Field | |
|---|---|
id, content, from | from is the sender's display name. |
own | Written by the user: on the right, in the accent colour. |
time | ISO 8601. Drives the day and time lines. |
replyTo | The quoted message, { id?, from, content }. With id, tapping the quote jumps to the original. |
reactions | Emoji shown on the bubble's corner. |
views | A view count under the message, e.g. for posts in a channel. |
Types
The parts adapt to info.type:
| Type | |
|---|---|
direct | Two people: no names or avatars on messages. |
group | Names above each run and avatars next to other people's messages. |
channel | Posts to read, with names. The footer shows a mute button instead of the composer, and messages can't be replied to. |
New kinds of chat are added to the package as a type, so every app gets them.
Other props
children: your own layout of the parts. Without it,Chatrenders the wholeLayout.className: for the ready-madeLayout. With children, give it to your own.classNames: see Styling.
Translations
The components use your app's i18next instance through react-i18next.
The package never initializes i18next and takes no locale: your app sets
i18next up, as usual, with initReactI18next or an I18nextProvider,
and the components follow its language. Add the translations under the components
namespace (exported as namespace):
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
i18n.use(initReactI18next).init({
resources: {
es: {
translation: { /* your app's strings */ },
components: {
chat: {
search: "Buscar en el chat",
views_one: "{{count}} vista",
views_other: "{{count}} vistas",
// …
},
},
},
},
lng: "es",
fallbackLng: "es",
});
English is the fallback. Every key you leave out is shown in English,
even when your fallbackLng is another language, so you can translate
only some keys. English needs no resources; to change its wording, add
en.components. Its strings are exported as en, e.g. as a template.
Without an i18next instance, the components show English.
Dates and numbers are formatted in i18next's current language, so use
the codes you want them in, e.g. es-419. Changing the language
(i18n.changeLanguage) re-renders the chat, including its dates.
Every key, under components:
| Key | English | Where |
|---|---|---|
chat.back | Back | The title bar's back button (mobile), which opens the sidebar |
chat.search | Search in chat | The search button and the search field's placeholder |
chat.closeSearch | Close search | The button that closes the search |
chat.messages | Messages | The log's accessible name |
chat.empty | No messages yet. | The log when it has no messages |
chat.noResults | No messages match your search. | The log when the search finds nothing |
chat.today | Today | The day line for today's messages |
chat.delivered | Delivered | Under the user's newest message |
chat.views_one, chat.views_other | {{count}} view, {{count}} views | The accessible name of a message's view count. Plural keys, with i18next's suffixes for the language (e.g. _many). |
chat.message | Message | The composer's placeholder and accessible name |
chat.send | Send | The send button |
chat.reply | Reply | The reply button beside a bubble on hover |
chat.replyTo | Replying to {{name}} | The reply preview above the composer |
chat.cancelReply | Cancel reply | The button that drops the reply |
chat.mute | Mute | The mute button, while notifications are on |
chat.unmute | Unmute | The mute button, while notifications are off |
chat.pin | Pin | The pin button, while the chat isn't pinned |
chat.unpin | Unpin | The pin button, while the chat is pinned |
chat.scrollToBottom | Scroll to the last message | The button that scrolls to the newest message |
chatList.search | Search | The list's search field |
chatList.folders | Folders | The folder tabs' accessible name |
chatList.all | All | The first folder tab, with every chat |
chatList.chats | Chats | The list's accessible name |
chatList.empty | No chats match. | The list when the search or folder finds nothing |
chatList.unread | Unread | The unread dot's accessible name |
chatList.muted | Muted | The muted icon's accessible name |
chatList.pinned | Pinned | The pin icon's accessible name |
chatList.yesterday | Yesterday | A row's time for yesterday's messages |
To type-check them with your other keys, add the namespace to i18next's
CustomTypeOptions.
Parts
Every part is available as Chat.*. The parts take only a className:
what they show and do comes from the surrounding Chat. Rendered with
children, Chat is only that provider, and the children are the layout.
<Chat backend={backend} /> is the same as:
import { Layout } from "@amauryuh/ui";
<Chat backend={backend}>
<Layout>
<Layout.Sidebar className="md:w-88">
<Chat.List />
</Layout.Sidebar>
<Chat.Container>
<Chat.Header />
<Chat.Messages />
<Chat.Footer />
<Chat.ScrollToBottom />
</Chat.Container>
</Layout>
</Chat>
Chat.Container without children renders those four parts, so a layout
of your own often only changes the shell, e.g. to add a settings button
to the list's top bar:
<Chat backend={backend}>
<Layout className="bg-white">
<Layout.Sidebar className="md:w-88">
<Chat.List actions={<SettingsButton />} />
</Layout.Sidebar>
<Chat.Container />
</Layout>
</Chat>
| Part | |
|---|---|
Chat.List | The list of chats, for a Layout.Sidebar. See Chat.List. |
Chat.Container | The open chat, a Layout.Container. Its children are the parts below; without children, all of them. Each chat opens fresh: its search closed, at its newest message. |
Chat.Header | Title bar: back (mobile), the avatar and title, then pin (with pin) and search. Turns into Chat.Search while searching. |
Chat.Search | The search field and its close button. |
Chat.Messages | The log in the container's scrolling body. |
Chat.Footer | The bottom bar: Chat.Composer, or in channels Chat.MuteButton. Steps aside while searching. |
Chat.Composer | The field, send button and reply preview. Renders nothing where the user can't post. |
Chat.MuteButton | Turns notifications off or on, with mute from the backend. Renders nothing without it. |
Chat.ScrollToBottom | Takes the user back to the newest message once they scroll up. |
Parts of your own can read the state:
useChats(), anywhere insideChat:backend,open(the open chat),setOpen(chatId)to open another one, anddraftOf(chatId).useChatContext(), insideChat.Container: the openchat,canPost,post,react,muteandpinfor it,messages(filtered by the search),search,setSearch,draft,setDraft,replyandjumpTo. For example, a footer of your own could hide whilesearch !== null, and a reply button of your own callsreply(message).
Avatar
Avatar shows a picture (src) or else a name's initials in a circle,
as the chat's own avatars do. It needs no Chat around it:
import { Avatar } from "@amauryuh/components";
<Avatar className="size-9 text-sm" name={user.name} src={user.photo} />
Chat.List
The backend's chats, for a Layout.Sidebar. It has:
- A top bar: the backend's
user, with their avatar and name, on the left, and on the right your ownactions(e.g. a settings button). Without either, there's no top bar. - Search by title.
- Folder tabs: All, then every folder the chats are in, in the order they first appear. They're hidden when no chat has a folder.
- A row per chat: avatar, title, muted icon, the time of its newest message (the time today, then "Yesterday", the weekday, the day and the date) and a two-line preview. Unread chats get a dot.
- Pinned chats (
info.pinned) come first in All, in the backend's order, with a pin on the right of their row. Folders keep the backend's order.
A row opens its chat, and closes the sidebar on mobile. The open chat is
marked with aria-current="page" and turns blue where both panes are in
view. The list's slots start with list.
Styling
Each part has a slot with default classes (chatStyles). Classes are
merged with tailwind-merge, and later ones win:
- the slot's default,
classNamesgiven toChat, by slot,- the part's own
className.
Layout and behaviour classes (flex, positions, truncation, the hidden state) always apply on top, so styles can change everything but the layout.
<Chat
backend={backend}
classNames={{
bubble: "rounded-lg data-own:bg-emerald-600",
composer: "rounded-lg bg-slate-100 shadow-none",
}}
/>
<Chat.Composer className="bg-amber-100" />
A part's state is in data attributes, to style with Tailwind variants:
| Attribute | On |
|---|---|
data-own | column, bubble, quote, quoteText, reactions |
data-last | bubble (last of a run: a tighter corner and the tail, drawn with ::before) |
data-arrived | bubble (just came in; animates) |
data-highlighted | message (after jumping to it) |
data-gap="sender" | "reactions" | message (space above it) |
data-hidden | footer (while searching) |
Slots of Chat.List: listPanel, listHeader, listUser,
listUserAvatar, listUserName, listSearch, listSearchInput,
listFolders, listFolder (data-selected on the chosen one), list, listRow (aria-current="page" on the open one),
listRowText, listRowTitle, listRowMuted, listRowTime,
listRowAvatar, listRowChevron, listRowPinned, listRowPreview, listUnread, listEmpty and
avatar.
Slots of Chat: root, header, headerBar, headerActions, headerButton, headerAvatar,
title, search, searchInput, messages, empty, log, dayLine,
message, column, sender, senderAvatar, bubble, quote,
quoteAuthor, quoteText, reactions, replyButton, swipeHint,
views, delivered, footer, composer, replyPreview,
replyPreviewTitle, replyPreviewText, cancelReply, input,
sendButton, muteButton, scrollToBottom and avatar.
To restyle every part at once, redefine the theme variables in your own
@theme:
@theme {
--color-chat-accent: #0a72e8; /* own bubbles, buttons */
--color-chat-bubble: #e9e9eb; /* other people's bubbles */
--color-chat-label: #000;
--color-chat-secondary: #6d6d72;
--color-chat-separator: #d1d1d6;
--color-chat-fill: #7676801f; /* search fields, folder tabs */
--color-chat-panel-top: #f6f6f7; /* the list's panel (md+) */
--color-chat-panel-bottom: #f1f1f3;
--color-chat-avatar-top: #b5bacd;
--color-chat-avatar-bottom: #8a8fa6;
--text-chat-body: 1.0625rem;
--text-chat-subhead: 0.9375rem;
--text-chat-footnote: 0.8125rem;
}
The arrival animations are animate-chat-bubble-in and
animate-chat-bubble-sent. They only run when the user doesn't ask for
reduced motion.