개요
관련 컨트롤과 작업을 그룹으로 묶고 키보드 탐색을 제공하는 컨테이너입니다. 텍스트 서식 지정, 툴바, 명령 패널에 주로 사용합니다.
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
font-size: 1.25rem;
background-color: transparent;
color: var(--primary-contrast);
}
[ngToolbarWidget]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}
[ngToolbarWidget]:active {
background-color: color-mix(in srgb, var(--primary-contrast) 15%, transparent);
}
[ngToolbarWidget]:focus {
outline-offset: -1px;
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
[ngToolbarWidget][aria-pressed="true"],
[ngToolbarWidget][aria-checked="true"] {
color: color-mix(in srgb, var(--hot-pink) 80%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 10%, transparent);
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar class="material-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
display: flex;
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
width: 3rem;
height: 3rem;
font-size: 1.25rem;
border-radius: 2rem;
color: color-mix(in srgb, var(--vivid-pink) 70%, var(--full-contrast));
background-color: color-mix(in srgb, var(--vivid-pink) 20%, transparent);
transition:
width 0.2s ease-in-out,
background-color 0.15s ease-in-out,
color 0.15s ease-in-out;
}
[ngToolbarWidget]:focus {
outline-offset: 2px;
outline: 2px solid var(--vivid-pink);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
background-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--full-contrast));
color: var(--page-background);
}
[ngToolbarWidget][aria-checked='true'] {
width: 4.5rem;
}
.group {
display: flex;
gap: 0.5rem;
}
.separator {
width: 1px;
margin: 0 1rem;
height: calc(100% - 2rem);
background-color: var(--quinary-contrast);
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar class="retro-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--page-background));
--retro-pressed-tint: #fbfbfb;
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
:host-context(.docs-dark-mode) {
--retro-pressed-tint: #151417;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 1rem;
}
.group {
gap: 1rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
color: var(--page-background);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
[ngToolbarWidget]:focus,
[ngToolbarWidget]:hover {
transform: translate(1px, 1px);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--retro-pressed-tint));
}
[ngToolbarWidget]:focus {
outline-offset: 4px;
outline: 4px dashed var(--retro-button-color);
}
사용법
툴바는 사용자가 자주 사용하는 관련 컨트롤을 그룹으로 묶을 때 적합합니다. 다음과 같은 경우에 툴바 사용을 고려하세요.
- 여러 관련 작업: 텍스트 서식 버튼처럼 서로 관련된 기능을 수행하는 컨트롤이 여러 개 있을 때
- 효율적인 키보드 조작: 방향키를 이용한 빠른 키보드 탐색이 사용자에게 도움이 될 때
- 컨트롤 그룹화: 구분선으로 컨트롤을 논리적인 섹션으로 구성해야 할 때
- 반복 사용: 작업 흐름에서 컨트롤을 반복해서 사용할 때
다음과 같은 경우에는 툴바를 피하세요.
- 간단한 버튼 그룹으로 충분한 경우: 관련 없는 작업이 2~3개뿐이라면 개별 버튼이 더 적합합니다.
- 컨트롤이 서로 관련 없는 경우: 툴바는 논리적인 그룹을 의미하므로 관련 없는 컨트롤은 사용자에게 혼란을 줍니다.
- 복잡하게 중첩된 탐색: 계층이 깊다면 메뉴나 탐색 컴포넌트가 더 적합합니다.
주요 기능
Angular의 툴바는 다음과 같이 접근성을 갖춘 툴바 구현을 제공합니다.
- 키보드 탐색: 이동식 tabindex를 사용해서 방향키, Home, End로 위젯을 탐색하며, 컨트롤은 기본 활성화 동작을 유지합니다.
- 스크린 리더 지원: 보조 기술을 위한 ARIA 어트리뷰트를 기본 제공합니다.
- 위젯 그룹: 라디오 버튼 그룹이나 토글 버튼 그룹처럼 관련 위젯을 구성합니다.
- 유연한 방향 설정: 수평/수직 배치에 맞춰 키보드 탐색을 자동으로 처리합니다.
- 시그널 기반 반응성: Angular 시그널로 상태를 반응형으로 관리합니다.
- 양방향 텍스트 지원: 오른쪽에서 왼쪽으로 쓰는 언어(RTL)를 자동으로 처리합니다.
- 포커스 설정: 탐색을 순환하거나 경계에서 멈추도록 선택합니다.
예제
기본 수평 툴바
수평 툴바는 텍스트 에디터와 디자인 도구에서 흔히 사용하는 방식으로 컨트롤을 왼쪽에서 오른쪽으로 배치합니다. 방향키로 위젯 사이를 이동하며, 사용자가 Tab을 눌러 페이지의 다음 엘리먼트로 이동할 때까지 포커스를 툴바 안에 유지합니다.
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
font-size: 1.25rem;
background-color: transparent;
color: var(--primary-contrast);
}
[ngToolbarWidget]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}
[ngToolbarWidget]:active {
background-color: color-mix(in srgb, var(--primary-contrast) 15%, transparent);
}
[ngToolbarWidget]:focus {
outline-offset: -1px;
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
[ngToolbarWidget][aria-pressed="true"],
[ngToolbarWidget][aria-checked="true"] {
color: color-mix(in srgb, var(--hot-pink) 80%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 10%, transparent);
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar class="material-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
display: flex;
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
width: 3rem;
height: 3rem;
font-size: 1.25rem;
border-radius: 2rem;
color: color-mix(in srgb, var(--vivid-pink) 70%, var(--full-contrast));
background-color: color-mix(in srgb, var(--vivid-pink) 20%, transparent);
transition:
width 0.2s ease-in-out,
background-color 0.15s ease-in-out,
color 0.15s ease-in-out;
}
[ngToolbarWidget]:focus {
outline-offset: 2px;
outline: 2px solid var(--vivid-pink);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
background-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--full-contrast));
color: var(--page-background);
}
[ngToolbarWidget][aria-checked='true'] {
width: 4.5rem;
}
.group {
display: flex;
gap: 0.5rem;
}
.separator {
width: 1px;
margin: 0 1rem;
height: calc(100% - 2rem);
background-color: var(--quinary-contrast);
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar class="retro-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--page-background));
--retro-pressed-tint: #fbfbfb;
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
:host-context(.docs-dark-mode) {
--retro-pressed-tint: #151417;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 1rem;
}
.group {
gap: 1rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
color: var(--page-background);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
[ngToolbarWidget]:focus,
[ngToolbarWidget]:hover {
transform: translate(1px, 1px);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--retro-pressed-tint));
}
[ngToolbarWidget]:focus {
outline-offset: 4px;
outline: 4px dashed var(--retro-button-color);
}
수직 툴바
수직 툴바는 컨트롤을 위에서 아래로 쌓아 배치하므로 사이드 패널이나 수직 명령 팔레트에 유용합니다. 위/아래 방향키로 위젯 사이를 이동합니다.
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
}
app.html
<div ngToolbar orientation="vertical" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: flex-start;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
flex-direction: column;
padding: 1rem 0.5rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
flex-direction: column;
}
.separator {
height: 1px;
align-self: center;
width: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
font-size: 1.25rem;
background-color: transparent;
color: var(--primary-contrast);
}
[ngToolbarWidget]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}
[ngToolbarWidget]:active {
background-color: color-mix(in srgb, var(--primary-contrast) 15%, transparent);
}
[ngToolbarWidget]:focus {
outline-offset: -1px;
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
[ngToolbarWidget][aria-pressed="true"],
[ngToolbarWidget][aria-checked="true"] {
color: color-mix(in srgb, var(--hot-pink) 80%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 10%, transparent);
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
}
app.html
<div
ngToolbar
orientation="vertical"
class="vertical-material-toolbar"
aria-label="Text Formatting Tools"
>
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: flex-start;
}
[ngToolbar] {
display: flex;
flex-direction: column;
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
width: 3rem;
height: 3rem;
font-size: 1.25rem;
border-radius: 2rem;
color: color-mix(in srgb, var(--vivid-pink) 70%, var(--full-contrast));
background-color: color-mix(in srgb, var(--vivid-pink) 20%, transparent);
transition:
height 0.2s ease-in-out,
background-color 0.15s ease-in-out,
color 0.15s ease-in-out;
}
[ngToolbarWidget]:focus {
outline-offset: 2px;
outline: 2px solid var(--vivid-pink);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
background-color: var(--vivid-pink);
color: var(--page-background);
}
[ngToolbarWidget][aria-checked='true'] {
height: 4.5rem;
}
.group {
gap: 0.5rem;
display: flex;
flex-direction: column;
}
.separator {
width: 1px;
margin: 1rem 0;
height: calc(100% - 2rem);
background-color: var(--quinary-contrast);
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
}
app.html
<div ngToolbar orientation="vertical" class="retro-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: flex-start;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--page-background));
--retro-pressed-tint: #fbfbfb;
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
:host-context(.docs-dark-mode) {
--retro-pressed-tint: #151417;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 1rem;
flex-direction: column;
}
.group {
gap: 1rem;
display: flex;
flex-direction: column;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
color: var(--page-background);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
[ngToolbarWidget]:focus,
[ngToolbarWidget]:hover {
transform: translate(1px, 1px);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--retro-pressed-tint));
}
[ngToolbarWidget]:focus {
outline-offset: 4px;
outline: 4px dashed var(--retro-button-color);
}
위젯 그룹
위젯 그룹은 텍스트 정렬 옵션이나 서식 토글처럼 함께 동작하는 관련 컨트롤을 구성합니다. 그룹은 이동식 tabindex 탐색을 유지하면서 보조 기술에 적절한 의미 구조를 제공합니다.
위 예제에서는 정렬 버튼을 role="radiogroup"이 지정된 ngToolbarWidgetGroup으로 감쌉니다.
선택 상태는 툴바 컨테이너와 분리되어 있으므로 Angular 시그널이나 커스텀 디렉티브로 상태를 관리할 수 있습니다.
<!-- 한 번에 하나만 선택할 수 있는 라디오 그룹 -->
<div ngToolbarWidgetGroup role="radiogroup" aria-label="Alignment">
<button
ngToolbarWidget
role="radio"
type="button"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
>
Left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
>
Center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
>
Right
</button>
</div>
<!-- 토글 버튼 그룹 -->
<div class="group" role="group" aria-label="Text styling">
<button ngToolbarWidget type="button" [attr.aria-pressed]="bold()" (click)="bold.set(!bold())">
Bold
</button>
<button
ngToolbarWidget
type="button"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
>
Italic
</button>
</div>
비활성화된 위젯
툴바는 두 가지 비활성화 모드를 지원합니다.
- 소프트 비활성화된 위젯은 포커스를 받을 수 있으며 사용할 수 없다는 사실을 시각적으로 표시합니다.
- 하드 비활성화된 위젯은 키보드 탐색에서 완전히 제외됩니다.
softDisabled의 기본값은 true이므로 비활성화된 위젯도 포커스를 받을 수 있습니다.
하드 비활성화 모드를 사용하려면 툴바에 [softDisabled]="false"를 설정하세요.
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
disabled
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div
ngToolbarWidgetGroup
role="radiogroup"
class="group"
aria-label="Text alignment options"
disabled
>
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
font-size: 1.25rem;
background-color: transparent;
color: var(--primary-contrast);
}
[ngToolbarWidget]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}
[ngToolbarWidget]:active {
background-color: color-mix(in srgb, var(--primary-contrast) 15%, transparent);
}
[ngToolbarWidget]:focus {
outline-offset: -1px;
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
[ngToolbarWidget][aria-pressed="true"],
[ngToolbarWidget][aria-checked="true"] {
color: color-mix(in srgb, var(--hot-pink) 80%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 10%, transparent);
}
[ngToolbarWidget][aria-disabled="true"] {
cursor: default;
opacity: 0.45;
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar class="disabled-material-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
disabled
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div
ngToolbarWidgetGroup
role="radiogroup"
class="group"
aria-label="Text alignment options"
disabled
>
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
display: flex;
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
width: 3rem;
height: 3rem;
font-size: 1.25rem;
border-radius: 2rem;
color: color-mix(in srgb, var(--vivid-pink) 70%, var(--full-contrast));
background-color: color-mix(in srgb, var(--vivid-pink) 20%, transparent);
transition:
width 0.2s ease-in-out,
background-color 0.15s ease-in-out,
color 0.15s ease-in-out;
}
[ngToolbarWidget]:focus {
outline-offset: 2px;
outline: 2px solid var(--vivid-pink);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
background-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--full-contrast));
color: var(--page-background);
}
[ngToolbarWidget][aria-checked='true'] {
width: 4.5rem;
}
.group {
display: flex;
gap: 0.5rem;
}
.separator {
width: 1px;
margin: 0 1rem;
height: calc(100% - 2rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget][aria-disabled="true"] {
cursor: default;
opacity: 0.45;
}
app.ts
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar class="retro-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
disabled
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div
ngToolbarWidgetGroup
role="radiogroup"
class="group"
aria-label="Text alignment options"
disabled
>
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--page-background));
--retro-pressed-tint: #fbfbfb;
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
:host-context(.docs-dark-mode) {
--retro-pressed-tint: #151417;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 1rem;
}
.group {
gap: 1rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
color: var(--page-background);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
[ngToolbarWidget]:focus,
[ngToolbarWidget]:hover {
transform: translate(1px, 1px);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--retro-pressed-tint));
}
[ngToolbarWidget]:focus {
outline-offset: 4px;
outline: 4px dashed var(--retro-button-color);
}
[ngToolbarWidget][aria-disabled='true'] {
cursor: default;
opacity: 0.45;
}
오른쪽에서 왼쪽으로 쓰는 언어(RTL) 지원
툴바는 오른쪽에서 왼쪽으로 쓰는 언어를 자동으로 지원합니다.
툴바를 dir="rtl"이 지정된 컨테이너로 감싸면 배치 방향과 키보드 탐색 방향이 반대로 바뀝니다.
방향키 탐색도 자동으로 조정되어 왼쪽 방향키는 다음 위젯으로, 오른쪽 방향키는 이전 위젯으로 이동합니다.
app.ts
import {Dir} from '@angular/cdk/bidi';
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Dir, Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar dir="rtl" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
font-size: 1.25rem;
background-color: transparent;
color: var(--primary-contrast);
}
[ngToolbarWidget]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}
[ngToolbarWidget]:active {
background-color: color-mix(in srgb, var(--primary-contrast) 15%, transparent);
}
[ngToolbarWidget]:focus {
outline-offset: -1px;
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
[ngToolbarWidget][aria-pressed="true"],
[ngToolbarWidget][aria-checked="true"] {
color: color-mix(in srgb, var(--hot-pink) 80%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 10%, transparent);
}
app.ts
import {Dir} from '@angular/cdk/bidi';
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Dir, Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar dir="rtl" class="rtl-material-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
display: flex;
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
width: 3rem;
height: 3rem;
font-size: 1.25rem;
border-radius: 2rem;
color: color-mix(in srgb, var(--vivid-pink) 70%, var(--full-contrast));
background-color: color-mix(in srgb, var(--vivid-pink) 20%, transparent);
transition:
width 0.2s ease-in-out,
background-color 0.15s ease-in-out,
color 0.15s ease-in-out;
}
[ngToolbarWidget]:focus {
outline-offset: 2px;
outline: 2px solid var(--vivid-pink);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
background-color: var(--vivid-pink);
color: var(--page-background);
}
[ngToolbarWidget][aria-checked='true'] {
width: 4.5rem;
}
.group {
display: flex;
gap: 0.5rem;
}
.separator {
width: 1px;
margin: 0 1rem;
height: calc(100% - 2rem);
background-color: var(--quinary-contrast);
}
app.ts
import {Dir} from '@angular/cdk/bidi';
import {Component, signal} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Dir, Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {
readonly bold = signal(false);
readonly italic = signal(false);
readonly underlined = signal(false);
readonly alignment = signal<'left' | 'center' | 'right'>('left');
}
app.html
<div ngToolbar dir="rtl" class="rtl-retro-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
class="material-symbols-outlined"
[attr.aria-pressed]="bold()"
(click)="bold.set(!bold())"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
class="material-symbols-outlined"
[attr.aria-pressed]="italic()"
(click)="italic.set(!italic())"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
class="material-symbols-outlined"
[attr.aria-pressed]="underlined()"
(click)="underlined.set(!underlined())"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'left'"
(click)="alignment.set('left')"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'center'"
(click)="alignment.set('center')"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
class="material-symbols-outlined"
[attr.aria-checked]="alignment() === 'right'"
(click)="alignment.set('right')"
translate="no"
>
format_align_right
</button>
</div>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--page-background));
--retro-pressed-tint: #fbfbfb;
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
:host-context(.docs-dark-mode) {
--retro-pressed-tint: #151417;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 1rem;
}
.group {
gap: 1rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
color: var(--page-background);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
[ngToolbarWidget]:focus,
[ngToolbarWidget]:hover {
transform: translate(1px, 1px);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--retro-pressed-tint));
}
[ngToolbarWidget]:focus {
outline-offset: 4px;
outline: 4px dashed var(--retro-button-color);
}
테스트
Angular Aria는 툴바 컴포넌트를 테스트할 수 있는 컴포넌트 하네스를 제공합니다. 컴포넌트 테스트에서 하네스를 사용하는 예제는 다음과 같습니다.
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {ToolbarHarness} from '@angular/aria/toolbar/testing';
import {MyToolbarComponent} from './my-toolbar'; // 테스트할 컴포넌트
describe('MyToolbarComponent', () => {
let fixture: ComponentFixture<MyToolbarComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [MyToolbarComponent],
});
fixture = TestBed.createComponent(MyToolbarComponent);
await fixture.whenStable();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should have widgets and update toggle state on click', async () => {
// 툴바 하네스를 불러옵니다.
const toolbar = await loader.getHarness(ToolbarHarness);
// 모든 위젯을 가져옵니다.
const widgets = await toolbar.getWidgets();
expect(widgets.length).toBe(3);
// 첫 번째 위젯을 클릭합니다.
await widgets[0].click();
// 클릭 핸들러가 눌림 상태를 갱신했는지 확인합니다.
expect(await widgets[0].isSelected()).toBe(true);
});
});
API 레퍼런스
자세한 API 문서는 다음 API 레퍼런스를 참고하세요.