CoUI TextBlock#
Quick Reference#
- Widget:
TextBlock - Variant: 없음 (variant 파라미터 없음)
- Size: 없음 (
CoreComponentSize없음) -
Style slot:
textBlockStyle: CoreTextBlockStyle(...)— Flutter-only 블록쿼트/코드 박스 크롬 override (Web에는 대응 파라미터 없음) - Canonical snippet:
TextBlock(text: 'Plain body text') // p + bodyMedium
한마디로#
텍스트블록(TextBlock)은 문서 속 글자를 "역할에 맞는 옷"을 입혀 보여주는 도구입니다. 워드에서 "제목 1", "본문", "인용문" 스타일을 고르는 것을 떠올리면 됩니다. 이 문서는 앱(모바일)과 웹사이트에서 제목·문단·인용문·코드 조각 같은 글을 똑같은 서체 규칙으로 보여주는 방법을 정리한 설명서입니다. 역할(제목인지 문단인지)과 스타일 프리셋만 골라주면 알아서 통일된 모양으로 나옵니다.
무엇을·언제#
- 무엇을 해주나요: 큰 제목(H1~H6), 본문 문단, 인용문(왼쪽에 세로줄이 있는 들여쓴 글), 코드 조각(회색 상자 안 고정폭 글씨), 흐릿한 보조 설명(캡션), 부제목 같은 "문서형 글"을 만들어 줍니다.
- 어떤 선택지가 있나요: 글의 역할 14가지(문단, 제목 1~6단계, 인용문, 코드, 라벨, 강조 등)와 모양 프리셋 15가지(제목 크기, 굵게, 기울임, 흐리게, 말줄임 등)를 조합합니다.
- 왜 좋은가요: 웹에서는 검색엔진과 화면낭독기(시각장애인 보조기기)가 이해하는 올바른 문서 구조(h1, p, blockquote 태그)로 만들어지고, 앱에서는 같은 크기·색 규칙의 글자로 만들어져 양쪽이 똑같아 보입니다.
- 언제 쓰이나요: 도움말 페이지, 블로그형 콘텐츠, 설정 화면의 설명 문구, 약관처럼 "읽는 글"이 필요한 화면 어디서나 이 설명서가 사용됩니다.
- 버튼·배지와 다른 점: 누르는 부품이 아니라 읽는 글 전용입니다. 크기(sm/md/lg)나 색상 변형(variant) 선택지가 따로 없고, 역할과 프리셋이 모양을 결정합니다.
핵심 용어#
| 용어 | 쉬운 설명 |
|---|---|
| TextBlock | 글에 역할별 스타일 옷을 입혀 보여주는 글자 상자 |
| element | 글의 역할 선택지 (문단 p, 제목 h1~h6, 인용문, 코드 등 14가지) |
| p / paragraph | 일반 본문 문단 |
| h1 ~ h6 | 제목 크기 1단계(가장 큼)부터 6단계(가장 작음)까지 |
| blockquote | 인용문 — 왼쪽에 세로 강조선이 붙는 들여쓴 글 |
| pre / inline code | 코드 조각 — 회색 상자 안에 고정폭 글씨로 보여주는 스타일 |
| preset | 미리 만들어 둔 모양 세트 (제목용, 굵게, 흐리게, 말줄임 등) |
| muted | 덜 중요한 보조 설명용 흐린 회색 글씨 |
| lead | 제목 아래에 오는 큰 부제목(리드 문장) |
| truncate | 칸을 넘치는 글을 "…"로 잘라 한 줄로 보여주기 |
| children | 글자 상자 안에 넣는 여러 조각 (예: 문장 중간에 코드 조각 끼우기) |
| textBlockStyle | 인용문 세로선 두께·코드 상자 색처럼 상자 장식만 살짝 바꾸는 세부 설정 |
| Flutter | 모바일/앱 화면을 만드는 기술 |
| Web | 웹 브라우저용 화면을 만드는 기술 |
| Legacy / Deprecated | 옛날 방식이라 새 코드에서는 쓰지 말아야 하는, 곧 사라질 항목 |
Overview#
TextBlock is the unified cross-platform prose component for headings, paragraphs, blockquotes, inline/preformatted code, labels, and text utilities. One component takes a semantic
element enum plus a list of typography presets; Web additionally binds each element to the matching semantic HTML tag so the document outline and screen readers stay correct.
-
Element:
CoreTextBlockElement— 14 values (p,span,h1–h6,blockquote,pre,label,strong,em,small); defaultCoreTextBlockElement.defaultElement(=p). -
Presets: platform-local
TextBlockStylevalues built through static helpers onTextBlock(TextBlock.paragraph,TextBlock.heading1(context), …). Same helper names on both platforms. -
Chrome slot:
textBlockStyle: CoreTextBlockStyle(...)— Flutter-only constructor param that overrides theblockquote/prebox chrome (border, indent, fill, radius, padding). On Web the sameCoreTextBlockStyle.default*constants are baked into thequote/inlineCodepreset class strings. -
No
variant, nosize: TextBlock has noCoreComponentSizeand no variant enum — the element + presets fully describe the appearance. There is also noCoreTextBlockThemeand nocoreComponentThemeslot for it.
Note the two similarly named classes: TextBlockStyle (platform-local preset value — wraps a
TextStyle fragment on Flutter, a Tailwind class string on Web) vs CoreTextBlockStyle
(shared coui_core chrome style for the blockquote/pre boxes).
Canonical API#
element — CoreTextBlockElement (14 values, full set)#
| Element | Web tag | Flutter base typography | Element-level chrome (Flutter) |
|---|---|---|---|
p (default) |
<p> |
bodyMedium |
— |
span |
<span> |
bodyMedium |
— |
h1 | <h1> | displaySmall | — |
h2 |
<h2> |
headlineLarge |
— |
h3 |
<h3> |
headlineSmall |
— |
h4 | <h4> | titleLarge | — |
h5 | <h5> | titleMedium | — |
h6 | <h6> | titleSmall | — |
blockquote |
<blockquote> |
bodyMedium |
left accent border (
onSurfaceVariant
,
stroke2
) +
space24
left indent
|
pre |
<pre> |
bodyMedium + mono |
surfaceContainer
box,
radius4
,
space4
×
space2
padding
|
label |
<label> |
labelLarge |
— |
strong |
<strong> |
bodyMedium + bold |
— |
em |
<em> |
bodyMedium + italic |
— |
small |
<small> |
bodySmall |
— |
On Flutter the element selects the base typography token (there is no document outline); on Web it selects the semantic HTML tag. The blockquote/pre chrome is drawn
by the element on Flutter, but carried by the preset classes on Web — so always pair the element with its matching preset (blockquote
+ TextBlock.quote, inline code span + TextBlock.inlineCode(context)) to stay visually identical across platforms.
presets — List<TextBlockStyle>? (full set)#
Presets compose left-to-right; later presets win on conflicting fields (Flutter TextStyle.merge
semantics; Web joins the class strings). Always construct them through the static helpers — never instantiate
TextBlockStyle directly.
Context-independent (usable in const lists):
| Preset | Flutter effect | Web classes |
|---|---|---|
TextBlock.paragraph |
line height 28/16 (prose leading) |
leading-7 |
TextBlock.bold |
fontWeight: w700 |
font-{bold} token class |
TextBlock.italic | italic | italic |
TextBlock.quote |
italic (border comes from the element) |
border-l-{stroke2}
+
border-{onSurfaceVariant}
+
pl-{space24}
+
italic
|
TextBlock.truncate |
TextOverflow.ellipsis |
truncate |
TextBlock.wrap |
no-op (naming parity — Flutter already wraps/breaks) | break-words |
Context-dependent — call with BuildContext:
| Preset | Flutter effect | Web classes |
|---|---|---|
TextBlock.heading1(context) |
displaySmall |
scroll-m-{space80} text-{displaySmall} tracking-{tight} |
TextBlock.heading2(context) |
headlineLarge |
same pattern with headlineLarge |
TextBlock.heading3(context) |
headlineSmall |
same pattern with headlineSmall |
TextBlock.heading4(context) |
titleLarge |
same pattern with titleLarge |
TextBlock.lg(context) |
titleMedium |
text-{titleMedium} |
TextBlock.sm(context) |
labelLarge + height: CoreLineHeight.h100 |
text-{labelLarge} leading-none |
TextBlock.muted(context) |
bodySmall + onSurfaceVariant colour |
text-{bodySmall} text-{onSurfaceVariant} |
TextBlock.lead(context) |
titleLarge + onSurfaceVariant colour |
text-{titleLarge} text-{onSurfaceVariant} |
TextBlock.inlineCode(context) |
bodySmall
+
mono
+
w600
+
surfaceContainer
background
|
rounded-{radius4} bg-{surfaceContainer} px/py padding, mono, semibold |
muted / lead foregrounds and the quote / inlineCode
chrome all route through CoreTextBlockStyle.default* token constants, so Flutter and Web resolve from the same source of truth (onSurfaceVariant,
surfaceContainer).
textBlockStyle — CoreTextBlockStyle? (Flutter-only chrome slot)#
Overrides the blockquote/pre box chrome per instance. Fields (all nullable): blockQuoteBorderColor
(CoreColor?), blockQuoteBorderWidth (double?), blockQuotePadding
(CoreEdgeInsets?), inlineCodeBackgroundColor (CoreColor?), inlineCodeRadius
(double?), inlineCodePadding (CoreEdgeInsets?). It exposes merge
/ copyWith and the shared defaults as statics: defaultBlockQuoteBorderColor
(onSurfaceVariant token), defaultBlockQuoteBorderWidth (CoreStrokeWidth.stroke2),
defaultBlockQuotePadding (left-only space24), defaultInlineCodeBackgroundColor
(surfaceContainer token), defaultInlineCodeRadius (CoreRadius.radius4),
defaultInlineCodePadding (space4 × space2), defaultMutedForegroundColor
(onSurfaceVariant token).
Resolve chain (Flutter resolveTextBlock): design-system default (CoreTextBlockStyle.default*) →
widget.textBlockStyle. There is no theme layer — unlike Badge/Button, TextBlock has no
Core*Theme.
Shared contract parameters (CoreTextBlockContract<W>)#
| Parameter | Type (Flutter) | Type (Web) | Default | Notes |
|---|---|---|---|---|
text |
String? |
String? |
null |
Plain text content; ignored when children is set |
element |
CoreTextBlockElement |
CoreTextBlockElement |
defaultElement (= p) |
Semantic role / HTML tag |
presets |
List<TextBlockStyle>? |
List<TextBlockStyle>? |
null |
Typography presets (platform-local value class) |
children |
List<Widget>? |
List<Component>? |
null |
Rich-text content; when non-null, text is ignored |
textBlockStyle |
CoreTextBlockStyle? |
— (not a param) | null |
Flutter-only blockquote/pre chrome override |
Web-only: id, classes, css (Jaspr Styles), attributes,
eventHandlers, plus copyWith(...).
Flutter (coui_flutter)#
Import#
import 'package:coui_flutter/coui_flutter.dart';
The barrel is the only supported import — never import src/ paths. There is no Material name clash for
TextBlock.
Basic usage#
TextBlock(text: 'Plain body text') // p + bodyMedium
const TextBlock(text: 'Comfort prose', presets: [TextBlock.paragraph])
TextBlock(text: 'A muted caption', presets: [TextBlock.muted(context)])
Enum-shorthand (inferred type) is idiomatic in real code:
TextBlock(text: '섹션 제목', element: .h2, presets: [TextBlock.heading2(context)])
Headings and lead subtitle#
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextBlock(
text: 'Design tokens in practice',
element: CoreTextBlockElement.h1,
presets: [TextBlock.heading1(context)],
),
TextBlock(
text: 'A practical guide to CoUI typography.',
presets: [TextBlock.lead(context)],
),
],
)
The heading preset carries the typography; the element keeps naming/parity with Web (on Flutter it also sets the same base style, so element-only headings work too).
Blockquote#
element: .blockquote draws the left accent border + space24 indent automatically; add the
quote preset for the italic text to match Web:
TextBlock(
text: 'Simplicity is the ultimate sophistication.',
element: CoreTextBlockElement.blockquote,
presets: const [TextBlock.quote],
)
Inline code and pre box#
Inline (span-level) code — the preset paints the surface tint via TextStyle.backgroundColor:
TextBlock(
text: 'coui_flutter',
element: CoreTextBlockElement.span,
presets: [TextBlock.inlineCode(context)],
)
Preformatted block — element: .pre renders the mono base style inside a surfaceContainer
box with radius4 corners:
const TextBlock(
text: 'dart run build_runner build',
element: CoreTextBlockElement.pre,
)
Rich text (children)#
When children is non-null, text is ignored and the resolved style flows down via
DefaultTextStyle.merge. A single child renders directly; multiple children stack vertically
in a Column (mainAxisSize: min, crossAxisAlignment: start) — they are not inline spans:
TextBlock(
presets: const [TextBlock.paragraph],
children: const [
Text('First stacked line.'),
Text('Second stacked line.'),
],
)
For truly inline composition on Flutter, keep it to one child and compose inside it.
Per-instance chrome (textBlockStyle)#
Chrome micro-tweaks for the blockquote/pre boxes only — typography stays with presets:
TextBlock(
text: 'Callout quote',
element: CoreTextBlockElement.blockquote,
presets: const [TextBlock.quote],
textBlockStyle: const CoreTextBlockStyle(
blockQuoteBorderColor: CoreColor.token(CoreColors.primary),
blockQuoteBorderWidth: CoreStrokeWidth.stroke4,
blockQuotePadding: CoreEdgeInsets.only(left: CoreSpace.space16),
),
)
TextBlock(
text: 'npm run dev',
element: CoreTextBlockElement.pre,
textBlockStyle: const CoreTextBlockStyle(
inlineCodeRadius: CoreRadius.radius8,
inlineCodePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space8,
vertical: CoreSpace.space4,
),
),
)
Flutter-specific behavior notes#
-
TextBlockis aStatelessWidgetimplementingCoreTextBlockContract<Widget>; it renders a plainText(orDefaultTextStyle.mergefor children) — no gesture handling, no focus, no animation. -
Border width, paddings, and radius from the chrome are multiplied by the theme
scalingfactor; colours resolve throughscheme.resolve(token). -
TextBlock.wrapis a documented no-op on Flutter (soft-wrap is already the default); it exists for naming parity with Web'sbreak-words. -
resolveTextBlock(context, element: …, presets: …, textBlockStyle: …)returningResolvedTextBlockis public if you need the paint-ready values (textStyle, blockquote border colour/width/padding, inline-code background/radius/padding).
Web (coui_web / Jaspr)#
Import#
import 'package:coui_web/coui_web.dart';
The barrel re-exports Jaspr (Component, Styles, dom helpers like div/span/text) and
coui_core, so no separate Jaspr import is needed.
Basic usage#
Same params; children are Jaspr Components. The component renders the semantic HTML tag for the element (<p>,
<h1>, <blockquote>, …):
TextBlock(text: 'Plain body text') // <p>
TextBlock(
text: 'Design tokens in practice',
element: CoreTextBlockElement.h1,
presets: [TextBlock.heading1(context)],
)
TextBlock(text: 'A muted caption', presets: [TextBlock.muted(context)])
Blockquote / inline code on Web#
On Web the box chrome lives in the preset class string (token-bound Tailwind classes), not in the element — pass both:
TextBlock(
text: 'Simplicity is the ultimate sophistication.',
element: CoreTextBlockElement.blockquote,
presets: [TextBlock.quote],
)
TextBlock(
text: 'dart pub add coui_web',
element: CoreTextBlockElement.span,
presets: [TextBlock.inlineCode(context)],
)
Using element: .blockquote without the quote preset yields a semantically correct
<blockquote> with no visual accent border.
Rich text (children — inline flow)#
Children flow inline inside the semantic element (normal HTML document flow), which makes mixed prose natural:
TextBlock(
element: CoreTextBlockElement.p,
presets: const [TextBlock.paragraph],
children: [
text('Install with '),
TextBlock(
text: 'dart pub add coui_web',
element: CoreTextBlockElement.span,
presets: [TextBlock.inlineCode(context)],
),
text(' and import the barrel.'),
],
)
Web-only constructor extras#
The Web constructor also accepts id, classes, css (Jaspr Styles),
attributes, and eventHandlers. User classes are merged after the preset class string. The component also provides
copyWith(...):
TextBlock(
text: 'Installation',
element: CoreTextBlockElement.h2,
presets: [TextBlock.heading2(context)],
id: 'installation',
classes: 'mt-8',
)
There is no textBlockStyle param on Web — chrome values come from the CoreTextBlockStyle.default*
constants baked into the quote / inlineCode preset class strings; use classes
for per-instance additions.
Document pattern#
div(
[
TextBlock(
text: 'Getting started',
element: CoreTextBlockElement.h1,
presets: [TextBlock.heading1(context)],
),
TextBlock(
text: 'Everything you need to ship your first screen.',
presets: [TextBlock.lead(context)],
),
TextBlock(
text: 'CoUI ships a single barrel per platform.',
presets: const [TextBlock.paragraph],
),
TextBlock(
text: 'Read the docs before overriding tokens.',
element: CoreTextBlockElement.blockquote,
presets: [TextBlock.quote],
),
],
classes: 'flex flex-col gap-4',
)
Flutter ↔ Web Differences#
| Aspect | Flutter | Web |
|---|---|---|
| Component class | TextBlock (StatelessWidget, CoreTextBlockContract<Widget>) |
TextBlock (UiComponent, CoreTextBlockContract<Component>) |
children type |
List<Widget>? |
List<Component>? |
| Render |
Text
/
DefaultTextStyle.merge
(+
DecoratedBox
for blockquote/pre)
|
Semantic HTML tag (
<p>
,
<h1>
…
<h6>
,
<blockquote>
,
<pre>
, …)
|
TextBlockStyle payload |
TextStyle fragment, merged in order (later wins) |
Tailwind class string, joined in order |
| blockquote / pre chrome | Drawn automatically by the element switch |
Carried by the quote / inlineCode preset classes |
Multiple children layout |
Vertical Column (start-aligned) |
Normal inline / document flow |
textBlockStyle param |
Yes — CoreTextBlockStyle? per-instance chrome override |
No — use classes; defaults are baked into preset class strings |
| Extra ctor params | — |
id
,
classes
,
css
,
attributes
,
eventHandlers
; also
copyWith(...)
|
| Element semantics | Typography token selection only (no outline) | Real document outline + screen-reader semantics |
Pitfalls#
-
TextBlock has no theme layer at all — unlike Badge/Button, there is no
CoreTextBlockThemeand nocoreComponentThemeslot; only the design-system default →textBlockStyleresolve chain applies. -
textBlockStyle(the chrome override) is Flutter-only — Web has no such constructor param; on Web the same defaults are baked into thequote/inlineCodepreset class strings, so useclassesinstead. -
Multiple
childrenbehave differently per platform: on Flutter they stack vertically in aColumn(not inline spans), while on Web the same children flow inline in normal document order. -
On Web,
element: .blockquoteused without thequotepreset produces a semantically correct<blockquote>but with no visual accent border — the chrome lives in the preset, not the element (the reverse of Flutter, where the element draws the border automatically). -
TextBlock.wrapis a documented no-op on Flutter (soft-wrap is already the default); it exists only for naming parity with Web'sbreak-wordsclass. -
Naming collision:
TextBlockStyle(platform-local preset value class) vsCoreTextBlockStyle(sharedcoui_corechrome style class) are easily confused — only the bareTextBlockStyle-family name was affected by the legacyCo-prefix removal.
v0.127 Migration (Legacy)#
v0.127 is a de-prefix hard break: the component class was renamed and no Co-prefixed alias remains.
Core* contract names (CoreTextBlockElement, CoreTextBlockStyle,
CoreTextBlockContract) are unchanged — do not confuse the removed platform preset class with the current Core chrome style.
| ❌ Legacy (pre-0.127, removed) | ✅ v0.127 |
|---|---|
❌ CoTextBlock(text: …) | TextBlock(text: …) |
❌
CoTextBlock.heading1(context)
/
CoTextBlock.muted(context)
(preset helpers)
|
TextBlock.heading1(context) / TextBlock.muted(context) |
❌ CoTextBlockStyle(…) (platform preset value class) |
TextBlockStyle(…) — but always build via the TextBlock.* static helpers |
Additional migration notes:
-
❌ The Web
TextBlockStylingmarker interface was removed (Phase 6g "Styling marker sweep") — a preset is now a plain immutableTextBlockStylevalue holding the resolved class string; the component joins them into the root element's className. -
The shared chrome class
CoreTextBlockStyle(Core-prefixed) is current API, not legacy — only the bare-platformCo-prefixed names were removed. -
Import stays the single barrel (
package:coui_flutter/coui_flutter.dart/package:coui_web/coui_web.dart); per-componentsrc/imports remain forbidden.