Create chat interfaces in Typst with ease
Ourchat is a Typst package for building chat UI mockups. It helps you document software features, create presentations, or prototype chat interfaces with themes for popular platforms like WeChat, Discord, and QQ.
#let yau = wechat.default-user(name: [ไธๆๆก๏ผๅฏๅ
ง๏ผ]) #wechat.chat( theme: "dark", ..oc.with-side-user( left, yau, oc.time[5ๆ16ๆฅ ไธๅ10:23], oc.free-message[ ๅทฒ็ถๅฐไบ็กๆฅ็ๅฐๆญฅใ ], oc.time[6ๆ18ๆฅ ๅๆจ00:06], oc.free-message[ ๆๅฎฃๅธไปๅทฒ็ถไธๆฏๆ็ๅญธ็ไบ ], oc.time[14:00], oc.free-message[ ้็จฎๆ็ธพ๏ผไฝฟไบบๆฑ้ก๏ผๅฆๆญคๆ็ธพ๏ผๅฆไฝๆ็๏ผ ], ), oc.message(right, yau)[ ๆๆฒๆ่ชฌ้้็จฎ่ฉฑ๏ผ โโ็ผ่ชๆ็ๆๆฉ ], )- Out-of-the-box themes: WeChat, Discord, QQNT theme support
- Simple API: Easy-to-use, declarative interface
- Customizable styling: Colors, avatars, layouts, and typography
- Just do it: Write anything inside messagesโCode blocks, tables, mathematical equationsโฆ
First, import the package in your Typst document:
#import "@preview/ourchat:0.2.1" as oc #import oc.themes: *Then create your first chat:
#let alice = wechat.user(name: [Alice], avatar: circle(fill: blue, text(white)[A])) #let bob = wechat.user(name: [Bob], avatar: circle(fill: green, text(white)[B])) #wechat.chat( oc.time[Today 14:30], oc.message(left, alice)[ Hey! How's the new project going? ], oc.message(right, bob)[ Great! Just finished the API integration. The performance improvements are impressive! ๐ ], )#let user1 = wechat.user(name: [Alice], avatar: circle(fill: blue, text(white)[A])) #let user2 = wechat.user(name: [Bob], avatar: circle(fill: green, text(white)[B])) #wechat.chat( theme: "light", // or "dark" layout: ( bubble-radius: 8pt, ), width: 400pt, oc.time[Monday 9:00 AM], oc.message(left, user1)[Hello world!], oc.message(right, user2)[Hi there! ๐], )#set text(font: ("gg sans", "IBM Plex Sans SC")) #let developer = discord.user( name: [Dev], avatar: circle(fill: purple, text(white)[D]) ) #let admin = discord.user( name: [Admin], avatar: circle(fill: red, text(white)[A]) ) #discord.chat( oc.time[Today at 2:14 PM], oc.message(left, developer)[ ```python def optimize_query(): return cache_strategy.redis_cluster() ``` What do you think about this approach? @admin ], oc.message(right, admin)[ @developer Looks good! The Redis cluster should handle the load well. ], )#let student = qqnt.user( name: [Student], avatar: circle(fill: orange, text(white)[S]) ) #let expert = qqnt.user( name: [Expert], avatar: circle(fill: teal, text(white)[E]) ) #qqnt.chat( theme: ( inherit: "light", bubble-left: rgb("#F0F8FF"), bubble-right: rgb("#E8F5E8"), text-right: rgb("#111111"), ), oc.message(left, student)[ Can someone explain Rust ownership? ], oc.message(right, expert)[ Sure! Ownership prevents data races at compile time... ], )For multiple messages from the same user, use with-side-user to avoid repetition:
#set text(font: ("gg sans", "IBM Plex Sans SC")) #let admin = oc.user( name: [System Admin], avatar: circle(fill: red.darken(20%), text(white, weight: "bold")[โก]) ) #discord.chat( oc.time[Today at 3:45 PM], // Instead of repeating the user for each message: // oc.message(left, admin)[Server maintenance scheduled], // oc.message(left, admin)[Downtime: 30 minutes max], // oc.message(left, admin)[Please save your work], // Use with-side-user for cleaner code: ..oc.with-side-user( left, admin, oc.free-message[๐จ *URGENT: Server Maintenance Alert*], oc.free-message[Scheduled downtime: Tonight 11 PM - 11:30 PM], oc.free-message[All services will be temporarily unavailable], oc.free-message[Please save your work and plan accordingly], ), )Create distinctive user profiles:
#let ceo = oc.user( name: [Sarah Chen], badge: qqnt.badge(text-color: purple, bg-color: purple.transparentize(80%))[#text(stroke: 0.05em + purple)[CEO]], avatar: rect( fill: blue.darken(20%), radius: 4pt, inset: 6pt, text(white, weight: "bold")[SC] ) ) #qqnt.chat( oc.message(left, ceo)[ Hi team! Ready for the quarterly review? ], )Include tables, code blocks, and visual elements:
#let analyst = wechat.user( name: [Data Analyst], avatar: circle(fill: green.darken(10%), text(white)[๐]) ) #wechat.chat( oc.message(left, analyst)[ Here's our performance analysis: #table( columns: (auto, auto, auto), [*Metric*], [*Before*], [*After*], [Response Time], [250ms], [120ms], [Throughput], [1000 RPS], [2500 RPS], ) The optimization yielded 58% improvement! ๐ ] )Modify existing themes or create your own:
#let custom_theme = ( inherit: "light", background: rgb("#F5F5F5"), bubble-left: rgb("#E3F2FD"), bubble-right: rgb("#C8E6C9"), text-primary: rgb("#212121"), text-secondary: rgb("#757575"), ) #wechat.chat(theme: custom_theme, ...)Fine-tune spacing and dimensions:
#wechat.chat( layout: ( content-width: 350pt, message-spacing: 0.8em, avatar-size: 32pt, bubble-padding: 12pt, ), ... )Explore our comprehensive example collection: https://quadnucyard.github.io/ourchat-typ
The source codes for these example are located at ./examples.
Ourchat follows a unified component architecture where oc provides the core building blocks:
oc.message(),oc.user(),oc.time()- Universal components that work across all themes- Built-in themes (
wechat,discord,qqnt) import all common components but may override them for platform-specific features- For example,
qqnt.user()extends the base user component withbadgesupport for role badges
- For example,
- Uses
chatas the rendering function of messages, which is defined in individual themes. Styling is decided here.
// Universal approach - works with any theme #let user = oc.user(name: [Alice]) // Theme-specific approach - leverages extended features #let qqnt_user = qqnt.user( name: [Alice], badge: qqnt.badge()[Admin] // QQNT specific feature )Built-in themes provide a solid foundation but donโt cover every possible customization. Youโre encouraged to:
- Extend existing themes for minor modifications using
themeandlayoutparameters. - Create entirely new themes for different platforms or unique designs with basic blocks. Refer to the source code of built-in themes as implementation guides
Here only lists exported functions and variables. Please refer to the documentation comments of each function for details
oc.user(name, avatar, badge): Create universal user profilesoc.message(side, user, body, time, merge): Add chat messages (leftorright)oc.time(body): Insert timestamp dividersoc.with-side-user(side, user, ..messages): Convenience for multiple messages from same useroc.free-message(body, time, merge): Create message without specific user or sideoc.plain(side, user, body): Create plain item without padding
Note: These are just helper functions for data wrapping. You can directly create data structures if you like.
WeChat layout and color schemes (light, dark)
wechat.chat(theme, layout, width, validate, ..messages): WeChat-style interfacewechat.default-user: Pre-configured user with WeChat avatar
QQNT layout and color schemes (light, dark)
qqnt.chat(theme, layout, width, validate, ..messages): QQNT-style interfaceqqnt.user(usesoc.userwith badge support): QQNT user with role supportqqnt.badge(body, text-color, bg-color): Create role badges
Discord layout and color schemes
discord.newbie-user: Pre-configured user with newbie badgediscord.mention(body): Create Discord-style mention elementdiscord.chat(theme, layout, width, validate, auto-mention, ..messages): Discord-style interface
validate-theme(theme, reference, field-type): Validate theme dictionary fieldsvalidate-layout(layout, reference): Validate layout dictionary fieldsresolve-theme(themes, theme, default, validate): Resolve theme with inheritance supportresolve-layout(layout, default-layout, validate): Merge and validate layout settingsstretch-cover(item): Scale content to cover its containerauto-mention-rule(auto-mention, styler): Create show rule for automatic mention styling
We welcome contributions! Please check our GitHub repository for:
- Bug reports and feature requests
- Code contributions and improvements
- Documentation updates
- New theme proposals and existing theme improvements
MIT License - see LICENSE file for details.