SelectableText
사용자가 선택하고 복사할 수 있는 텍스트 컴포넌트
목차
사용자가 마우스로 드래그하여 선택하고 복사할 수 있는 텍스트 컴포넌트입니다. Flutter 는 m.SelectableText 로 텍스트 선택을 처리하고, Web 은
select-text 클래스로 user-select: text 동작을 적용합니다. 양쪽 모두 plain text 와 rich text 를 지원합니다.
기본 사용법 (Basic Usage)#
// plain text
const SelectableText('You can select and copy this text.')
// 커스텀 텍스트 스타일
const SelectableText(
'Selectable text with a custom style.',
selectableTextStyle: CoreSelectableTextStyle(
textStyle: CoreTextStyle(
fontSize: CoreFontSize.s18,
fontWeight: CoreFontWeight.semiBold,
),
),
)
// rich text
SelectableText.rich(children: [Text('Selectable content')])
// plain text
const SelectableText('You can select and copy this text.')
// 커스텀 텍스트 스타일
const SelectableText(
'Selectable text with a custom style.',
selectableTextStyle: CoreSelectableTextStyle(
textStyle: CoreTextStyle(
fontSize: CoreFontSize.s18,
fontWeight: CoreFontWeight.semiBold,
),
),
)
// rich text
SelectableText.rich(children: [span([text('Selectable content')])])
빠른 오버라이드 (Chain)#
이미 만든 SelectableText 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class SelectableTextChainExample extends StatelessWidget {
const SelectableTextChainExample({super.key});
@override
Widget build(BuildContext context) {
return const SelectableText(
'You can select and copy this text.',
).withStyle(
const CoreSelectableTextStyle(
textStyle: CoreTextStyle.token(
CoreTextStyles.bodyLarge,
color: CoreColor.token(CoreColors.primary),
),
cursorColor: CoreColor.token(CoreColors.primary),
cursorWidth: CoreStrokeWidth.stroke3,
cursorRadius: CoreBorderRadius.all(CoreRadius.radius4),
selectionColor: CoreColor.token(
CoreColors.primary,
opacity: CoreOpacity.opacity25,
),
),
);
}
}
class SelectableTextChainExample extends StatelessComponent {
const SelectableTextChainExample({super.key});
@override
Component build(BuildContext context) {
return const SelectableText(
'You can select and copy this text.',
).withStyle(
const CoreSelectableTextStyle(
textStyle: CoreTextStyle.token(
CoreTextStyles.bodyLarge,
color: CoreColor.token(CoreColors.primary),
),
cursorColor: CoreColor.token(CoreColors.primary),
cursorWidth: CoreStrokeWidth.stroke3,
cursorRadius: CoreBorderRadius.all(CoreRadius.radius4),
selectionColor: CoreColor.token(
CoreColors.primary,
opacity: CoreOpacity.opacity25,
),
),
);
}
}
Live Preview#
코드 보기
class SelectableTextDefaultExample extends StatelessComponent {
const SelectableTextDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const SelectableText('You can select and copy this text.');
}
}
class SelectableTextDefaultExample extends StatelessWidget {
const SelectableTextDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const SelectableText('You can select and copy this text.');
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 사용자가 본문 텍스트를 복사할 수 있어야 할 때 (코드 토큰, ID, 안내 문구 등)
- Web 의
user-select: none상속으로 인해 선택이 막히는 영역에서 선택을 보장할 때 - 읽기 전용이지만 복사 가능해야 하는 정보 표시 영역
대신 다른 컴포넌트를 사용하세요:
Text: 선택 기능이 필요 없는 일반 텍스트 표시CodeSnippet: 복사 버튼이 있는 코드 블록TextField: 사용자가 편집해야 하는 입력 필드
Props / Parameters#
W는 rich 자식의 타입으로 Flutter의 Widget, Web의 Component입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
text |
String? |
null |
표시할 plain text. 기본 생성자에서 필수, .rich 에서는 사용 안 함 |
children |
List<W>? |
null |
rich text 자식 노드 (.rich 생성자) |
selectableTextStyle |
CoreSelectableTextStyle? |
null |
텍스트 스타일 / 커서 chrome override 슬롯 |
maxLines |
int? |
null |
클리핑 전 최대 줄 수 |
textAlign |
CoreTextAlign? |
null |
텍스트 수평 정렬 |
textDirection |
CoreTextDirection? |
null |
텍스트 읽기 방향 |
enableInteractiveSelection |
bool |
true |
사용자 선택 / 하이라이트 허용 여부 |
semanticsLabel |
String? |
null |
보조 기술용 시맨틱 라벨 (Flutter: semantics, Web: aria-label) |
onTap |
void Function()? |
null |
텍스트 탭 / 클릭 콜백 |
autofocus |
bool |
false |
첫 빌드 시 포커스 여부 (Flutter 전용) |
showCursor |
bool |
false |
텍스트 선택 커서 표시 여부 (Flutter 전용) |
minLines |
int? |
null |
최소 차지 줄 수 (Flutter 전용) |
스타일 시스템 (Style System)#
모든 텍스트 스타일 / 커서 chrome override 는 selectableTextStyle 슬롯 하나로 흐릅니다.
| 슬롯 | 타입 | 비고 |
|---|---|---|
textStyle |
CoreTextStyle? |
콘텐츠 텍스트 스타일 (fontSize / fontWeight / color 등). |
cursorColor | CoreColor? | 캐럿 색상 (Flutter 전용). |
cursorWidth |
double? |
캐럿 너비 logical px (Flutter 전용). |
cursorHeight |
double? |
캐럿 높이 logical px (Flutter 전용). |
cursorRadius |
CoreBorderRadius? |
캐럿 모서리 반경 (Flutter 전용). |
캐럿의 시각적 렌더링은 Flutter 에만 적용됩니다 — Web 의 비편집 텍스트는 브라우저 기본 캐럿을 사용합니다. 다만 Web resolver 는 파리티 / 툴링을 위해
caret-color 와 --coui-caret-width / --coui-caret-height / --coui-caret-radius
커스텀 프로퍼티로 해소된 캐럿 chrome 을 노출합니다 (text_area / text_field 와 동일).
Resolve chain#
design system default (CoreSelectableTextStyle.defaultX)
→ CoreSelectableTextTheme.style // 프로젝트 공통
→ widget.selectableTextStyle // 인스턴스별
동작 스펙 (Behavior)#
인터랙션#
- 사용자가 마우스로 드래그하여 텍스트를 선택 / 하이라이트할 수 있습니다.
-
선택 영역 하이라이트의 기본 색은
primaryContainer토큰이고, 선택된 글자의 색은 바뀌지 않습니다(onSurface그대로).selectableTextStyle의selectionColor로 바꿉니다. -
하이라이트는 글자 뒤에 칠해지므로 색을 준 그대로(알파 포함) 칠합니다 —
opacity: CoreOpacity.opacity25를 주면 두 플랫폼 모두 25% 로 칠해집니다. 불투명도를 따로 곱하지 않습니다. enableInteractiveSelection: false로 선택을 비활성화할 수 있습니다.
레이아웃#
-
Flutter 는
m.SelectableText/m.SelectableText.rich로 렌더, Web 은<span>+select-text클래스로 렌더. maxLines가 지정되면 해당 줄 수 이후 클리핑.
토큰#
| 항목 | Flutter | Web |
|---|---|---|
| 캐럿 너비 | CoreSelectableTextStyle.defaultCursorWidth (CoreFocusRing.ringWidth) |
--coui-caret-width 커스텀 프로퍼티 (시각 캐럿은 브라우저 기본) |
| 텍스트 스타일 | selectableTextStyle.textStyle → TextStyle |
selectableTextStyle.textStyle → inline CSS |
사용 가이드라인 (Usage Guidelines)#
읽기 전용 내용의 선택·복사가 필요하면 SelectableText를 사용합니다. 문서의 제목·인용 구조에는 TextBlock, 시각 스타일만 필요하면 Text, 값 편집이 필요하면 TextField를 선택합니다.
여러 줄 입력과 리사이즈가 필요하면 TextArea를 사용합니다.
✅ Do#
복사가 필요한 정보성 텍스트에 사용하세요:
const SelectableText('Order ID: A1B2-C3D4')
❌ Don't#
편집이 필요한 입력 필드 대신 사용하지 마세요. 편집이 필요하면 TextField를 쓰세요.
// ❌ — 편집 불가
const SelectableText('편집해 주세요')
접근성 (Accessibility)#
역할#
-
Flutter: 컴포넌트 자신은
Semantics를 호출하지 않고m.SelectableText에semanticsLabel을 넘기기만 합니다 (selectable_text.dart:179·:201). 프레임워크가 그 값을Semantics(label: …, excludeSemantics: semanticsLabel != null)로 감싸므로 (Flutter SDKmaterial/selectable_text.dart:804-806), 라벨을 주면 실제 텍스트가 접근성 트리에서 빠지고 라벨이 그 자리를 대신합니다. 별도의 role 은 없습니다. -
Web:
role이 붙지 않는 평범한<span>입니다 (selectable_text.dart:48·:129-140).semanticsLabel이 있으면aria-label,textDirection이 있으면dir="rtl"/"ltr"이 추가되고 (:123-124), 그 둘을 제외한 나머지는 호출자가 넘긴attributes가 그대로 통과합니다 (:122).
키보드#
처리하는 키가 없습니다 — 두 구현 어디에도 키 핸들러가 없습니다.
| 키 | Flutter | Web |
|---|---|---|
Tab |
정지하지 않음 —
m.SelectableText
의 내부
FocusNode
가
skipTraversal: true
로 생성됩니다 (SDK
material/selectable_text.dart:528
). CoUI 는
focusNode
파라미터를 노출하지 않아 이를 바꿀 수 없습니다
|
정지하지 않음 — <span> 에 tabindex 가 없습니다 |
Enter / Space | 없음 | 없음 |
onTap 은 Flutter 에서 m.SelectableText(onTap:) 제스처, Web 에서 <span>
의 click 리스너 (selectable_text.dart:137) 로만 배선되어 포인터 전용입니다. 첫 빌드에 포커스를 요청하는 경로는 Flutter 의
autofocus: true (:171 · :193) 이고, Web 에는 대응 파라미터가 없습니다.
스크린 리더#
접근 가능한 이름은 화면에 그려지는 텍스트 자체(text 또는 children)입니다. semanticsLabel
을 주면 이름의 출처가 바뀌는데 그 의미가 플랫폼마다 다릅니다 — Flutter 는 텍스트를 트리에서 제외하고 라벨로 대체하는 반면, Web 은 텍스트 노드를 그대로 둔 채
<span> 에 aria-label 을 덧붙일 뿐입니다.
live region 은 없습니다. 선택 범위가 바뀌거나 사용자가 복사해도 통보되지 않습니다 — 양쪽 파일 모두 liveRegion / aria-live
를 설정하지 않습니다.
포커스 관리#
오버레이가 아니므로 진입·이탈 시 포커스 이동이 없고 트랩도 없습니다. 위 키보드 절대로 두 플랫폼 모두 traversal 정지점을 만들지 않아, 이 컴포넌트는 Tab 순서에 아예 등장하지 않습니다.
알려진 제약#
-
onTap에 키보드 경로가 없습니다 — Flutter 는 제스처, Web 은click리스너로만 연결되고 포커스 가능한 요소가 아니라서 (selectable_text.dart:129-140,tabindex없음), 마우스·터치를 쓰지 않는 사용자는 그 동작을 실행할 방법이 없습니다. 대안: 활성화가 필요한 텍스트라면Link또는Button을 쓰고,SelectableText는 복사 대상 텍스트에만 씁니다. -
semanticsLabel의 도달 범위가 플랫폼마다 다릅니다 — Flutter 는 텍스트를 대체하지만 Web 은 role 없는<span>에aria-label을 얹습니다 (selectable_text.dart:48·:124). ARIA 는 role 이 없는<span>(generic)에 대한aria-label을 이름 지정 대상으로 보지 않으므로 보조 기술이 이를 무시할 수 있고, 그러면 같은 코드가 Flutter 에서는 라벨을, Web 에서는 원문을 읽습니다. 대안: Web 은attributes진입점이 열려 있으므로 (:122) 이름이 실제로 필요하면attributes: {'role': 'img'}처럼 이름을 지원하는 role 을 함께 지정합니다. -
maxLines가 Web 에서는 시각적으로만 잘라냅니다 — Web resolver 는-webkit-line-clamp+overflow: hidden을 emit 하므로 (selectable_style.dart:185-191) 잘려 보이는 줄이 DOM 에 그대로 남아 스크린 리더는 전문을 읽습니다. 화면에 보이는 분량과 읽히는 분량이 어긋나고, 잘린 부분을 펼칠 어포던스는 컴포넌트가 제공하지 않습니다. 대안: 잘라낸 텍스트를 펼칠 수 있어야 하면 호출부가maxLines를 직접 토글합니다.
이 컴포넌트가 직접 다루지 않는 대비·모션 등 전역 축은 전역 접근성 축 에서 한 곳으로 관리됩니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 선택 엔진 | Material SelectableText의 선택 동작 | <span>과 CSS user-select의 브라우저 동작 |
| rich 콘텐츠 | WidgetSpan 목록을 TextSpan에 합성 | <span>의 Component 자식 목록 |
| 캐럿/줄 인프라 | autofocus·showCursor·minLines와 캐럿 chrome |
읽기 전용 DOM 텍스트의 브라우저 선택; 캐럿 크기 변수는 시각 캐럿을 만들지 않음 |
| 탭 통보 | onTap을 프레임워크 위젯에 전달 | onTap을 DOM click에 연결 |
| 접근성 이름 | semanticsLabel | aria-label |
관련 컴포넌트 (Related Components)#
- Text: 선택 기능 없는 일반 텍스트
- CodeSnippet: 복사 버튼이 있는 코드 블록
- Link: 클릭 가능한 텍스트 링크