# Built-in Components Reference

## Overview

go-tui ships with three built-in components: `Input` (single-line text), `TextArea` (multi-line text), and `Modal` (overlay dialog). Input and TextArea handle text entry, cursor management, and focus. Modal handles backdrop rendering, focus trapping, and preemptive key blocking.

All three implement `Component`, `KeyListener`, and `AppBinder`. Use them standalone or embed them in a larger UI.

As GSX elements (`<input>`, `<textarea>`, `<modal>`), they mount against the component's receiver, so place them inside a struct method component rather than a pure `templ` function. The same applies to `<markdown>`.

## Input

A single-line text input with cursor management, horizontal scrolling, and placeholder support.

```go
import tui "github.com/grindlemire/go-tui"

inp := tui.NewInput(
    tui.WithInputWidth(30),
    tui.WithInputBorder(tui.BorderRounded),
    tui.WithInputPlaceholder("Type here..."),
    tui.WithInputOnSubmit(func(text string) {
        // handle submitted text
    }),
)
```

### NewInput

```go
func NewInput(opts ...InputOption) *Input
```

Creates a new `Input` with the given options. Default values:

| Setting     | Default      | Description                              |
|-------------|--------------|------------------------------------------|
| Width       | 20           | Characters visible before scrolling      |
| Border      | `BorderNone` | No border                                |
| TextStyle   | `Style{}`    | Default terminal style                   |
| Placeholder | `""`         | No placeholder text                      |
| PlaceholderStyle | `Style{}.Dim()` | Dim text for placeholder          |
| Cursor      | real cursor  | Framework places the terminal cursor; `WithInputVirtualCursor` draws `'▌'` instead |
| FocusColor  | `Cyan`       | Border color when focused                |

### Reactive Value Binding

Bind the Input to a `*State[string]` for two-way binding. The Input shares the state directly, so typing updates the state and changing the state updates the display:

```go
name := tui.NewState("")
inp := tui.NewInput(
    tui.WithInputValue(name),
    tui.WithInputBorder(tui.BorderRounded),
)
// Later: name.Set("Alice") updates the input display
// Typing in the input updates name.Get()
```

In `.gsx`:

```gsx
<input value={s.name} placeholder="Type your name..." border={tui.BorderRounded} />
```

### GSX Attributes

All `<input>` attributes and their types:

| Attribute | Type | Description |
|-----------|------|-------------|
| `value` | `*State[string]` | Two-way text binding |
| `placeholder` | `string` | Text shown when empty and unfocused |
| `placeholderStyle` | `tui.Style` | Placeholder styling (default: dim) |
| `width` | `int` | Width in characters (default 20) |
| `border` | `tui.BorderStyle` | Border style |
| `textStyle` | `tui.Style` | Text styling |
| `cursor` | `rune` | Cursor character (default '▌') |
| `focusColor` | `tui.Color` | Border color when focused (default Cyan) |
| `borderGradient` | `tui.Gradient` | Border gradient when unfocused |
| `focusGradient` | `tui.Gradient` | Border gradient when focused |
| `onSubmit` | `func(string)` | Called when Enter is pressed |
| `onChange` | `func(string)` | Called when text changes |
| `autoFocus` | `bool` | Focus this input on startup |

### Focus Border Styling

Control how the border looks when focused and unfocused:

```go
// Solid color when focused (default: Cyan)
tui.WithInputFocusColor(tui.Magenta)

// Gradient border when unfocused
tui.WithInputBorderGradient(tui.NewGradient(tui.Blue, tui.Cyan))

// Gradient border when focused (overrides focusColor)
tui.WithInputFocusGradient(tui.NewGradient(tui.Cyan, tui.Magenta))
```

In `.gsx`:

```gsx
<input
    value={s.query}
    border={tui.BorderRounded}
    focusColor={tui.Magenta}
    borderGradient={tui.NewGradient(tui.Blue, tui.Cyan)}
    focusGradient={tui.NewGradient(tui.Cyan, tui.Magenta)}
/>
```

### Keyboard Behavior

**Text input:**

| Key         | Action                            |
|-------------|-----------------------------------|
| Any rune    | Insert character at cursor        |
| Backspace   | Delete character before cursor    |
| Delete      | Delete character at cursor        |

**Navigation:**

| Key         | Action                            |
|-------------|-----------------------------------|
| Left        | Move cursor left                  |
| Right       | Move cursor right                 |
| Home        | Move cursor to start              |
| End         | Move cursor to end                |

