name: charm-glamour description: "Render markdown to styled ANSI terminal output in Go with glamour v2. Use when rendering markdown programmatically in Go, glamour, terminal markdown rendering, or styled markdown output. NOT for viewing markdown files in terminal (use glow)."
glamour - Terminal Markdown Rendering
charm.land/glamour/v2 renders markdown to styled ANSI output. Built on goldmark, supports GFM (tables, task lists, strikethrough), syntax highlighting via Chroma, emoji, and fully customizable stylesheets.
Quick Start
go get charm.land/glamour/v2@latest
One-liner
import "charm.land/glamour/v2"
out, err := glamour.Render("# Hello\n\nSome **bold** text.", "dark")
fmt.Print(out)
With renderer (reusable)
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(80),
)
if err != nil {
log.Fatal(err)
}
out, err := r.Render(markdown)
fmt.Print(out)
Core API
Package-level functions
| Function | Description |
|---|---|
Render(in, stylePath string) (string, error) |
One-shot render with a style name or file path |
RenderBytes(in []byte, stylePath string) ([]byte, error) |
Same but bytes in/out |
RenderWithEnvironmentConfig(in string) (string, error) |
Uses GLAMOUR_STYLE env var, defaults to "dark" |
TermRenderer
Created via NewTermRenderer(options ...TermRendererOption). Reusable for multiple renders.
Methods:
| Method | Description |
|---|---|
Render(in string) (string, error) |
Render markdown string |
RenderBytes(in []byte) ([]byte, error) |
Render markdown bytes |
Write(b []byte) (int, error) |
Implements io.Writer, buffer markdown input |
Close() error |
Flush buffered input, call before Read |
Read(b []byte) (int, error) |
Implements io.Reader, read rendered output |
io.ReadWriter pattern (streaming):
r, _ := glamour.NewTermRenderer(glamour.WithWordWrap(80))
r.Write([]byte("# Streamed\n\nContent here."))
r.Close()
rendered, _ := io.ReadAll(r)
fmt.Print(string(rendered))
Options
| Option | Description |
|---|---|
WithStandardStyle(name string) |
Use a built-in style by name |
WithStylePath(path string) |
Style name OR path to JSON file |
WithStyles(cfg ansi.StyleConfig) |
Programmatic style struct |
WithStylesFromJSONBytes(b []byte) |
Parse style from JSON bytes |
WithStylesFromJSONFile(path string) |
Load style from JSON file |
WithEnvironmentConfig() |
Use GLAMOUR_STYLE env var |
WithWordWrap(width int) |
Word wrap width (default: 80) |
WithTableWrap(wrap bool) |
Wrap table content (default: true). False truncates with ellipsis |
WithInlineTableLinks(inline bool) |
Render links inline in tables instead of footer list |
WithPreservedNewLines() |
Keep newlines instead of reflowing |
WithEmoji() |
Enable :emoji_code: rendering |
WithBaseURL(url string) |
Resolve relative URLs against this base |
WithChromaFormatter(fmt string) |
Set Chroma formatter for code blocks |
WithOptions(opts ...TermRendererOption) |
Combine multiple options |
Built-in styles
| Constant | String | Use case |
|---|---|---|
styles.DarkStyle |
"dark" |
Dark terminal backgrounds (default) |
styles.LightStyle |
"light" |
Light terminal backgrounds |
styles.DraculaStyle |
"dracula" |
Dracula color scheme |
styles.TokyoNightStyle |
"tokyo-night" |
Tokyo Night color scheme |
styles.PinkStyle |
"pink" |
Pink accent theme |
styles.AsciiStyle |
"ascii" |
ASCII-only, no unicode box chars |
styles.NoTTYStyle |
"notty" |
No ANSI codes at all, plain text |
Each has a corresponding StyleConfig variable: styles.DarkStyleConfig, styles.LightStyleConfig, etc.
Common Patterns
Custom style (programmatic)
Start from a built-in config and modify fields. Style fields use pointers for optional values.
import (
"charm.land/glamour/v2"
"charm.land/glamour/v2/ansi"
"charm.land/glamour/v2/styles"
)
func boolPtr(b bool) *bool { return &b }
func strPtr(s string) *string { return &s }
func uintPtr(u uint) *uint { return &u }
func customRenderer() (*glamour.TermRenderer, error) {
style := styles.DarkStyleConfig
// Custom H1: green text, no background
style.H1 = ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: strPtr("34"),
Bold: boolPtr(true),
Prefix: "# ",
},
}
// Wider margins
style.Document.Margin = uintPtr(4)
// Custom code block theme
style.CodeBlock.Theme = "monokai"
return glamour.NewTermRenderer(
glamour.WithStyles(style),
glamour.WithWordWrap(100),
)
}
Custom style (JSON file)
{
"document": {
"color": "252",
"margin": 2,
"block_prefix": "\n",
"block_suffix": "\n"
},
"heading": {
"color": "39",
"bold": true,
"block_suffix": "\n"
},
"h1": {
"color": "228",
"background_color": "63",
"bold": true,
"prefix": " ",
"suffix": " "
},
"h2": {
"prefix": "## "
},
"code_block": {
"theme": "dracula",
"margin": 2
},
"link": {
"color": "123",
"underline": true
},
"strong": {
"bold": true
},
"emph": {
"italic": true
}
}
r, err := glamour.NewTermRenderer(
glamour.WithStylesFromJSONFile("./my-style.json"),
glamour.WithWordWrap(80),
)
StyleConfig structure reference
StyleConfig
Document, BlockQuote, Paragraph -> StyleBlock
List -> StyleList (StyleBlock + LevelIndent)
Heading, H1-H6 -> StyleBlock
Text, Emph, Strong, Strikethrough -> StylePrimitive
HorizontalRule -> StylePrimitive (use Format for custom rule)
Item, Enumeration -> StylePrimitive (BlockPrefix for bullet char)
Task -> StyleTask (Ticked/Unticked strings)
Link, LinkText -> StylePrimitive
Image, ImageText -> StylePrimitive
Code -> StyleBlock (inline code)
CodeBlock -> StyleCodeBlock (Theme + Chroma)
Table -> StyleTable (separators)
DefinitionList/Term/Description -> StyleBlock/StylePrimitive
HTMLBlock, HTMLSpan -> StyleBlock
StyleBlock
Indent *uint, IndentToken *string, Margin *uint
+ StylePrimitive (all fields below)
StylePrimitive
Color, BackgroundColor *string // ANSI color number or hex "#RRGGBB"
Bold, Italic, Underline *bool
CrossedOut, Faint *bool
Inverse, Conceal, Blink *bool
Upper, Lower, Title *bool // text transform
Prefix, Suffix string // per-line prefix/suffix
BlockPrefix, BlockSuffix string // before/after entire block
Format string // Go template, e.g. link format
Color downsampling with lipgloss (v2)
Glamour v2 is "pure" - same input always gives same output. It does NOT auto-detect terminal color capabilities. Use lipgloss to downsample colors for the actual terminal.
import (
"charm.land/glamour/v2"
"charm.land/lipgloss/v2"
)
r, _ := glamour.NewTermRenderer(glamour.WithWordWrap(80))
out, _ := r.Render(markdown)
// lipgloss detects terminal capabilities and downsamples
lipgloss.Print(out)
Alternative with colorprofile for explicit control:
import "github.com/charmbracelet/colorprofile"
w := colorprofile.NewWriter(os.Stdout, os.Environ())
fmt.Fprintf(w, "%s", out)
Detect terminal background for style selection
import "charm.land/lipgloss/v2"
style := "dark"
if !lipgloss.HasDarkBackground() {
style = "light"
}
r, _ := glamour.NewTermRenderer(glamour.WithStandardStyle(style))
Environment-based style
export GLAMOUR_STYLE=dracula
# or a file path:
export GLAMOUR_STYLE=/path/to/custom.json
// Picks up GLAMOUR_STYLE, falls back to "dark"
out, err := glamour.RenderWithEnvironmentConfig(markdown)
// Or with a renderer:
r, err := glamour.NewTermRenderer(glamour.WithEnvironmentConfig())
Integration
Bubbletea viewport (scrollable markdown)
import (
"charm.land/glamour/v2"
tea "charm.land/bubbletea/v2"
"charm.land/bubbles/v2/viewport"
)
type model struct {
viewport viewport.Model
content string
}
func initialModel(markdown string) model {
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(78), // viewport width minus padding
)
rendered, _ := r.Render(markdown)
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
vp.SetContent(rendered)
return model{viewport: vp, content: rendered}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
func (m model) View() string {
return m.viewport.View()
}
Key point: set WithWordWrap to viewport width minus any horizontal padding/margin. Re-render when terminal resizes.
Re-render on resize
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport.SetWidth(msg.Width)
m.viewport.SetHeight(msg.Height)
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(msg.Width - 2),
)
rendered, _ := r.Render(m.rawMarkdown)
m.viewport.SetContent(rendered)
}
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
Lipgloss styled container around rendered markdown
import "charm.land/lipgloss/v2"
border := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Padding(1, 2)
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(76), // account for border + padding (2 border + 4 padding = 6)
)
rendered, _ := r.Render(markdown)
fmt.Println(border.Render(rendered))
Common Mistakes
Using WithAutoStyle() or WithColorProfile() - Removed in v2. Use WithStandardStyle("dark") and lipgloss.Print() for color handling.
Not accounting for margin/padding in word wrap - If you wrap a glamour-rendered block in a lipgloss container with padding/border, subtract that width from the word wrap value or text will overflow.
Creating a new renderer per render when unnecessary - TermRenderer is reusable. Create once, call Render() many times.
Using fmt.Print instead of lipgloss.Print - If colors look wrong on some terminals, you need color downsampling. Use lipgloss.Print(out) instead of fmt.Print(out).
Forgetting Close() when using Write/Read pattern - After writing markdown via r.Write(), you must call r.Close() before reading with r.Read() or io.ReadAll(r).
Import path still on v1 - v2 uses charm.land/glamour/v2, not github.com/charmbracelet/glamour.
Setting style fields directly instead of via pointer - Color, Bold, Italic, etc. are pointer types. Use helper functions like func boolPtr(b bool) *bool { return &b }.
Using WithStandardStyle with a file path - WithStandardStyle only accepts built-in style names. For file paths, use WithStylePath or WithStylesFromJSONFile.
Checklist
- Import
charm.land/glamour/v2(not the old github path) - Pick a style: built-in name, JSON file, or programmatic
StyleConfig - Set
WithWordWrapto match your output width minus borders/padding - Use
lipgloss.Print()for proper color downsampling on real terminals - For bubbletea: re-render on
WindowSizeMsgwith updated wrap width - Handle errors from
NewTermRendererandRender(malformed styles, etc.) - For env-based config: use
WithEnvironmentConfig()orRenderWithEnvironmentConfig() - Test with
"notty"style for CI/non-terminal environments