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.0" 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 withbadge
support for role badges
- For example,
- Uses
chat
as 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
theme
andlayout
parameters. - 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 (left
orright
)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.user
with 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.