**Submit:**

| Key         | Action                            |
|-------------|-----------------------------------|
| Enter       | Trigger `onSubmit` callback       |

### Programmatic Editing

Move the cursor and insert text from code, for example to seed a value or build an autocomplete.

```go
func (inp *Input) CursorPos() int
func (inp *Input) SetCursorPos(pos int)
func (inp *Input) InsertText(s string)
```

`CursorPos` returns the cursor position as a grapheme-cluster index, the count of whole glyphs before the cursor. That is the unit `SetCursorPos` accepts, so it is not a byte or rune offset and will not slice the text correctly if used that way. `SetCursorPos` clamps to `[0, ClusterCount(text)]` and lands on a cluster boundary. `InsertText` inserts at the cursor and advances past the inserted text, routing through the same path as typing so combining marks join the preceding glyph.

```go
inp.SetCursorPos(0)
inp.InsertText("> ") // prefix the line, cursor ends after the prefix
```

### InputOption Functions

| Function | Description |
|----------|-------------|
| `WithInputWidth(int)` | Width in characters (default 20) |
| `WithInputBorder(BorderStyle)` | Border style |
| `WithInputTextStyle(Style)` | Text style |
| `WithInputPlaceholder(string)` | Placeholder text |
| `WithInputPlaceholderStyle(Style)` | Placeholder style (default: dim) |
| `WithInputCursor(rune)` | Cursor character (default '▌') |
| `WithInputValue(*State[string])` | Reactive two-way text binding |
| `WithInputFocusColor(Color)` | Border color when focused (default Cyan) |
| `WithInputBorderGradient(Gradient)` | Border gradient when unfocused |
| `WithInputFocusGradient(Gradient)` | Border gradient when focused |
| `WithInputOnSubmit(func(string))` | Enter key callback |
| `WithInputOnChange(func(string))` | Text change callback |
| `WithInputVirtualCursor()` | Draw the `▌` glyph instead of using the real terminal cursor |
| `WithInputCursorRune(rune)` | Glyph used in virtual-cursor mode (default `▌`) |

By default the Input drives the real terminal cursor and draws no glyph. Pass `WithInputVirtualCursor()` to paint the block glyph instead, and `WithInputCursorRune` to change it. `WithInputCursor` still works as a deprecated alias for `WithInputCursorRune`.

## TextArea

A multi-line text input with word wrapping and a blinking cursor.

```go
import tui "github.com/grindlemire/go-tui"

ta := tui.NewTextArea(
    tui.WithTextAreaWidth(60),
    tui.WithTextAreaBorder(tui.BorderRounded),
    tui.WithTextAreaPlaceholder("Type something..."),
    tui.WithTextAreaOnSubmit(func(text string) {
        // handle submitted text
    }),
)
```

### NewTextArea

```go
func NewTextArea(opts ...TextAreaOption) *TextArea
```

Creates a new `TextArea` with the given options. Default values:

| Setting     | Default      | Description                              |
|-------------|--------------|------------------------------------------|
| Width       | 40           | Characters per line before wrapping      |
| MaxHeight   | 0 (no limit) | Maximum rows of text visible             |
| Border      | `BorderNone` | No border                                |
| TextStyle   | `Style{}`    | Default terminal style                   |
| Placeholder | `""`         | No placeholder text                      |
| PlaceholderStyle | `Style{}.Dim()` | Dim text for placeholder          |
| Cursor      | real cursor  | Framework places the terminal cursor; `WithTextAreaVirtualCursor` draws `'▌'` instead |
| FocusColor  | `Cyan`       | Border color when focused                |
| SubmitKey   | `KeyEnter`   | Enter submits, Ctrl+J inserts newline    |

### GSX Attributes

All `<textarea>` attributes and their types:

| Attribute | Type | Description |
|-----------|------|-------------|
| `value` | `*State[string]` | Two-way text binding |
| `placeholder` | `string` | Text shown when empty and unfocused |
| `placeholderStyle` | `tui.Style` | Placeholder styling (default: dim) |
| `width` | `int` | Width in characters (default 40) |
| `maxHeight` | `int` | Maximum visible rows (0 = unlimited) |
| `border` | `tui.BorderStyle` | Border style |
| `textStyle` | `tui.Style` | Text styling |
| `cursor` | `rune` | Cursor character (default '▌') |
| `focusColor` | `tui.Color` | Border color when focused (default Cyan) |
| `borderGradient` | `tui.Gradient` | Border gradient when unfocused |
| `focusGradient` | `tui.Gradient` | Border gradient when focused |
| `submitKey` | `tui.Key` | Key that triggers submit (default KeyEnter) |
| `onSubmit` | `func(string)` | Called when submit key is pressed |
| `autoFocus` | `bool` | Focus this text area on startup |

