@swiss-ui/angular
v0.1.0
Published
Angular components for Swiss UI design system
Readme
@swiss-ui/angular
Angular components for the Swiss UI design system.
Installation
npm install @swiss-ui/angularArchitecture
Every component ships in two modes:
- Headless — logic, state, accessibility, ARIA. No classes by default.
- Styled — wraps headless, adds
swiss-*classes from@swiss-ui/core.
// Headless (zero runtime styles)
import { SwissButtonComponent } from '@swiss-ui/angular';
// With swiss-* classes
import { StyledButtonComponent } from '@swiss-ui/angular/styled';Bootstrap (standalone)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideAnimations()],
});Components
Primitives
SwissBoxComponent
Polymorphic container element.
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| el | BoxElement | 'div' | HTML tag to render |
| role | string \| null | null | ARIA role |
| ariaLabel | string \| null | null | ARIA label |
<swiss-box el="section" role="region" ariaLabel="Main content">
Content
</swiss-box>Layout
SwissContainerComponent
| Input | Type | Default |
|-------|------|---------|
| size | 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full' | 'lg' |
<swiss-container size="xl">
Page content
</swiss-container>SwissStackComponent
| Input | Type | Default |
|-------|------|---------|
| direction | 'row' \| 'column' \| 'row-reverse' \| 'column-reverse' | 'row' |
| align | 'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline' | 'start' |
| justify | 'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly' | 'start' |
| gap | string | '0' |
| wrap | boolean | false |
<swiss-stack direction="column" gap="1rem" align="center">
<swiss-button>One</swiss-button>
<swiss-button>Two</swiss-button>
</swiss-stack>SwissGridComponent / SwissGridItemComponent
| Input | Type | Default |
|-------|------|---------|
| cols | number | 12 |
| gap | string | '0' |
| align | 'start' \| 'center' \| 'end' \| 'stretch' | 'stretch' |
SwissGridItemComponent:
| Input | Type | Default |
|-------|------|---------|
| colSpan | number \| null | null |
| rowSpan | number \| null | null |
<swiss-grid cols="3" gap="1rem">
<swiss-grid-item colSpan="2">Wide cell</swiss-grid-item>
<swiss-grid-item>Narrow cell</swiss-grid-item>
</swiss-grid>Typography
SwissHeadingComponent
| Input | Type | Default |
|-------|------|---------|
| level | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | 1 |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| '3xl' \| '4xl' \| null | null |
| weight | 'normal' \| 'medium' \| 'semibold' \| 'bold' \| null | null |
| align | 'left' \| 'center' \| 'right' \| 'justify' \| null | null |
<swiss-heading level="2" size="xl" weight="bold">Page Title</swiss-heading>SwissTextComponent
| Input | Type | Default |
|-------|------|---------|
| el | 'p' \| 'span' \| 'label' \| 'small' \| 'strong' \| 'em' \| 'div' | 'p' |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| null | null |
| weight | 'normal' \| 'medium' \| 'semibold' \| 'bold' \| null | null |
| align | TextAlign \| null | null |
| color | 'default' \| 'muted' \| 'subtle' \| 'disabled' \| 'primary' \| 'success' \| 'warning' \| 'error' \| null | null |
| truncate | boolean | false |
<swiss-text size="sm" color="muted" truncate>Long text that will be truncated</swiss-text>Controls
SwissButtonComponent
| Input | Type | Default |
|-------|------|---------|
| el | 'button' \| 'a' | 'button' |
| variant | 'solid' \| 'outline' \| 'ghost' \| 'link' | 'solid' |
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| loading | boolean | false |
| disabled | boolean | false |
| type | 'button' \| 'submit' \| 'reset' | 'button' |
| href | string \| null | null |
Content projection:
[swissButtonLeftIcon]— icon before label[swissButtonRightIcon]— icon after label
<swiss-button variant="outline" size="lg">
<span swissButtonLeftIcon>+</span>
Add item
</swiss-button>SwissInputComponent
Implements ControlValueAccessor. Compatible with FormControl and ngModel.
| Input | Type | Default |
|-------|------|---------|
| type | InputType | 'text' |
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| disabled | boolean | false |
| invalid | boolean | false |
| placeholder | string | '' |
| describedBy | string \| null | null |
| ariaLabel | string \| null | null |
| Model | Type | Description |
|-------|------|-------------|
| value | string | Two-way binding via model() |
Content projection:
[swissInputLeftAddon][swissInputRightAddon]
Reactive Forms:
<swiss-input [formControl]="emailControl" type="email" placeholder="Email" />Template-driven:
<swiss-input [(ngModel)]="email" type="email" />SwissTextareaComponent
| Input | Type | Default |
|-------|------|---------|
| resize | 'none' \| 'both' \| 'horizontal' \| 'vertical' | 'vertical' |
| rows | number | 3 |
| autoResize | boolean | false |
| disabled | boolean | false |
| invalid | boolean | false |
| placeholder | string | '' |
SwissSelectComponent
| Input | Type | Default |
|-------|------|---------|
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| disabled | boolean | false |
| invalid | boolean | false |
<swiss-select [formControl]="countryControl">
<option value="ch">Switzerland</option>
<option value="de">Germany</option>
</swiss-select>SwissCheckboxComponent
| Input | Type | Default |
|-------|------|---------|
| indeterminate | boolean | false |
| disabled | boolean | false |
| invalid | boolean | false |
| value | string | '' |
| name | string \| null | null |
| Model | Type |
|-------|------|
| checked | boolean |
<swiss-checkbox [formControl]="agreeControl">I agree to the terms</swiss-checkbox>SwissRadioGroupComponent + SwissRadioComponent
SwissRadioGroupComponent implements ControlValueAccessor. Child SwissRadioComponent instances connect via SWISS_RADIO_GROUP_TOKEN.
| Input (group) | Type | Default |
|---------------|------|---------|
| orientation | 'horizontal' \| 'vertical' | 'vertical' |
| disabled | boolean | false |
| name | string | auto-generated |
| Input (radio) | Type | Required |
|---------------|------|----------|
| value | unknown | yes |
| disabled | boolean | no |
<swiss-radio-group [formControl]="sizeControl" orientation="horizontal">
<swiss-radio value="sm">Small</swiss-radio>
<swiss-radio value="md">Medium</swiss-radio>
<swiss-radio value="lg">Large</swiss-radio>
</swiss-radio-group>SwissSwitchComponent
| Input | Type | Default |
|-------|------|---------|
| disabled | boolean | false |
| ariaLabel | string \| null | null |
| describedBy | string \| null | null |
| Model | Type |
|-------|------|
| checked | boolean |
<swiss-switch [formControl]="darkModeControl" ariaLabel="Enable dark mode" />Feedback
SwissBadgeComponent
| Input | Type | Default |
|-------|------|---------|
| variant | 'solid' \| 'outline' \| 'subtle' | 'subtle' |
| color | 'default' \| 'primary' \| 'success' \| 'warning' \| 'error' \| 'info' | 'default' |
<swiss-badge variant="solid" color="success">Active</swiss-badge>SwissAlertComponent
| Input | Type | Default |
|-------|------|---------|
| variant | 'info' \| 'success' \| 'warning' \| 'error' | 'info' |
Content projection:
[swissAlertIcon][swissAlertTitle][swissAlertDescription][swissAlertActions]
<swiss-alert variant="warning">
<span swissAlertTitle>Warning</span>
<span swissAlertDescription>Please review before proceeding.</span>
</swiss-alert>SwissSpinnerComponent
| Input | Type | Default |
|-------|------|---------|
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' |
| label | string | 'Loading...' |
| color | string \| null | null |
Respects prefers-reduced-motion.
<swiss-spinner size="lg" label="Saving changes" />Overlay
SwissModalComponent
| Model | Type | Description |
|-------|------|-------------|
| open | boolean | Controls visibility |
| Input | Type | Default |
|-------|------|---------|
| closeOnEscape | boolean | true |
| closeOnBackdrop | boolean | true |
Composed via InjectionToken — child components (swiss-modal-header, swiss-modal-body, etc.) connect automatically.
Composed parts:
SwissModalOverlayComponent(swiss-modal-overlay)SwissModalContentComponent(swiss-modal-content)SwissModalHeaderComponent(swiss-modal-header)SwissModalBodyComponent(swiss-modal-body)SwissModalFooterComponent(swiss-modal-footer)SwissModalCloseButtonComponent(swiss-modal-close-button)
<button (click)="isOpen.set(true)">Open</button>
<swiss-modal [(open)]="isOpen">
<swiss-modal-header>
<swiss-heading level="2">Confirm action</swiss-heading>
<swiss-modal-close-button>x</swiss-modal-close-button>
</swiss-modal-header>
<swiss-modal-body>
<swiss-text>Are you sure you want to continue?</swiss-text>
</swiss-modal-body>
<swiss-modal-footer>
<swiss-button variant="ghost" (click)="isOpen.set(false)">Cancel</swiss-button>
<swiss-button variant="solid">Confirm</swiss-button>
</swiss-modal-footer>
</swiss-modal>SwissTooltipComponent
| Model | Type |
|-------|------|
| open | boolean |
| Input | Type | Default |
|-------|------|---------|
| placement | Placement | 'bottom' |
| delay | number | 200 |
| disabled | boolean | false |
Content projection:
[swissTooltipTrigger][swissTooltipContent]
<swiss-tooltip placement="top">
<button swissTooltipTrigger>Hover me</button>
<span swissTooltipContent>Helpful information</span>
</swiss-tooltip>SwissDropdownComponent
| Model | Type |
|-------|------|
| open | boolean |
Keyboard: ArrowDown, ArrowUp, Home, End, Escape.
Composed parts:
SwissDropdownTriggerComponent(swiss-dropdown-trigger)SwissDropdownContentComponent(swiss-dropdown-content)SwissDropdownItemComponent(swiss-dropdown-item)SwissDropdownSeparatorComponent(swiss-dropdown-separator)SwissDropdownLabelComponent(swiss-dropdown-label)
<swiss-dropdown>
<swiss-dropdown-trigger>Actions</swiss-dropdown-trigger>
<swiss-dropdown-content>
<swiss-dropdown-label>File</swiss-dropdown-label>
<swiss-dropdown-item>New</swiss-dropdown-item>
<swiss-dropdown-item>Open</swiss-dropdown-item>
<swiss-dropdown-separator />
<swiss-dropdown-item>Delete</swiss-dropdown-item>
</swiss-dropdown-content>
</swiss-dropdown>Services
SwissThemeService
import { SwissThemeService } from '@swiss-ui/angular/services';
@Component({ ... })
export class AppComponent {
private theme = inject(SwissThemeService);
readonly currentTheme = this.theme.theme; // Signal<'light' | 'dark'>
readonly isDark = this.theme.isDark; // Signal<boolean>
toggleTheme() {
this.theme.toggle();
}
}SwissOverlayService
import { SwissOverlayService } from '@swiss-ui/angular/services';
@Component({ ... })
export class AppComponent {
private overlay = inject(SwissOverlayService);
openDialog() {
const ref = this.overlay.open(MyDialogComponent);
// ref.destroy() to close programmatically
}
}Directives
import {
SwissFocusTrapDirective,
SwissClickOutsideDirective,
SwissEscapeKeyDirective,
SwissAutoPlacementDirective,
} from '@swiss-ui/angular/directives';| Directive | Selector | Events |
|-----------|----------|--------|
| SwissFocusTrapDirective | [swissFocusTrap] | — |
| SwissClickOutsideDirective | [swissClickOutside] | swissClickOutside |
| SwissEscapeKeyDirective | [swissEscapeKey] | swissEscapeKey |
| SwissAutoPlacementDirective | [swissAutoPlacement] | placementChange |
Forms Integration
Reactive Forms
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { SwissInputComponent, SwissButtonComponent, SwissCheckboxComponent } from '@swiss-ui/angular';
@Component({
standalone: true,
imports: [ReactiveFormsModule, SwissInputComponent, SwissButtonComponent, SwissCheckboxComponent],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<swiss-input formControlName="email" type="email" placeholder="Email"
[invalid]="form.controls.email.invalid && form.controls.email.touched" />
<swiss-checkbox formControlName="agree">Accept terms</swiss-checkbox>
<swiss-button type="submit" [disabled]="form.invalid">Submit</swiss-button>
</form>
`,
})
export class SignupComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
agree: [false, Validators.requiredTrue],
});
submit() {
if (this.form.valid) console.log(this.form.value);
}
}Template-driven Forms
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SwissInputComponent, SwissSwitchComponent } from '@swiss-ui/angular';
@Component({
standalone: true,
imports: [FormsModule, SwissInputComponent, SwissSwitchComponent],
template: `
<form #f="ngForm" (ngSubmit)="submit(f)">
<swiss-input [(ngModel)]="name" name="name" required placeholder="Name" />
<swiss-switch [(ngModel)]="notifications" name="notifications">Notifications</swiss-switch>
<button type="submit">Save</button>
</form>
`,
})
export class ProfileComponent {
name = '';
notifications = true;
submit(form: NgForm) {
if (form.valid) console.log({ name: this.name, notifications: this.notifications });
}
}Accessibility
All components follow WCAG 2.1 AA:
- Keyboard navigation on all interactive elements
- ARIA roles, states, and properties on all components
- Focus management in Modal (focus trap, restore on close)
role="switch"witharia-checkedon Switchrole="radiogroup"with linked radio inputsaria-invalid,aria-describedbyon form controlsaria-modal,aria-labelledby,aria-describedbyon dialogs- Screen-reader-only labels on Spinner
prefers-reduced-motionrespected in Spinner and Modal animations- SSR-safe (no
document/windowaccess withoutisPlatformBrowser)
Entrypoints
| Import path | Contents |
|-------------|----------|
| @swiss-ui/angular | All headless components, directives, services, tokens |
| @swiss-ui/angular/styled | Styled variants with swiss-* CSS classes |
| @swiss-ui/angular/directives | Directives only |
| @swiss-ui/angular/services | Services only |
