Textual: attributed text for SwiftUI, rebuilt from MarkdownUI
Render and customize rich attributed text in SwiftUI
At a glance
- What is it?
- Textual is a MIT licensed Swift package that renders attributed text in SwiftUI, and it is not a Markdown view with a fancy name: it keeps SwiftUI's own text pipeline, exposes two views for inline and block content, and lets you plug in a custom parser. The judgement is that it is a well scoped engine that a team can theme, and that the styling API is where the real learning curve sits.
- Who is it for?
- Textual is a good fit for an iOS or macOS team that needs chat messages, release notes or documentation rendered as real text rather than as a web view, and that wants the result to inherit system text sizing, selection and accessibility behaviour instead of reimplementing them.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 130 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A rendering engine that happens to read Markdown
The framing in the README is deliberate, and it is the first thing to understand about this package. Textual is described as the spiritual successor to MarkdownUI, reimagined from the ground up to address the lessons learned from community feedback, and the difference is one of perspective. MarkdownUI focused on Markdown rendering, whereas Textual is designed as a SwiftUI text rendering engine that happens to support Markdown. Every design decision follows from that, and the practical consequence is that the output is a SwiftUI view tree, not a web view or a picture of text. The rendering flow is described in four stages: markup is transformed into attributed content, attachments are resolved asynchronously, styling is applied through environment values, and SwiftUI's own layout system positions everything. That last point is what makes the text scale with the user's text size and accessibility settings instead of being pinned to a fixed font size.
Two views, inline content and block documents
The package offers two specialised views, and the choice between them is the first decision an integrator makes. InlineText is for inline formatted text, and the README is clear that it is a drop-in replacement for SwiftUI's Text with attachment support and styling, so you can swap it into an existing view hierarchy and change what happens. StructuredText is for block based documents, with headings, paragraphs, lists, code blocks and tables, each of which can be customised independently. A chat transcript is a mixture of the two, which is where the split becomes awkward, because a message that contains a paragraph is not inline content while a document that is mostly prose still needs the block treatment. The minimal usage is short, and the markdown parameter takes a string literal:
InlineText(
markdown: "Shipped the new feature :rocket: and it's working :sparkles:",
syntaxExtensions: [.emoji(emoji)]
)The image inside a paragraph is an inline attachment that adapts to the width of its container, and the same mechanism carries emoji, math and animated images.
Any format that can produce PresentationIntent attributes
Markdown support is not baked into the rendering engine, it is one implementation of a protocol. The built-in parser sits on top of Foundation's AttributedString markdown parser, and the README says you can plug in any format that can produce strings with PresentationIntent attributes by conforming your parser to the MarkupParser protocol. That is a narrow and specific contract, and it is the reason the package can support non-Markdown input without forking the renderer. It also explains where the limits are, because anything that cannot be expressed as a presentation intent loses fidelity. The built-in parser additionally supports syntax extensions, which is how custom emoji work: you define a set of emoji with shortcodes, and the substitution happens after parsing.
let emoji: Set<Emoji> = [
Emoji(
shortcode: "rocket",
url: URL(string: "https://example.com/rocket.png")!
),
Emoji(
shortcode: "sparkles",
url: URL(string: "https://example.com/sparkles.gif")!
),
]Math expressions work the same way, as another syntax extension, and they are rendered as attachments:
StructuredText(
markdown: "The area is $A = \\pi r^2$.",
syntaxExtensions: [.math]
)Attachments are what make the engine feel different
The feature list is long, and most of it reduces to one idea, that inline attachments flow with the text. Images, custom emoji, math expressions as inline or block attachments, and animated images in GIF, APNG and WebP formats are all handled as attachments rather than as separate views placed next to the text. That is why a picture inside a paragraph reflows correctly, why the demo image in the README example adapts to the container width, and why the selection behaviour described later behaves sensibly around them. The other half of the feature list is about syntax highlighting with customisable themes, and about styling for headings, code blocks, tables, links and lists. Reading the two together, the package is betting that the hard part of rich text in SwiftUI is not drawing glyphs, it is keeping inline and block content in one selectable, accessible, dynamically sized run of text.
Style presets first, element overrides second
Styling is layered, and the layering is the part of the API a team has to learn. At the top there are two complete presets, .default and .gitHub, applied with a single modifier:
StructuredText(
markdown: """
## The Problem
...
"""
)
.textual.structuredTextStyle(.gitHub)That one modifier configures the whole rendering stack, covering inline styles such as code, emphasis, strong and links, block styles such as headings, paragraphs, blockquotes, code blocks and tables, and the list markers. Below that you can override individual block types or write fully custom styles, and the inline side has its own system applied with the inlineStyle modifier:
InlineText(
markdown: "Use `git status` to check _uncommitted changes_"
)
.font(.custom("Avenir Next", size: 18))
.textual.inlineStyle(
InlineStyle()
.code(
.monospaced,
.fontScale(0.85),
.backgroundColor(.purple),
.foregroundColor(.white)
)
.emphasis(.italic, .underlineStyle(.single))
)The inline builder takes a font style, a scale factor and explicit colours, and it composes with standard SwiftUI modifiers, so .font and .foregroundStyle still work alongside it. Measurements are font relative, which is what lets the layout scale with the user's text size rather than with a hard coded assumption.
Selection is a per view decision, not a global one
Text selection has its own modifier, textual.textSelection(_:), which controls whether users can select text inside InlineText or StructuredText. It is worth reading the note on scrollable regions carefully, because it describes behaviour that surprises people. Scrollable areas such as code blocks handle their own selection contexts, and when you select text in a scrollable area any document level selection clears automatically, and the same happens in reverse. In other words, selection is scoped rather than global, which is the behaviour you want in a long document with a code block in the middle, and which also means that a naive drag across a code block will not select the surrounding paragraph. Native selection with proper copy and paste support is listed as a key feature, so the point of the modifier is control, not the addition of a capability that SwiftUI's Text already had.
One package, six platforms in the test matrix
The repository layout shows a package that takes platform breadth seriously. The Makefile drives a default target that runs the test suite across macOS, Mac Catalyst, iOS, tvOS, watchOS and visionOS, with xcodebuild invocations per destination and simulator UDID variables resolved through a helper, and it also carries a format target built on swift format. There is a Package.swift, a resolved dependency file, an .spi.yml for Swift Package Index, an xcworkspace, a Scripts directory, a Tests directory, an Examples directory containing TextualDemo, and a LICENSE-3rdparty.csv that tracks the licences of everything vendored in. On maintenance, the facts are simple. The licence field on the repository says MIT, the last push was on 2026-06-15, the repository is not archived, and the tags are 0.5.0 and 0.4.0 on 2026-06-15 with 0.3.1 on 2026-01-25. Two versions landing on the same day is a sign of a burst of work rather than a slow drip, and a version number below 1.0 means the API, in particular the styling types, can still move under you.
Editorial conclusion
Textual is a good fit for an iOS or macOS team that needs chat messages, release notes or documentation rendered as real text rather than as a web view, and that wants the result to inherit system text sizing, selection and accessibility behaviour instead of reimplementing them. It is a poor fit if your content is HTML, if you need a full Markdown feature set beyond what the AttributedString parser covers, or if you cannot absorb a styling API that is still settling: the package is pre 1.0 at version 0.5.0. Before adopting it, render one screen with the .gitHub preset and one with your own InlineStyle, then read the structuredTextStyle and inlineStyle modifiers, because that is where the behaviour you will argue about later is defined.
Frequently asked questions
How do I render Markdown text in a SwiftUI view with Textual?
Use InlineText with its markdown parameter for inline content, or StructuredText for block documents with headings, lists, code blocks and tables. The README describes InlineText as a drop-in replacement for SwiftUI's Text with attachment support, and you can customise it with standard modifiers such as .font() and .foregroundStyle().
Can Textual parse a markup format other than Markdown?
Yes. The built-in Markdown support sits on Foundation's AttributedString markdown parser, and you can plug in any format that produces strings with PresentationIntent attributes by conforming your parser to the MarkupParser protocol.
Does Textual support syntax highlighting and ready made themes?
Syntax highlighting with customisable themes is listed among the key features, and two complete style presets ship with the package, .default and .gitHub, applied through the textual.structuredTextStyle(_:) modifier, which configures inline styles, block styles and list markers in one call.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/gonzalezreal-textual)Was this useful?