### State Access Methods

#### Text

```go
func (t *TextArea) Text() string
```

Returns the current text content.

#### SetText

```go
func (t *TextArea) SetText(s string)
```

Replaces the text and moves the cursor to the end.

```go
ta.SetText("Hello, world!")
fmt.Println(ta.Text()) // "Hello, world!"
```

#### Clear

```go
func (t *TextArea) Clear()
```

Removes all text and resets the cursor to position 0.

#### Height

```go
func (t *TextArea) Height() int
```

Returns the total rendered height in rows, including border rows if a border is set. The height depends on the current text content and word wrapping. If `maxHeight` is set, the returned value is capped to that limit (plus border rows).

#### Programmatic Editing

```go
func (t *TextArea) CursorPos() int
func (t *TextArea) SetCursorPos(pos int)
func (t *TextArea) InsertText(s string)
```

`CursorPos` returns the cursor position as a grapheme-cluster index (whole glyphs before the cursor), which is the unit `SetCursorPos` accepts. It is not a byte or rune offset. `SetCursorPos` clamps to `[0, ClusterCount(text)]` and lands on a cluster boundary. `InsertText` inserts at the cursor and advances past it, using the same path as typing so the bound text and cursor stay cluster-consistent.

```go
ta.InsertText("\n- ") // start a new bullet at the cursor
```

### Focus Methods

`TextArea` implements the `Focusable` interface. When focused, the cursor blinks and keystrokes are captured. When unfocused, placeholder text appears (if configured) and no keystrokes are processed.

#### IsFocusable

```go
func (t *TextArea) IsFocusable() bool
```

Always returns `true`.

#### Focus

```go
func (t *TextArea) Focus()
```

Activates the text area. The cursor becomes visible and starts blinking.

#### Blur

```go
func (t *TextArea) Blur()
```

Deactivates the text area. The cursor disappears and placeholder text shows if the input is empty.

### HandleEvent

```go
func (t *TextArea) HandleEvent(e Event) bool
```

Processes a keyboard event against the TextArea's key map. Returns `true` if the event was handled (and propagation should stop), `false` otherwise. This method is part of the `Focusable` interface and is called automatically by the focus manager when the TextArea has focus.

### BindApp

```go
func (t *TextArea) BindApp(app *App)
```

Binds the TextArea's internal reactive states to the given `App`, so that state changes trigger re-renders. Called automatically when the TextArea is used as a root component or mounted as a sub-component.

### Keyboard Behavior

`TextArea` returns a `KeyMap` with built-in bindings. All bindings use stop propagation so keystrokes don't bubble up to parent components.

**Text input:**

| Key         | Action                            |
|-------------|-----------------------------------|
| Any rune    | Insert character at cursor        |
| Backspace   | Delete character before cursor    |
| Delete      | Delete character at cursor        |

**Navigation:**

| Key         | Action                            |
|-------------|-----------------------------------|
| Left        | Move cursor left                  |
| Right       | Move cursor right                 |
| Up          | Move cursor up one line           |
| Down        | Move cursor down one line         |
| Home        | Move cursor to start of line      |
| End         | Move cursor to end of line        |

**Submit and newline (default submit key = Enter):**

| Key         | Action                            |
|-------------|-----------------------------------|
| Enter       | Trigger `onSubmit` callback       |
| Ctrl+J      | Insert newline character          |

When `submitKey` is set to something other than `KeyEnter`, the behavior flips: Enter inserts a newline and the configured key triggers submit.

### Cursor

By default the TextArea drives the real terminal cursor and draws no glyph, so the terminal handles the blink. Pass `WithTextAreaVirtualCursor()` to paint the `▌` glyph in the buffer instead, and `WithTextAreaCursorRune` to change it.

In virtual-cursor mode, `TextArea` implements `WatcherProvider` and returns a timer watcher that toggles the glyph every 500ms while focused. The glyph resets to visible on every keystroke so the user always sees where they are typing.

### TextAreaOption Functions

Options follow the functional options pattern. Each returns a `TextAreaOption` (which is `func(*TextArea)`).

#### WithTextAreaWidth

```go
func WithTextAreaWidth(cells int) TextAreaOption
```

Sets the width in characters. Text wraps at this boundary. Default: 40.

```go
ta := tui.NewTextArea(tui.WithTextAreaWidth(80))
```

#### WithTextAreaMaxHeight

```go
func WithTextAreaMaxHeight(rows int) TextAreaOption
```

Caps the visible height to `rows` lines of text. Set to 0 (the default) for unlimited height. This does not include border rows. If a border is set, the actual element height is `maxHeight + 2`.

```go
ta := tui.NewTextArea(tui.WithTextAreaMaxHeight(10))
```

#### WithTextAreaBorder

```go
func WithTextAreaBorder(b BorderStyle) TextAreaOption
```

Sets the border style around the text area. Default: `BorderNone`.

```go
ta := tui.NewTextArea(tui.WithTextAreaBorder(tui.BorderRounded))
```

#### WithTextAreaTextStyle

```go
func WithTextAreaTextStyle(s Style) TextAreaOption
```

Sets the style for the text content. Default: zero-value `Style{}` (terminal default).

```go
ta := tui.NewTextArea(
    tui.WithTextAreaTextStyle(tui.NewStyle().Foreground(tui.ANSIColor(tui.Cyan))),
)
```

#### WithTextAreaPlaceholder

```go
func WithTextAreaPlaceholder(text string) TextAreaOption
```

Sets placeholder text shown when the TextArea is empty and unfocused. Default: empty string (no placeholder).

```go
ta := tui.NewTextArea(tui.WithTextAreaPlaceholder("Enter your message..."))
```

#### WithTextAreaPlaceholderStyle

```go
func WithTextAreaPlaceholderStyle(s Style) TextAreaOption
```

Sets the style for placeholder text. Default: `Style{}.Dim()`.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaPlaceholderStyle(tui.NewStyle().Dim().Italic()),
)
```

#### WithTextAreaCursorRune

```go
func WithTextAreaCursorRune(r rune) TextAreaOption
```

Sets the glyph drawn in virtual-cursor mode. Default: `'▌'` (left half block). It takes effect only with `WithTextAreaVirtualCursor`, since the default real-cursor mode draws no glyph. `WithTextAreaCursor` remains as a deprecated alias.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaVirtualCursor(),
    tui.WithTextAreaCursorRune('█'),
)
```

#### WithTextAreaSubmitKey

```go
func WithTextAreaSubmitKey(k Key) TextAreaOption
```

Sets which key triggers the `onSubmit` callback. Default: `KeyEnter`.

When the submit key is `KeyEnter`, pressing Enter triggers submit and Ctrl+J inserts a newline. For any other submit key, Enter inserts a newline and the configured key triggers submit.

```go
// Ctrl+S submits, Enter inserts newlines (good for multi-line editing)
ta := tui.NewTextArea(tui.WithTextAreaSubmitKey(tui.KeyCtrlS))
```

#### WithTextAreaOnSubmit

```go
func WithTextAreaOnSubmit(fn func(string)) TextAreaOption
```

Sets the callback invoked when the submit key is pressed. The callback receives the current text content.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaOnSubmit(func(text string) {
        fmt.Println("Submitted:", text)
    }),
)
```

#### WithTextAreaOnChange

```go
func WithTextAreaOnChange(fn func(string)) TextAreaOption
```

Sets a callback invoked whenever the text content changes. The callback receives the full text after each change. It fires on user edits (typing, backspace, delete) and on programmatic changes through `SetText`, `Clear`, and `InsertText`. Use it to mirror the text elsewhere or to validate as the user types.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaOnChange(func(text string) {
        charCount.Set(len(text))
    }),
)
```

This is a Go option. Unlike `<input onChange>`, the `<textarea>` tag does not yet expose `onChange` as an attribute, so set it through `WithTextAreaOnChange` when constructing the component.

#### WithTextAreaValue

```go
func WithTextAreaValue(state *State[string]) TextAreaOption
```

Binds the TextArea to a `*State[string]` for two-way binding. The TextArea shares the state directly, so parent components can read or change the text at any time.

```go
note := tui.NewState("")
ta := tui.NewTextArea(tui.WithTextAreaValue(note))
// note.Get() reflects whatever the user types
// note.Set("preset text") updates the textarea display
```

In `.gsx`:

```gsx
<textarea value={s.note} placeholder="Write a note..." border={tui.BorderRounded} />
```

#### WithTextAreaFocusColor

```go
func WithTextAreaFocusColor(c Color) TextAreaOption
```

Sets the border color when focused. Default: `Cyan`. Only visible when a border is set.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaBorder(tui.BorderRounded),
    tui.WithTextAreaFocusColor(tui.Magenta),
)
```

##### WithTextAreaBorderGradient

```go
func WithTextAreaBorderGradient(g Gradient) TextAreaOption
```

Sets a gradient for the border color when unfocused. Only visible when a border is set.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaBorder(tui.BorderRounded),
    tui.WithTextAreaBorderGradient(tui.NewGradient(tui.Blue, tui.Cyan)),
)
```

#### WithTextAreaFocusGradient

```go
func WithTextAreaFocusGradient(g Gradient) TextAreaOption
```

Sets a gradient for the border color when focused. Takes priority over `focusColor` when set.

```go
ta := tui.NewTextArea(
    tui.WithTextAreaBorder(tui.BorderRounded),
    tui.WithTextAreaFocusGradient(tui.NewGradient(tui.Cyan, tui.Magenta)),
)
```

#### WithTextAreaVirtualCursor

```go
func WithTextAreaVirtualCursor() TextAreaOption
```

Switches to the drawn `▌` glyph instead of the real terminal cursor. The glyph is customizable with `WithTextAreaCursorRune`.

### Example

A note-taking input using `TextArea` with a rounded border and Ctrl+S to submit:

```go
package main

import (
    "fmt"
    "os"

    tui "github.com/grindlemire/go-tui"
)

func main() {
    ta := tui.NewTextArea(
        tui.WithTextAreaWidth(60),
        tui.WithTextAreaMaxHeight(10),
        tui.WithTextAreaBorder(tui.BorderRounded),
        tui.WithTextAreaPlaceholder("Write a note..."),
        tui.WithTextAreaSubmitKey(tui.KeyCtrlS),
        tui.WithTextAreaOnSubmit(func(text string) {
            fmt.Printf("Saved: %s\n", text)
        }),
    )

    app, err := tui.NewApp(
        tui.WithRootComponent(ta),
    )
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
    defer app.Close()

    if err := app.Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}
```

For an inline chat pattern with `PrintAbove`, see the [Inline Mode Guide](../guides/12-inline-mode.md).

## Modal

Modal renders a full-screen overlay with a backdrop effect, focus trapping, and preemptive key handling.

### Constructor

```go
func NewModal(opts ...ModalOption) *Modal
```

### ModalOption Functions

| Function | Description |
|----------|-------------|
| `WithModalOpen(state *State[bool])` | Bind visibility to a boolean state (required) |
| `WithModalBackdrop(b string)` | Backdrop style: `"dim"` (default), `"blank"`, or `"none"` |
| `WithModalCloseOnEscape(v bool)` | Escape closes the modal (default `true`) |
| `WithModalCloseOnBackdropClick(v bool)` | Backdrop click closes the modal (default `true`) |
| `WithModalTrapFocus(v bool)` | Restrict Tab navigation to modal children and block unhandled keys from parent handlers (default `true`) |
| `WithModalKeyMap(km KeyMap)` | Custom key bindings for the modal; fire after built-in handlers but before the catch-all. Use `OnPreemptStop`; non-preemptive bindings (`On`, `OnStop`) are inert when `trapFocus` is true |
| `WithModalElementOptions(opts ...Option)` | Pass layout options to the overlay container (used by generated code for `class` attributes) |

### GSX Attributes

All `<modal>` attributes and their types:

| Attribute | Type | Description |
|-----------|------|-------------|
| `open` | `*State[bool]` | Controls visibility (required) |
| `backdrop` | `string` | `"dim"` (default), `"blank"`, or `"none"` |
| `closeOnEscape` | `bool` | Escape closes the modal (default true) |
| `closeOnBackdropClick` | `bool` | Backdrop click closes the modal (default true) |
| `trapFocus` | `bool` | Tab/Shift+Tab restricted to modal children; also blocks unhandled keys from parents (default true) |
| `keyMap` | `expression` | Custom `KeyMap` bindings for the modal |
| `class` | `string` | Tailwind classes for positioning (e.g. `"justify-center items-center"`) |

### Behavior

When open, the modal:

- Applies the backdrop effect (dim, blank, or none) to the buffer before rendering the overlay
- Handles Enter by calling `Activate()` on the focused element
- Closes on Escape (if `closeOnEscape` is true)
- Closes on backdrop click (if `closeOnBackdropClick` is true)
- Walks clicked elements up to find `onActivate` callbacks for mouse support

When `trapFocus` is true (the default):

- Tab/Shift+Tab only cycle through focusable children inside the modal
- A catch-all binding blocks unhandled keys from parent handlers

When `trapFocus` is false, Tab and unhandled keys propagate to parent components normally.

Custom key bindings provided via `WithModalKeyMap` (or the `keyMap` GSX attribute) fire after the built-in Escape/Tab/Enter handlers but before the catch-all. This lets you add hotkeys to a modal while still blocking everything else:

```go
tui.WithModalKeyMap(tui.KeyMap{
    tui.OnPreemptStop(tui.Rune('n'), func(ke tui.KeyEvent) { startNewGame() }),
    tui.OnPreemptStop(tui.Rune('q'), func(ke tui.KeyEvent) { quit() }),
})
```

When closed, it returns a hidden placeholder element with no key bindings.

### Interfaces Implemented

| Interface | Purpose |
|-----------|---------|
| `Component` | `Render(app *App) *Element` returns the overlay element |
| `KeyListener` | `KeyMap()` returns Escape, Tab, Enter, custom, and catch-all bindings (catch-all only when `trapFocus` is true) |
| `MouseListener` | `HandleMouse()` handles backdrop click and onActivate delegation |
| `AppBinder` | `BindApp()` wires the open state to the app |

### GSX Usage

```gsx
<modal open={s.showDialog} class="justify-center items-center" backdrop="dim">
    <div class="border-rounded p-2 flex-col gap-1 w-40">
        <span class="font-bold">Title</span>
        <button class="px-2 border-rounded focusable" onActivate={s.onConfirm}>OK</button>
    </div>
</modal>
```

The `class` attribute on `<modal>` controls how the dialog is positioned within the full-screen overlay. Use `justify-center items-center` for a centered dialog or `justify-end items-stretch` for a bottom sheet.

### Inline Mode

Modals are not supported in inline mode (`WithInlineHeight`). The overlay system requires a full-screen buffer for backdrop effects, centering, and mouse hit testing. Modal overlays registered while in inline mode are silently ignored.

To show a modal from an inline app, switch to the alternate screen first:

```go
app.EnterAlternateScreen()
s.showDialog.Set(true)
// ... user interacts with modal ...
// on close:
app.ExitAlternateScreen()
```

See the [Inline Mode Guide](../guides/15-inline-mode) for the full pattern.

## Markdown

Markdown renders a markdown string into the widget tree. It is a pure content renderer with no scroll state or key handling, so it is wrapped in a scrollable container for long documents. The parsed block tree is cached and re-parsed only when the resolved source string changes.

### Constructor

```go
func NewMarkdown(opts ...MarkdownOption) *Markdown
```

### MarkdownOption Functions

| Function | Description |
|----------|-------------|
| `WithMarkdownSource(s string)` | Static markdown content. Ignored when a state source is set |
| `WithMarkdownState(s *State[string])` | Reactive source; takes precedence over the static source and re-renders on change |
| `WithMarkdownWidth(w int)` | Fixed render width in characters. `0` (the default) fills the width the parent assigns |
| `WithMarkdownTheme(t MarkdownTheme)` | Override the default styling theme |

### GSX Attributes

All `<markdown>` attributes and their types:

| Attribute | Type | Description |
|-----------|------|-------------|
| `source` | `string` | Static markdown content (string expression) |
| `state` | `*State[string]` | Reactive source; re-renders on change. Takes precedence over `source` |
| `width` | `int` | Fixed render width in characters (`0` fills the parent width) |
| `theme` | `MarkdownTheme` | Override the default styling |

The tag is self-closing. Content comes from `source` or `state` rather than from children, because the generator cannot tell a literal markdown string from a Go expression that returns one.

### Supported Markdown

| Construct | Notes |
|-----------|-------|
| Headings | ATX (`#` through `######`) and single-line setext (`===`, `---`) |
| Emphasis | Bold, italic, and bold-italic, with `*`/`_` and `**`/`__` markers |
| Inline code | Backtick spans |
| Links | Rendered as OSC 8 hyperlinks on capable terminals |
| Code blocks | Fenced blocks, syntax-highlighted for Go, JSON, Bash, and JS/TS |
| Tables | Pipe tables drawn as a full grid with inline formatting kept in cells |
| Lists | Ordered, unordered (`-`, `*`, `+`), and nested |
| Blockquotes | Including nested quotes and quotes that contain a list |

An emphasis delimiter with no closer stays literal, so `see **docs` and `3 * 4` render as written.

### Width and Wrapping

With `width` at `0` (the default), the component fills the width its parent assigns. Paragraphs and headings wrap to that width, while list and blockquote content renders on one line and clips on overflow. Set an explicit `width` to wrap list and blockquote content as well.

### Theming

`MarkdownTheme` is a flat struct of `Style` fields plus a few extras. `DefaultMarkdownTheme()` returns a glow-inspired theme. Override individual fields and pass the result to `WithMarkdownTheme` or the `theme` attribute.

| Field | Type | Controls |
|-------|------|----------|
| `Heading` | `[6]Style` | Per-level heading styles, indexed `0` (h1) through `5` (h6) |
| `Paragraph` | `Style` | Body text |
| `Bold`, `Italic`, `CodeSpan`, `Link` | `Style` | Inline runs, layered over the surrounding text |
| `CodeBlockText` | `Style` | Fenced code block text |
| `CodeBlockBg` | `Color` | Code block fill (default: no fill) |
| `CodeBlockBorder` | `BorderStyle` | Box border around code blocks |
| `CodeHighlighter` | `CodeHighlighter` | Colorizes fenced code; `nil` disables highlighting |
| `TableHeader` | `Style` | Table header cells |
| `TableBorder` | `BorderStyle` | Table grid border |
| `BlockquoteBar` | `rune` | Left bar glyph |
| `BlockquoteBarStyle` | `Style` | Left bar style |
| `BlockquoteText` | `Style` | Quoted text |
| `BulletMarker` | `string` | Unordered-list marker, e.g. `"• "` |

### Syntax Highlighting

Fenced code blocks run through the theme's `CodeHighlighter`. The default is a built-in zero-dependency lexer covering Go, JSON, Bash, and JS/TS. An unrecognized language renders in `CodeBlockText`.

```go
type CodeHighlighter interface {
    Highlight(lang, code string) [][]TextSpan
}
```

| Function | Description |
|----------|-------------|
| `NewHighlighter(p Palette)` | Built-in highlighter using palette `p` |
| `DefaultPalette()` | Default One Dark color scheme as a `Palette` (a `map[TokenKind]Color`) |

Set `theme.CodeHighlighter = nil` to render code uncolored, pass `NewHighlighter` a custom `Palette` to recolor the built-in lexer, or implement `CodeHighlighter` to plug in another engine such as chroma.

### Interfaces Implemented

| Interface | Purpose |
|-----------|---------|
| `Component` | `Render(app *App) *Element` returns the rendered block tree |
| `AppBinder` | `BindApp()` wires the reactive state source to the app (a no-op for a static source) |
| `PropsUpdater` | `UpdateProps()` receives fresh source, width, and theme when re-rendered from cache |

### GSX Usage

```gsx
<div class="overflow-y-scroll scrollbar-hidden grow" ref={s.content} scrollOffset={0, s.scrollY.Get()}>
    <markdown source={s.doc} width={80} />
</div>
```

Markdown holds no scroll position, so the surrounding container provides the ref, the `scrollOffset` binding, and the key handling. See the [Markdown Guide](../guides/24-markdown) for the full scrollable viewer.

## Cross-References

- [Component Interfaces Reference](interfaces.md) — `Component`, `KeyListener`, `WatcherProvider`, `Focusable`, `AppBinder`
- [Events Reference](events.md) — `KeyEvent`, `Key` constants, `KeyMap`
- [State Reference](state.md) — `State[T]` used internally by TextArea
- [Styling Reference](styling.md) — `Style` and `BorderStyle` for visual configuration
- [Focus Guide](../guides/13-focus.md) — Focus management and Tab navigation
- [Inline Mode Guide](../guides/12-inline-mode.md) — Using TextArea in inline mode
