GUILD OF GLEKS UIv21.14.0

@guildofgleks/ui

AGENTS.md

The per-component API reference the package ships for a coding agent to read — v21.14.0. Read from node_modules at build time rather than copied here, so it cannot describe an input the installed package does not have.

Download AGENTS.md

This file is for an AI coding agent (Claude, Copilot, Cursor, etc.) helping a developer build an app that consumes the published @guildofgleks/ui npm package. It is not about authoring the library — if you are working inside the gleks_web_ui monorepo itself, read .github/instructions/*.md instead.

Everything below reflects the library's actual source as of 21.9.0 (in progress — the released version is 21.8.0; see CHANGELOG.md for what 21.9.0 adds). 21.7.0 removed the three abbreviated token prefixes and 21.5.0 removed a batch of deprecated API — see Removed in 21.7.0 and Removed in 21.5.0 near the end of this file, which exist so code written against an older version can be migrated — and CHANGELOG.md has the rest. README.md covers the same ground at a higher level — install, setup, theming, global configuration — and is accurate; this file goes further, into per-component input tables, and is the one to trust for exact names, types and defaults.

Maintainers: this file ships inside the npm package and is the API reference an agent reads while writing code against it, so a stale table here becomes wrong code in someone else's app — silently, because nothing fails a build. Any change to an input, output, slot, type, service method or default updates this file in the same change, and moves the version marker in the paragraph above. See .github/instructions/gleks-ui-library.instructions.md, definition of done, step 9.

Quick facts

  • Angular v21+ only (peerDependencies require ^21.2.0 for @angular/core, @angular/common, @angular/forms, @angular/platform-browser). No support for older Angular.
  • No Angular CDK, no Material. Only runtime dependency is tslib.
  • Every component is standalone, ChangeDetectionStrategy.OnPush, and built with signals — input() / output() / model(), never @Input()/@Output() decorators, never ngClass/ ngStyle.
  • Reactive Forms only. Every form control implements ControlValueAccessor and is built and tested against [formControl] / formControlName. The library never imports FormsModule and [(ngModel)] is untested — don't suggest it.
  • Theming is 100% CSS custom properties (--gog-*) — no Sass config, no JS theme objects, no build step to restyle anything.
  • Tree-shakeable: "sideEffects": false and every component is a separate standalone import, so importing ButtonComponent alone does not pull in the rest of the library — measured on the real CLI, a button costs about 10 kB gzip over an empty app and all 31 components about 75 kB. It does not yet code-split: a component used only behind a lazy route still ships in the initial bundle, because the root is one module. docs/entry-points.md in the repository is the plan that fixes it.
  • Import from @guildofgleks/ui. @guildofgleks/ui/shared also resolves — it is the package's internal entry point, which the root and its other entry points share so that GOG_CONFIG exists once. It is not an API to build an app on.
  • SSR-safe: anything touching window/document is guarded with isPlatformBrowser/ afterNextRender.

Install & setup

bash
npm install @guildofgleks/ui
# or
yarn add @guildofgleks/ui
# or — installs it and adds the stylesheet below to angular.json automatically
ng add @guildofgleks/ui

Add the baseline stylesheet once — it carries every token the components read plus their utility classes, so without it components render unstyled:

jsonc
// angular.json → projects.<app>.architect.build.options
"styles": [
  "node_modules/@guildofgleks/ui/styles/index.css",
  "src/styles.scss", // your own styles, after the baseline so they win
],

Import components where you use them — every one is standalone:

ts
import { Component } from '@angular/core';
import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';

@Component({
  selector: 'app-example',
  imports: [ButtonComponent, SelectComponent],
  template: `
    <gog-select label="Region" [options]="regions" [(value)]="region" />
    <gog-button (gogClick)="save()">Save</gog-button>
  `,
})
export class ExampleComponent {}

Core conventions (read once, applies everywhere)

These hold for essentially every component in the library. Knowing them means you can guess a new component's API correctly instead of guessing wrong and hallucinating an input that doesn't exist.

  • Selector prefix gog- for components (gog-button, gog-select, …), attribute selectors for directives (gogTooltip, [gogBadge]).
  • Outputs are prefixed gog so they never collide with native DOM events — gogClick, gogToggle, gogSearch, gogTabChange, gogRemove, gogScroll, gogLoadMore, gogDateSelect. Inputs keep their natural name (variant, size, disabled).
  • Two-way binding via model(). Wherever a component holds a value the consumer drives, it's a model() input — bind with [(value)]="signal" / [(checked)]="signal" / [(open)]="signal" etc., or split into [value] + (valueChange).
  • Every input has a zero-config default. Nothing requires configuration to render something reasonable.
  • size is GogSize = 'xsm' | 'sm' | 'md' | 'lg' | 'slg', shared by every sized component. Default is 'md' almost everywhere — exceptions: gog-accordion and gog-table default to 'lg' (their size means row/section density, not form-control size), gog-paginator defaults to 'sm'.
  • variant is GogVariant = 'primary' | 'secondary' | 'outline' | 'ghost' on gog-button. Status-colored components (gog-tag, gog-badge) use a different, four-value GogTagVariant = 'success' | 'danger' | 'warning' | 'info' instead — don't confuse the two.
  • errorDisplay: GogErrorDisplay = 'auto' | 'manual' (default 'manual') on every control that shows a validation message (inputfield, textarea, select, multiselect, autocomplete, radio-group, slider, datepicker). 'manual': the field shows errorMessage whenever it's non-empty — you own the timing (errorMessage="control.invalid && control.touched ? 'Required' : ''"). 'auto': shown once the attached [formControl]/formControlName is touched and invalid — you only supply the message text. 'auto' silently behaves like 'manual' if there's no real form control attached.
  • inputId is optional everywhere. Every form control renders a real id — its own if you pass one, a generated one otherwise — so the <label for> and the error message's aria-describedby are always wired up. Pass inputId only when something outside the component needs to reference the field by a known id; never pass one just to get a label.
  • User-visible chrome strings come from GOG_CONFIG.labels, not from an input per string — "Clear", "Close dialog", "Go to page 4" and the rest. Per-instance label inputs exist where a single control realistically differs and win over the config. See labels.
  • floatLabel: GogFloatLabelVariant = 'none' | 'in' | 'on' | 'over' (default 'none') on the six field controls: inputfield, textarea, select, multiselect, autocomplete, datepicker. 'in' floats up but stays inside the border, 'on' floats to sit centered on the top border line, 'over' floats fully above the field. Pair with floatLabelShowPlaceholder (default false) to reveal the field's own placeholder once the label has floated clear.
  • clearable (default varies) on inputfield, textarea, select, multiselect, autocomplete, datepicker — shows a clear (×) button once the field has content. Off by default everywhere except gog-multiselect, which had one before the input existed.
  • Generic option accessors, not a fixed DTO. Any collection-driven control (gog-select, gog-multiselect, gog-autocomplete, gog-button-toggle-group) takes your own object shape through optionLabel / optionValue / optionDisabled — each is a property path ('name', dot-paths like 'profile.title' work) or a function (option: T) => TResult. Defaults are 'name' / 'id' / 'disabled'. Set [optionValue]="null" to emit the option object itself instead of a plucked id — the control then round-trips your own object with no lookup table needed:
    html
    <gog-select [options]="members" [optionLabel]="nameOf" [optionValue]="null" [(value)]="member" />
  • Global defaults via GOG_CONFIG / provideGogConfig(...) — see its own section below. Precedence is always: the instance's own input (if set) → GOG_CONFIG → the component's built-in default.
  • Don't bind both a model() and a form directive on the same instance. Every CVA control (checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete, slider, datepicker) exposes its value as both a two-way model() ([(checked)], [(value)]) and, separately, ControlValueAccessor for [formControl]/formControlName. Pick one per instance — wiring both gives the value two competing sources of truth.
  • The custom-content slot pattern. Wherever a component needs custom markup for a specific part of itself, it's an attribute directive read with contentChild(), given a typed context via let- variables — never a plain TemplateRef input, never a string-keyed lookup. Recognize the shape:
    html
    <gog-accordion [items]="items">
      <ng-template gogAccordionHeader let-item let-open="open">{{ item.title }}</ng-template>
    </gog-accordion>
    See the per-component tables below for which slot directives exist on which component.
  • Legacy TemplateRef inputs and string-keyed lookups still exist on a few components and still work, but are @deprecated — do not use them in new code. See Deprecated patterns — do not use in new code.
  • Accessibility is built in, not optional: keyboard navigation (roving tabindex, arrow keys, Home/End), ARIA roles/states, :focus-visible styling, prefers-reduced-motion handling, and WCAG AA contrast are already implemented — you don't need to add any of this yourself, just supply ariaLabel/label inputs where a component has no visible text of its own (icon-only buttons, gog-progressbar, gog-scroll).
  • aria-label on the host tag does nothing. Several components (gog-button chief among them) render their real interactive element (a <button>) inside the component's own host tag. An aria-label attribute placed directly on <gog-button> in a template lands on the custom element wrapper, not on the inner <button>, so assistive tech never sees it — always use the component's own ariaLabel input instead.

Theming

Full model is in README.md's Theming section; short version:

  • Every visual value (color, spacing, radius, shadow, duration) is a --gog-* CSS custom property, layered foundation (--gog-accent-color, --gog-space-md, …, restyles everything) → component (--gog-button-primary-bg, …, one block per component, named after the component's own element) → instance (--gog-button-bg, …, deliberately undeclared escape hatch for one element).

  • --gog-control-boundary-color is the edge that identifies a control (since 21.12.0), and it is not --gog-border-color, which is the decorative hairline for dividers, table rules and panel outlines. gog-chip, gog-toggle and gog-button-toggle read it. A theme sets both.

  • Shadows are an elevation ladder (since 21.12.0): --gog-elevation-0 … -5, Z doubling 0/1/2/4/8/16. Step 1 is a thumb riding on a control, 2 an elevated card or panel, 3 anything anchored to a control (dropdown panel, tooltip, menu), 4 a toast, 5 a modal dialog. The steps are generated from ten per-theme knobs (--gog-elevation-ink, the two alphas, -contact-blur, the three per-Z multipliers -key-x/-key-y/-key-blur that carry the style, -ring-width, and the two -highlight-*). A theme declares all ten or none — they inherit, so a partial set borrows the enclosing theme's weight. --gog-panel-shadow, --gog-dialog-shadow, --gog-toast-shadow, --gog-menu-shadow, --gog-toggle-thumb-shadow and the *-elevated-shadow pair are still the names to override for one surface; their default is now a step. Never hand-write a shadow in a theme block — npm run check:elevation fails on it.

  • Foundation includes a small character layer (since 21.7.0, docs/themes.md iteration 1): --gog-radius (corner rounding), --gog-control-border-*/--gog-panel-border-*/--gog-border-* (border weight — form fields, raised surfaces, everything smaller and inline, respectively), --gog-text-transform/--gog-letter-spacing (emphasis casing/tracking). Component tokens in the categories these cover derive from them by default; setting one in a [data-theme] block restyles every component that reads it, with nothing to re-list per component.

  • The type scale is --gog-text-xs | sm | md | lg | slg | xl | 2xl | 3xl. slg (1.25rem) fills the gap between lg and xl and is named for the control size that needed it. Every component font size that is one of these reads the token, so retuning the scale retunes the library; the handful that do not are off-scale on purpose (an 11px chip, the accordion chevron's px ramp, the toggle's own micro-ramp).

  • Weight is --gog-font-weight-medium | semibold | bold | heavy (500/600/700/900). Every component weight reads one of them, so a lighter or heavier house style is four declarations.

  • --gog-z-base moves the whole stacking order. Badge +1, toast +100, dropdowns, dialogs and menus +300, tooltip +400, the blocking spinner overlay +8000. Set the base to lift the library above your own chrome without disturbing its internal order.

  • --gog-density is the character layer for spacing (since 21.7.0, docs/themes.md iteration 6). It multiplies the ten-step scale --gog-space-4 … --gog-space-48, named for their pixel value at density 1, and every padding and gap in the library derives from a step. --gog-density: 0.9 in a [data-theme] block makes the whole library tighter; nothing else needs to be named. --gog-space-xs|sm|md|lg|2xl are aliases for steps 4/8/16/24/48 and still work. Every step is a multiple of 4 (since 21.11.0): the five 2px-granular steps came out once the last of their 102 readers moved, so "on the grid" is a fact about the scale rather than a habit. Three lengths stay off it on purpose and say so in their own comments — a toggle thumb's inset, a scrollbar thumb's, and the resize grip's hairline gap — because a length inside a single painted mark defines that mark's shape rather than spacing two things apart. Icon offsets, dropdown panel gaps, error-line offsets and the badge's overhang follow density; the glyph box, the focus-ring offset, the float-label reserve and the scrollbar/toggle thumb insets deliberately do not — those are legibility or geometry fitted to a fixed-width track, not spacing. Since 21.9.0 the split is enforced rather than trusted: check-tokens rule H fails the build on a length token that restates a scale step's value as a bare literal, with the three exceptions named in the script.

  • Component prefixes are spelled out since 21.5.0: --gog-button-*, --gog-multiselect-*, --gog-confirmation-dialog-*. The abbreviated --gog-btn-*, --gog-ms-* and --gog-confirm-* were removed in 21.7.0 — if you're reading a codebase or an example that still uses one, rename it; it no longer resolves. The exception is --gog-input-*, which is not an abbreviation: it is the shared text-field block that gog-inputfield and gog-textarea both render, and it keeps that name.

  • The package does not need the app's box-sizing reset (since 21.6.0): utilities.css sets border-box on every element carrying a gog-* class, including the ones the library puts on a consumer's own element. Do not add a reset "so the components line up" — they already do, and a * { box-sizing: content-box } in an app is the only thing that undoes it.

  • Theme switch is a data-theme attribute, usually on <html>, toggled through the ThemeService (inject(ThemeService).setTheme('dark') / .toggleTheme() / .theme signal). Ships light and dark, plus nine importable presets at @guildofgleks/ui/styles/presets/<name>.css. All nine set palette and character (since 21.7.0 — before it, three were palette-only, which made them recoloured defaults):

    Preset Radius Density Identity
    slate 12px 1.05 soft modern — hairline borders, roomy
    one-dark/one-light 4px 0.9 editor chrome; identical character, two tones
    material 4px 1.1 Material Design 3, pill buttons
    primeng 6px 0.95 PrimeNG Aura
    ledger 0 0.9 administrative — hard offset shadow, no motion
    terminal 0 0.85 green phosphor, monospaced throughout, no motion
    bevel 0 0.9 early-web desktop — outset/inset borders
    parchment 0 1.1 ink on paper — old-style serif, oxblood

    material, primeng and bevel also set a few genuinely per-component things the character layer has no vocabulary for (a pill button, a table's header font, a button bevel that has to disagree with a field's); see their own file headers.

  • A preset never makes a network request. Each sets a font stack resolving to a real system face. Where a webfont is worth offering, it is a separate opt-in file — terminal.fonts.css (IBM Plex Mono), parchment.fonts.css (EB Garamond) — imported after the preset, since it re-points the same tokens and later wins. Do not add an @import url(…) to a preset itself; put it in a companion file, or the import becomes a download nobody asked for.

  • Restyle one instance without touching a theme: <gog-button style="--gog-button-bg: #ff4edb">.

  • Build a custom theme by declaring a palette and a character against a new data-theme value (see README.md's Theming section for the full worked example) — component tokens re-derive automatically, you don't restate them.

Right-to-left

Supported since 21.5.0. dir="rtl" on <html> or on any wrapper mirrors every component — you write nothing per component. Portaled overlays (select/multiselect panels, tooltip bubbles) copy a scoped dir onto themselves, so an RTL region inside an LTR page works too.

Physical by design, in both directions: gogTooltip [position]="'left' | 'right'" and ToastConfig.position ('top-right', …). Use the tooltip's 'auto' for direction-aware placement; a toast corner is a deliberate choice, so it is not mirrored.

Global configuration — GOG_CONFIG / provideGogConfig(...)

For the handful of inputs an app typically wants to set once (a size for every form control, a locale for every datepicker) rather than repeat on every instance:

ts
import { provideGogConfig } from '@guildofgleks/ui';

bootstrapApplication(App, {
  providers: [
    provideGogConfig({
      control: { size: 'sm', errorDisplay: 'auto', clearable: true },
      dropdown: { appendToBody: true, filter: true },
      datepicker: { locale: 'de-DE', firstDayOfWeek: 1, format: 'dd.MM.yyyy' },
      toast: { position: 'top-right', duration: 4000 },
    }),
  ],
});

Precedence, always: instance input → GOG_CONFIG → component's built-in default. A nested provideGogConfig(...) (in a route's or component's own providers) layers onto the parent's config, one level deep per key — it does not replace it.

Key Fields Applies to
control size, errorDisplay, clearable size: button, [gogButton], button-toggle-group, checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete, datepicker. errorDisplay: inputfield, textarea, select, multiselect, autocomplete, datepicker, radio-group, slider. clearable: inputfield, textarea, select, multiselect, autocomplete, datepicker. Not table/accordion/paginator (density, not form size), not spinner/skeleton/tag/chip.
dropdown appendToBody, direction, filter, filterPosition, virtualize gog-select, gog-multiselect. gog-datepicker/gog-autocomplete honour appendToBody/direction too (autocomplete has no filter box — it filters via the trigger's own text). virtualize reaches all three dropdowns; not gog-table.
floatLabel variant, showPlaceholder inputfield, textarea, select, multiselect, autocomplete, datepicker.
datepicker locale, firstDayOfWeek, format gog-datepicker, gog-calendar.
autocomplete searchDebounce, minLength, openOnFocus gog-autocomplete.
tooltip position, showDelay, hideDelay the gogTooltip directive.
spinner component, variant every spinner the library draws — gog-spinner, gog-spinner-overlay, and the ones inside gog-button, gog-autocomplete and gog-table, which have no input of their own. component takes your component and renders it in place of the built-in look. The overlay honoured neither key until 21.10.0, and gog-table was simply never listed.
scroll autoHide, hideDelay, size, overscrollBehavior, showTrack, horizontalWheel gog-scroll (and every component that uses one internally).
button debounce gog-button.
ripple enabled the press ripple on gog-button, [gogButton], gog-button-toggle-group, gog-chip, gog-tabs, gog-accordion, gogCollapsibleTrigger, gogMenuItem and the gog-select/gog-multiselect/gog-autocomplete options. Off by default. Each of those takes a ripple input that wins over it. Not the gogRipple directive — writing that attribute is already the per-element decision.
inputfield showSpinButtons gog-inputfield.
textarea resize gog-textarea.
paginator showPageSizeSelect, pageSizeOptions gog-paginator, and through it gog-table's built-in pagination.
toast position, duration ToastService.
theme storageKey, defaultTheme, followSystem, lightTheme, darkTheme ThemeService. All off/neutral by default — see below.
labels every fixed string the library renders — see below inputfield, textarea, select, multiselect, autocomplete, datepicker, calendar, paginator, table, DialogService, ToastService.

Anything visual does not belong here — override the --gog-* token instead.

labels — translating the library

Every string a component renders that the consumer never writes markup for. An app that isn't in English sets these once rather than on every control:

ts
provideGogConfig({
  labels: {
    clear: 'Löschen', // inputfield / textarea clear button
    clearSelection: 'Auswahl löschen', // select / multiselect / autocomplete
    clearDate: 'Datum löschen', // datepicker
    selectAll: 'Alle auswählen', // multiselect panel
    clearAll: 'Alle löschen', // multiselect panel
    increment: 'Erhöhen', // number spin buttons
    decrement: 'Verringern',
    showPassword: 'Passwort anzeigen',
    hidePassword: 'Passwort verbergen',
    closeDialog: 'Schließen',
    closeToast: 'Schließen',
    closeAlert: 'Meldung schließen', // gog-alert's dismiss button
    pagination: 'Seitennavigation',
    previousPage: 'Vorherige Seite',
    nextPage: 'Nächste Seite',
    openCalendar: 'Kalender öffnen',
    togglePanel: 'Bereich umschalten', // gog-panel's toggle, only when it has no heading
    rowsPerPage: 'Zeilen pro Seite', // gog-paginator's size select
    total: 'Gesamt', // gog-table's row-count label
    tablePagination: 'Tabellennavigation',
    selectRow: 'Zeile auswählen',
    selectAllRows: 'Alle Zeilen auswählen',
    today: 'Heute',
    thisMonth: 'Aktueller Monat',
    previousMonth: 'Vorheriger Monat',
    nextMonth: 'Nächster Monat',
    previousYear: 'Vorheriges Jahr',
    nextYear: 'Nächstes Jahr',
    hours: 'Stunden',
    minutes: 'Minuten',
    seconds: 'Sekunden',
    // The one non-string field: it interpolates the page number, and word order and
    // agreement around a number vary by language, so it takes a formatter.
    page: (page, isCurrent) => (isCurrent ? `Seite ${page}, aktuell` : `Zu Seite ${page} wechseln`),
  },
});

Strings that describe one control rather than library chrome — gog-checkbox's ariaLabel, gog-button's ariaLabel, any field's label/placeholder — are deliberately not here. Those stay per instance. Where a per-instance label input exists (clearAriaLabel, todayLabel, …) it still wins over the configured value.

Services

ThemeService

ts
private readonly theme = inject(ThemeService);
this.theme.theme();          // Signal<string>, READ-ONLY — current data-theme
this.theme.setTheme('dark'); // any theme name, including a custom one you declared in CSS
this.theme.toggleTheme();    // flips between the configured light and dark names

theme is read-only on purpose: writing to it would move the signal without touching the data-theme attribute the styles actually read. Never suggest theme.set(...) — it does not exist.

Zero-config behaviour: adopt whatever data-theme is already on <html>, else 'light'. Persistence and following the OS setting are opt-in, so upgrading cannot change which theme an existing app opens in:

ts
provideGogConfig({
  theme: {
    storageKey: 'app-theme', // persist the choice in localStorage; unset = no persistence
    followSystem: true, // open in the OS prefers-color-scheme, and keep following it
    // until the app calls setTheme/toggleTheme
    lightTheme: 'light', // the two names followSystem maps to and toggleTheme alternates
    darkTheme: 'one-dark', // between
    defaultTheme: 'light', // used when nothing else decides
  },
});

Resolution order at startup: existing data-theme on the document → persisted value → OS setting (if followSystem) → defaultTheme → 'light'.

ToastService

Root-provided singleton. Requires a <gog-toast-container /> placed once in your app (see gog-toast below — it is not wired up automatically).

ts
private readonly toast = inject(ToastService);

this.toast.success('Saved');
this.toast.error('Could not save', {
  isSticky: true,
  actions: [{ label: 'Retry', onClick: () => this.save() }],
});
// also: .warning(msg, config?), .info(msg, config?), .show(config), .dismiss(id), .dismissAll()

ToastConfig: { message, type?, iconName?, iconTemplate?, actions?, dedupeKey?, isSticky?, duration?, position? }. Repeated calls with the same (explicit or inferred) dedupeKey replace the existing toast in place instead of stacking a duplicate.

DialogService

Import from @guildofgleks/ui/dialog — DialogService, DIALOG_DATA, DIALOG_REF, DialogRef, DialogConfig and the dialog components. The root does not export them (it did, deprecated, until 21.13.0), which is what lets a route that loads them lazily keep their code out of the initial bundle. Their dependencies from the root — buttons, icons, scroll, and for the table the paginator and select — still land wherever the root does.

Root-provided singleton, imperative dynamic-component dialogs. Requires a <gog-dialog /> placed once in your app (see gog-dialog below — also not automatic).

ts
private readonly dialogService = inject(DialogService);

async confirmDelete(): Promise<void> {
  const handle = this.dialogService.open<boolean>({
    component: ConfirmationDialogComponent, // or your own component
    title: 'Delete this item?',
    role: 'alertdialog',
    data: { message: 'This cannot be undone.' },
  });
  const confirmed = await handle.afterClosed; // boolean | undefined
}

DialogConfig<TData>: { title?, component, data?: TData, modal? (default true), closable?, draggable?, closeIconName?, closeIconTemplate?, width?, maxWidth?, role? ('dialog' default | 'alertdialog'), zIndex? }. open<TResult, TData>() returns { close(result?), afterClosed: Promise<TResult | undefined> }. Also: closeAll(result?), updatePosition(id, offsetX, offsetY) (for draggable dialogs).

open<TResult, TData>() type-checks data against TData when you supply both type arguments — supplying only TResult (the common case above) leaves TData as unknown, exactly as before:

ts
interface EditUserData {
  userId: string;
}

const handle = this.dialogService.open<{ saved: boolean }, EditUserData>({
  component: EditDialogComponent,
  data: { userId: user.id }, // checked against EditUserData here
});

This checks only the call site. EditDialogComponent still reads its data via inject(DIALOG_DATA) — an InjectionToken<unknown> shared by every dialog, so it still needs its own cast (inject<EditUserData>(DIALOG_DATA), shown below). Angular's DI has no way to carry a per-call-site type through one shared token, so the receiving half of the round trip is still on trust — this closes only the half that can be closed.

The library ships a ready-made ConfirmationDialogComponent for yes/no prompts — pass it as component with data: { title, description, confirmText, cancelText }; it resolves the dialog's result to true/false.

Wiring a custom component into a dialog — it reads its data via DIALOG_DATA and closes itself via DIALOG_REF:

ts
import { Component, inject } from '@angular/core';
import { DIALOG_DATA, DIALOG_REF } from '@guildofgleks/ui/dialog';

@Component({ selector: 'app-edit-dialog', template: `…` })
export class EditDialogComponent {
  protected readonly data = inject<{ userId: string }>(DIALOG_DATA);
  private readonly ref = inject(DIALOG_REF);

  save(): void {
    this.ref.close({ saved: true });
  }
}

Component reference

Every component below is exported from @guildofgleks/ui's root — import { X } from '@guildofgleks/ui' — except gog-table, gog-datepicker/gog-calendar and gog-dialog with DialogService, which have their own entry points: @guildofgleks/ui/table, /datepicker and /dialog. They are the three components heavy enough to be worth keeping out of an app's initial bundle, and since 21.14.0 their own entry point is the only place they are exported from — import { TableComponent } from '@guildofgleks/ui' does not compile. "CVA" = implements ControlValueAccessor (works with [formControl]/formControlName).

Buttons & choices

gog-button

Input Type Default Notes
variant GogVariant 'primary'
severity GogSeverity 'accent' what the action means; orthogonal to variant — see below
size GogSize | undefined 'md' via GOG_CONFIG.control.size
disabled boolean false
fullWidth boolean false
type 'button' | 'submit' | 'reset' 'button'
loading boolean false shows an inline gog-spinner, blocks clicks
debounce number | undefined 300 ms; via GOG_CONFIG.button.debounce — see note below
ariaLabel string | null null use this, not a raw aria-label attribute
ariaPressed boolean | 'mixed' | null null toggle button; false renders aria-pressed="false"
ariaExpanded boolean | null null disclosure / popup trigger
ariaControls string | null null id of the controlled element; pairs with ariaExpanded
ariaHasPopup GogAriaHasPopup | null null boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
ripple boolean | undefined false press ripple; via GOG_CONFIG.ripple.enabled

Outputs: gogClick: MouseEvent.

severity says what the action means; variant says how loudly it is drawn (21.9.0). The two are orthogonal, so this is not a fifth variant — it re-points the colours all four are built from, and every combination is real: variant="ghost" severity="danger" is a quiet delete, variant="primary" severity="danger" a loud one. 'accent' is the default and the absence of a claim, so nothing has to opt out of a severity it does not have. GogSeverity is shared with gog-progressbar, whose GogProgressbarVariant is now an alias of it.

html
<gog-button severity="danger" (gogClick)="deleteAccount()">Delete account</gog-button>
<gog-button variant="outline" severity="warning">Discard draft</gog-button>
<a gogButton severity="success" routerLink="/done">Finish</a>

Two colour rules are worth knowing before you override anything. A filled severity button's label is --gog-<status>-text-color, which each theme states for its own hue — material and primeng put near-black on their bright ones, the rest white — and hover and press deepen the fill away from that label (--gog-<status>-shade), so a state always makes the label easier to read rather than harder. A transparent one's label is --gog-button-<status>-ink: the status hue mixed halfway toward the page's ink, because the raw hue is legible body text in only five of the eleven shipped themes. Override --gog-button-<status>-ink if your own theme wants more colour there, and check it: all four severities across all four variants and all their states are gated by npm run check:contrast.

Every ARIA attribute this button needs has an input, and a raw attribute is not a substitute. <gog-button [attr.aria-pressed]="on()"> compiles, throws nothing, and does nothing: the attribute lands on the <gog-button> custom element, which has no role, while the real <button> inside stays unmarked. The failure is invisible — the control looks right and is simply not a toggle to a screen reader. Use [ariaPressed], [ariaExpanded], [ariaControls], [ariaHasPopup] and ariaLabel.

false is not the same as unset. null omits the attribute; false renders aria-pressed="false" / aria-expanded="false", which is what an off toggle or a closed disclosure has to say — a button with no aria-pressed at all is not a toggle button.

A toggle button now looks toggled (21.9.0). aria-pressed="true" (or "mixed") draws an inset ring — --gog-button-<variant>-toggled-shadow, overridable per instance with --gog-button-toggled-shadow. A ring rather than a fill because hover and press already own the background: the state has to survive both, and until 21.9.0 it did not exist at all, so a button could announce itself as on to a screen reader and look identical to an off one. [gogButton] gets the same look from the attribute you write on your own element.

A disabled toggle keeps the ring (21.10.0), dimmed by --gog-button-disabled-opacity like the rest of the button. disabled on a real <button> does not remove aria-pressed, so "on, and unavailable" is announced either way and has to be visible; the rule had excluded :disabled until then, copied from the hover and press rules where the guard belongs. gog-chip's selected ring has always behaved this way, and the two are now the same.

[gogButton] needs none of these inputs. It styles an element you own, so write the ARIA attributes on your own <button>/<a> directly. Same for [gogMenuTrigger], which sets aria-haspopup/aria-expanded/aria-controls on its host — put it on your own <button gogButton>, as its own example shows, not on a <gog-button>.

html
<gog-button [ariaPressed]="mirrored()" (gogClick)="toggleMirror()">Mirror</gog-button>

<gog-button
  [ariaExpanded]="open()"
  ariaControls="filters"
  ariaHasPopup="dialog"
  (gogClick)="open.set(!open())"
  >Filters</gog-button
>

The press is a colour, not only a movement. :active deepens the button's background (and the label where the fill demands it) as well as scaling it by --gog-button-active-scale. Under prefers-reduced-motion: reduce the scale is dropped and the colour stays, so the press is still visible to a reader who has switched animations off — before 21.9.0 that reader got no feedback at all, since the ripple is off by default and is itself suppressed under reduced motion. Override per instance with --gog-button-press-bg / --gog-button-press-color, or per theme with --gog-button-<variant>-active-bg.

Every other pressable surface in the library does the same thing since 21.9.0 — menu items, chips, tab and accordion headers, button-toggle options and the three dropdowns' option rows — each through its own --gog-<block>-press-bg. gogCollapsibleTrigger is the exception: the library paints nothing on that element in any state, because it is yours.

debounce is a spam guard, not a delay before the first click. The first click in a window fires immediately (leading edge); further clicks within debounce ms are silently dropped.

Use (gogClick), never (click), on gog-button. The click handler that drives debounce and emits gogClick is bound on the <button> inside the component's own template, not on the host — a native click still bubbles up through <gog-button>, so a (click) listener written there fires on every press, silently bypassing the debounce entirely. This is specific to the component: [gogButton] on your own <a>/<button> has no debounce to bypass, so (click) on it works exactly as written.

html
<gog-button variant="primary" [loading]="saving()" (gogClick)="save()">Save</gog-button>
<gog-button variant="ghost" ariaLabel="Close" (gogClick)="close()"
  ><gog-icon name="close"
/></gog-button>

gog-button-toggle-group

A row of buttons, single- or multi-select, built from your own option objects.

Input Type Default Notes
options TOption[] []
optionLabel accessor 'name'
optionValue accessor | null 'id' null emits the option object
optionDisabled accessor 'disabled'
optionIcon accessor → GogIconName | null | null null optional leading icon per option
multiple boolean false changes ARIA role entirely — see note
appearance 'joined' | 'separated' 'joined'
orientation GogOrientation 'horizontal'
size GogSize | undefined 'md' via GOG_CONFIG.control.size
disabled, fullWidth, ariaLabel false, false, ''
ripple boolean | undefined false press ripple; via GOG_CONFIG.ripple.enabled

Model: value: TValue | TValue[] | null (single value, or array in multiple mode). CVA: yes. Slot: <ng-template gogButtonToggleOption let-opt let-selected="selected"> for custom button markup. Single mode is a radio group (role="radiogroup", arrows move and select); multiple mode is a toolbar of independent toggles (role="group", arrows only move, Space toggles) — this is a real ARIA distinction, not cosmetic.

html
<gog-button-toggle-group [options]="alignments" [(value)]="align" />
<gog-button-toggle-group [options]="tools" [multiple]="true" [(value)]="activeTools" />

Form fields

gog-inputfield

Input Type Default Notes
label, placeholder string ''
type GogInputType 'text' text/password/email/number/search/tel/url/date/time/datetime-local
readonly boolean false value stays focusable and submitted, edits blocked; hides the clear button and stepper
maxlength, minlength number | null null native attributes
pattern string '' native attribute, regex source
inputMode GogInputMode | null null on-screen keyboard hint (numeric, tel, …)
spellcheck boolean | null null unset = browser default
inputId string '' → generated a real id is always rendered; pass one only to reference the field externally
min, max, step number | null null type="number" only
showSpinButtons boolean | undefined true own +/- glyphs on type="number"; via GOG_CONFIG.inputfield.showSpinButtons
errorMessage, errorDisplay '', 'manual' see conventions
disabled, size, fullWidth false, 'md', true
iconStart / iconEnd GogIconName | '' '' bare leading/trailing icon
clearable, clearAriaLabel false, 'Clear' on type="number" the clear button renders alongside the stepper
floatLabel, floatLabelShowPlaceholder 'none', false
showPasswordLabel / hidePasswordLabel string | undefined 'Show password'/'Hide password' type="password" reveal toggle aria-labels; via GOG_CONFIG.labels
incrementLabel / decrementLabel string | undefined 'Increment'/'Decrement' spin button aria-labels; via GOG_CONFIG.labels

Model: value: string (always a string, even for type="number" — the form control value is number | null, but the [(value)] model mirrors the raw text). CVA: yes.

Slots: project <span gogInputAddonStart>/<span gogInputAddonEnd> (or a <button>) for custom leading/trailing markup — a normal DOM element with its own aria-label, click handler and disabled state, not a component-managed slot. This is the current, non-deprecated replacement for the old icon-template/icon-fn/icon-label input quartet — see Deprecated patterns.

html
<gog-inputfield
  label="Email"
  type="email"
  formControlName="email"
  errorDisplay="auto"
  errorMessage="Enter a valid email"
  [clearable]="true"
/>

<gog-inputfield label="Amount" [fullWidth]="false">
  <span gogInputAddonStart>€</span>
</gog-inputfield>

gog-textarea

Input Type Default
label, placeholder string ''
rows number 4
readonly boolean false
maxlength, minlength number | null null
spellcheck boolean | null null
inputId string '' → generated, same as inputfield
resize GogTextareaResize | undefined ('vertical'|'horizontal'|'both'|'none') 'vertical'; via GOG_CONFIG.textarea.resize
errorMessage, errorDisplay, disabled, size, fullWidth same shape as inputfield
clearable, clearAriaLabel, floatLabel, floatLabelShowPlaceholder same shape as inputfield

Model: value: string. CVA: yes.

html
<gog-textarea label="Notes" formControlName="notes" [rows]="6" resize="vertical" />

gog-select

Extends the shared listbox behaviour (GogDropdownBase) that also backs gog-multiselect and partly gog-autocomplete — placement, the append-to-body overlay, click-outside, keyboard nav, and CVA all come from there. Full shared input surface (documented once, applies to both select and multiselect unless noted otherwise):

Input Type Default Notes
label, ariaLabel, placeholder string '', '', 'Select...'
options TOption[] [] your own objects
optionLabel accessor 'name' path or fn
optionValue accessor | null 'id' null = emit the option object
optionDisabled accessor 'disabled'
clearable, clearAriaLabel false (select) / true (multiselect), 'Clear selection'
minWidth string | null null only with [fullWidth]="false"
filter boolean | undefined false search box in the panel; via GOG_CONFIG.dropdown.filter
filterPlaceholder, filterEmptyMessage string 'Search...', 'No matches'
filterPosition 'top' | 'bottom' | undefined 'top' via GOG_CONFIG.dropdown.filterPosition
filterMatch ((option, query) => boolean) | null null custom matcher, else case-insensitive substring on the resolved label
errorMessage, errorDisplay '', 'manual'
size GogSize | undefined 'md'
dropdownDirection 'auto' | 'up' | 'down' | undefined 'auto'
dropdownZIndex, dropdownWidth, dropdownMaxHeight null only meaningful with appendToBody
appendToBody boolean | undefined false renders the panel into <body> — needed inside a scroll/overflow-clipped container
disabled, fullWidth false, true
floatLabel, floatLabelShowPlaceholder 'none', false
inputId (select/autocomplete only) string ''
ripple boolean | undefined false press ripple; via GOG_CONFIG.ripple.enabled

gog-select-specific: value: model<TValue>(null), and virtualize: boolean | undefined (default false, via GOG_CONFIG.dropdown.virtualize) — see below. virtualize: boolean | undefined (default false, via GOG_CONFIG.dropdown.virtualize) is on gog-select, gog-multiselect and gog-autocomplete alike.

gog-multiselect-specific additions: value: model<TValue[]>([]), showControls: boolean (default false, a select-all/clear row), controlsPosition: 'top'|'bottom' (default 'top'), and selectAllLabel/clearAllLabel for that row's two buttons ('Select all'/'Clear', also via GOG_CONFIG.labels).

CVA: yes, both. Slots (shared): <ng-template gogDropdownChevron> (custom chevron markup), <ng-template gogDropdownOption let-opt let-selected="selected" let-label="label"> (custom option row). Multiselect adds <ng-template gogMultiselectClearIcon>.

virtualize for a list in the thousands, and only when you mean it. Unwindowed, 10 000 options build 10 000 DOM rows to show about six: measured in Chrome that is 512ms before the panel appears, against 21ms windowed, on the same data. Windowed, the DOM holds roughly twenty rows whatever the count is, and the scrollbar is identical because spacers stand in for the rows that are not there.

It is off by default and never switched on at a row-count threshold, which is the same call GOG_CONFIG.ripple.enabled makes: a windowed list behaves differently in ways nothing about the data predicts, so flipping it when enough rows happen to arrive works in development and surprises in production. Set it per field, or app-wide with GOG_CONFIG.dropdown.virtualize.

What changes while it is on:

  • Ctrl+F finds only the rendered rows, and CSS targeting :last-child matches the last rendered row. Those are the two that catch people out.
  • The announced count stays honest. aria-setsize and aria-posinset carry the real list and the real position, so a screen reader is told "10 000 items", not "20".
  • Scrolling a keyboard-focused row out of view hands focus back to the trigger, because the row it was on no longer exists. Escape and ArrowDown both work from there. An unwindowed list never has to do this.
  • Arrow keys move through the whole list rather than the rendered part, so ArrowUp from the trigger still reaches option 10 000.

All three dropdowns take it — gog-select, gog-multiselect and gog-autocomplete. gog-table does not, and [lazy] is not a substitute: that keeps the fetch small and still stamps every row it is handed. The same distinction applies to gog-autocomplete's gogLoadMore, and the two compose — pair them on a long list, because neither implies the other.

On gog-autocomplete the focus caveat above does not apply: it is a combobox, so focus never leaves the text field and the highlight is carried by aria-activedescendant either way.

Turn filter on past about seven options — or order them instead. Choice time grows with the log of the count (T = b · log₂(n + 1)), so beyond roughly seven a panel stops being scanned and starts being read. The escape is not always the filter box: the law governs unordered choices, and a list the reader can predict — alphabetical countries, ascending amounts, a familiar fixed sequence — is one they search rather than choose from, so ordering it well is worth as much as filtering it. Both, for a long list of neither. GOG_CONFIG.dropdown.filter sets this once for the app rather than per dropdown, which is usually the right place for it.

html
<gog-select
  label="Region"
  [options]="regions"
  optionLabel="title"
  [(value)]="regionId"
  [filter]="true"
/>

<gog-multiselect
  label="Tags"
  [options]="tags"
  [(value)]="selectedTagIds"
  [showControls]="true"
  formControlName="tags"
  errorDisplay="auto"
/>

gog-autocomplete

Shares GogDropdownBase too, but the trigger is a real <input> (combobox pattern, aria-activedescendant), not a listbox button — so it does not reuse the base's built-in panel-filter box; it filters/searches off what's typed in the field itself.

Input Type Default Notes
(all the shared GogDropdownBase inputs above except filter/filterPlaceholder/filterPosition)
filterLocal boolean true narrow options client-side as you type; turn off when gogSearch already returns a filtered server list (avoids double-filtering)
minLength number | undefined 1 via GOG_CONFIG.autocomplete.minLength
openOnFocus boolean | undefined true via GOG_CONFIG.autocomplete.openOnFocus
searchDebounce number | undefined 300 ms before gogSearch fires; via GOG_CONFIG.autocomplete.searchDebounce
loading boolean false shows a spinner in the trailing slot
emptyMessage string 'No matches'
forceSelection boolean true see note below
ripple boolean | undefined false press ripple; via GOG_CONFIG.ripple.enabled

Outputs: gogSearch: string (debounced query — wire your server lookup here), gogLoadMore: void (panel scrolled to the end — fetch the next page).

Model: value: TValue | null. CVA: yes.

forceSelection matters. On (default): the field always ends up reflecting a real selection — free-typed text that matches nothing snaps back on blur/Escape. Off: what the user typed is itself meaningful (a create-as-you-type flow) — read the typed text from gogSearch, not from value, since value clears the moment the text stops matching the selection.

html
<gog-autocomplete
  [options]="users"
  optionLabel="profile.fullName"
  [optionValue]="null"
  [(value)]="user"
  [loading]="searching()"
  (gogSearch)="search($event)"
/>

gog-checkbox

Input Type Default
label, ariaLabel string ''
size GogSize | undefined 'md'
indeterminate, disabled, fullWidth boolean false

Model: checked: boolean. CVA: yes. Slot: <ng-template gogCheckboxIcon> for a custom tick icon.

html
<gog-checkbox label="I agree to the terms" formControlName="agree" />

gog-toggle

An on/off switch (role="switch") — semantically different from a checkbox ("is this setting on", not "is this one of the things you selected").

Input Type Default Notes
label, ariaLabel string ''
size GogSize | undefined 'md' via GOG_CONFIG.control.size
disabled, fullWidth boolean false
labelPosition 'start' | 'end' 'end'
onLabel, offLabel string '' text rendered inside the track itself

Model: checked: boolean. CVA: yes.

html
<gog-toggle label="Notifications" formControlName="notificationsOn" onLabel="ON" offLabel="OFF" />

gog-radio-group

Input Type Default
options GogRadioOption[] ({ id, label, disabled? }) []
label, ariaLabel, name string ''
size GogSize | undefined 'md'
disabled, fullWidth boolean false
orientation GogOrientation 'vertical'
errorMessage, errorDisplay '', 'manual'

Model: value: string | number | null. CVA: yes. Fixed { id, label, disabled? } shape (not a generic accessor, unlike select/multiselect/button-toggle).

html
<gog-radio-group
  [options]="[{id:'m',label:'Male'},{id:'f',label:'Female'}]"
  formControlName="gender"
/>

gog-slider

Input Type Default
label, ariaLabel string ''
min, max, step number 0, 100, 1
showValue, showThumb boolean true
errorMessage, errorDisplay '', 'manual'
disabled boolean false
fullWidth boolean true (ignored when orientation="vertical")
orientation GogSliderOrientation 'horizontal'
range boolean false — two thumbs; see below
startDisabled, endDisabled boolean false — range only
startAriaLabel, endAriaLabel string 'Minimum' / 'Maximum', prefixed by label

Models: value: number, and rangeValue: GogSliderRange ({ start: number; end: number }). CVA: yes. Backed by a real <input type="range"> (rotated via writing-mode for vertical), so dragging/touch/keyboard all come from the platform.

html
<gog-slider label="Volume" [min]="0" [max]="100" formControlName="volume" />

Range mode. [range]="true" puts a second thumb on the track and switches which model is live: bind [(rangeValue)] instead of [(value)]. The two are mutually exclusive — value (and a form control's writeValue) is ignored while range is on, and vice versa.

html
<gog-slider label="Price" [range]="true" [(rangeValue)]="price" startAriaLabel="Lowest" />

Each thumb needs its own accessible name, because one <label> cannot be associated with two inputs through for; unset, they fall back to 'Minimum'/'Maximum' prefixed with label ('Price Minimum'). startDisabled/endDisabled pin one end while the other stays movable — they are ORed with disabled rather than overriding it, and unlike it they do not dim the whole control or cut pointer events over the track, which would take the still-enabled thumb with them.

gog-datepicker / gog-calendar

Import from @guildofgleks/ui/datepicker — DatepickerComponent, CalendarComponent, GogCalendarDay and GogDatepickerValue; the date helpers (formatDate, parseDate, …) and GogDateRange stay in the root. The root does not export them (it did, deprecated, until 21.13.0), which is what lets a route that loads them lazily keep their code out of the initial bundle. Their dependencies from the root — buttons, icons, scroll, and for the table the paginator and select — still land wherever the root does.

gog-datepicker is a field + panel; gog-calendar is the month grid alone (what inline mode renders). Native Date only — no date library, no adapter.

Input Type Default Notes
inputId, label, ariaLabel, placeholder string ''
selectionMode GogDateSelectionMode ('single'|'range') 'single'
min, max Date | null null
disabledDates ((date: Date) => boolean) | null null predicate, not a list
defaultMonth Date | null null which month opens when nothing is selected
numberOfMonths number 1 2 is what makes a range picker usable
showTime, hourFormat, minuteStep, showSeconds false, '24', 1, false
showTodayButton boolean true selects today
showThisMonthButton boolean false only moves the view, leaves selection alone
format string | null null display/parse pattern ('dd.MM.yyyy'); derived from showTime when unset
locale string | undefined 'en-US' via GOG_CONFIG.datepicker.locale
firstDayOfWeek number | undefined locale's own via GOG_CONFIG.datepicker.firstDayOfWeek
allowTextInput boolean true typed text parsed against format; unparseable drafts don't clear the value
inline boolean false renders the calendar with no field/panel
disabled, fullWidth false, true
clearable, clearAriaLabel false, 'Clear date'
errorMessage, errorDisplay, size '', 'manual', 'md'
floatLabel, floatLabelShowPlaceholder 'none', false
appendToBody, dropdownDirection, dropdownZIndex false, 'auto', null

Model: value: Date | GogDateRange | null (GogDateRange = { start: Date | null; end: Date | null }). CVA: yes.

gog-calendar (usable standalone) takes most of the same date/range/time inputs directly, plus gogDateSelect: output<GogDatepickerValue>() fired only on a complete selection. It resolves locale and firstDayOfWeek from GOG_CONFIG.datepicker itself, so a standalone calendar honours an app-wide locale without being handed one; its navigation, shortcut and time labels (todayLabel, thisMonthLabel, previousMonthLabel, nextMonthLabel, previousYearLabel, nextYearLabel, hoursLabel, minutesLabel, secondsLabel) resolve through GOG_CONFIG.labels the same way.

Also exported for direct reuse: formatDate(date, pattern), parseDate(text, pattern), and a family of date-math helpers (addDays, addMonths, isSameDay, isWithinBounds, …) from date-utils.

Sizing. gog-calendar caps itself at its own month grid — you do not need to give it a width. --gog-calendar-max-width (default max-content) is the cap, and it covers the size variants, numberOfMonths, showTime and wider locales on its own; set it to 100% for a calendar that fills its container. This is also what sizes inline mode, because inline is gog-calendar with a border and nothing else. The dropdown panel is separate: --gog-datepicker-panel-width, also max-content.

html
<gog-datepicker label="Birth date" [(value)]="birthDate" [max]="today" />
<gog-datepicker selectionMode="range" [(value)]="stayRange" [numberOfMonths]="2" />

Display, feedback & status

gog-icon

Input Type Default
name GogIconName 'close'
template TemplateRef | null null — custom markup instead of the built-in SVG
title string ''
ariaHidden boolean true

The package ships 41 glyphs (GogBuiltinIconName), all from Lucide and inlined so the package keeps zero runtime dependencies:

Group Names
Chevrons & arrows chevron-up, chevron-down, chevron-left, chevron-right, arrow-left, arrow-right
Confirm & dismiss check, close, checkbox, checkbox-checked
Status success, error, warning, info
Sorting sort, sort-up, sort-down, filter
Actions search, plus, minus, trash, pencil, copy, download, upload, refresh, external-link
Chrome menu, more-horizontal, more-vertical, settings
Objects & state user, lock, mail, calendar, clock, eye, eye-off, star, star-filled

star / star-filled is the one outline/filled pair, for a rating or favourite toggle — the same reason checkbox / checkbox-checked exists. The set is otherwise outline-only on purpose; a blanket solid duplicate of every glyph would double the payload for a distinction almost nothing needs. If you want a filled variant of something else, register it with provideGogIcons.

Object.keys(ICON_DEFS) is the runtime list, if you need to enumerate them (an icon picker, a gallery). Do not hand-copy the names into an array — that is what goes stale.

html
<gog-icon name="calendar" />
Registering your own icons — provideGogIcons(...)

name is typed GogIconName = GogBuiltinIconName | (string & {}): the built-ins autocomplete, and any name you register is accepted. This is the supported way to use your own icon set — prefer it over the template input, which costs an <ng-template> at every use site and is for one-offs.

ts
// app.config.ts
import { provideGogIcons } from '@guildofgleks/ui';

providers: [
  provideGogIcons({
    cart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
    rocket: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
  }),
];
html
<gog-icon name="cart" />
<gog-tag iconName="cart">In basket</gog-tag>
<!-- works anywhere an icon *name* is taken -->
  • Registered names win over built-ins of the same name — that is how you replace the library's checkmark or chevrons across every component at once, without touching any of them.
  • Nested provideGogIcons(...) layers onto the parent set rather than replacing it, the same as provideGogConfig: a lazy route can register only what it uses.
  • An unknown name renders nothing and warns in dev mode; it never throws. An icon is decoration — failing the render over a typo would be the worse outcome.
  • GOG_ICONS is the InjectionToken<Readonly<Record<string, string>>> behind it, exported for the one case provideGogIcons does not cover: reading the registered set back (inject(GOG_ICONS)) to enumerate it in an icon picker. Provide it through provideGogIcons(...) rather than directly — the helper is what layers a child injector's icons onto the parent's instead of replacing them.
  • Write the SVG for inheritance: a viewBox, stroke="currentColor" (or fill), and no width/height — gog-icon drives size and stroke width from the --gog-icon-* tokens, so a registered icon scales and colours like a built-in.
  • Security: the markup is inserted with bypassSecurityTrustHtml (Angular's HTML sanitizer strips SVG, so there is no alternative). That is fine for static icon markup you authored; never build a registered icon string from user input or fetch it at runtime unsanitized.

gog-button renders its own <button>, so it can never be a link. [gogButton] inverts that: the element stays yours and the directive only gives it the look.

html
<a gogButton routerLink="/pricing">See pricing</a>
<a gogButton variant="ghost" href="https://example.com" target="_blank" rel="noreferrer">Docs</a>
<button gogButton variant="outline" size="sm" type="submit">Save</button>
<a gogButton fullWidth routerLink="/checkout">Checkout</a>
Input Type Default
variant GogVariant 'primary'
severity GogSeverity 'accent'; same as gog-button
size GogSize | undefined 'md'; via GOG_CONFIG.control.size
fullWidth boolean (bare attr ok) false
ripple boolean | undefined false; via GOG_CONFIG.ripple.enabled

Selector is a[gogButton], button[gogButton] — deliberately not a bare [gogButton], because on a <div> the result looks like a button and is invisible to the keyboard and to assistive tech.

Which to reach for. gog-button for a button that acts on the page: it owns loading (a centred spinner it projects), debounce click throttling and the gogClick output, none of which a bare element can provide. [gogButton] when the element must be a link, or when you need to keep directives of your own on it — routerLink, href, target, download, type="submit" and anything else keep working because they were never brokered through an input in the first place. That is also why the library still has no @angular/router dependency.

Two things it deliberately does not do: no disabled on an <a> (there is no such thing — drop the href or render a real <button>), and no loading state (the spinner is a projected child a directive cannot add without taking over the element's content).

[gogBadge] — directive, not a component

Decorates an existing element (a button, an icon, an avatar) with a count/status dot — it never wraps its host.

Input Type Default
gogBadge string | number | null null — the content
badgePosition GogBadgePosition ('top-end'|'top-start'|'bottom-end'|'bottom-start') 'top-end'
badgeVariant GogTagVariant 'danger'
badgeDot boolean false — bare dot, no text
badgeMax number 99 — beyond this, renders N+
badgeHidden boolean false
badgeAriaLabel string ''

Renders nothing when the value is 0, null or empty and badgeDot is off — "0" badges are impossible by design.

html
<gog-button gogBadge="12" badgeAriaLabel="12 unread">Inbox</gog-button>
<gog-icon name="info" gogBadge badgeDot />

gog-chip

Input Type Default
size GogSize 'md'
shape GogTagShape ('rounded'|'pill') 'rounded'
disabled, clickable boolean false, true
selected boolean | null (two-way) null — see below
removable boolean false
fullWidth boolean false
ariaLabel, removeAriaLabel string '', 'Remove chip'
avatarUrl, avatarAlt string | null / string null, ''
iconName GogIconName | null null
ripple boolean | undefined false; via GOG_CONFIG.ripple.enabled

Outputs: gogClick: MouseEvent | KeyboardEvent, gogRemove: void.

html
<gog-chip [avatarUrl]="user.photo" [removable]="true" (gogRemove)="removeUser(user)"
  >{{ user.name }}</gog-chip
>

selected makes it a filter chip (21.9.0) — a chip you toggle on and off rather than press. It is tri-state, and null is the default so nothing about an existing chip changes: no aria-pressed, no selected look, activation only emits gogClick. Set it to false and the chip is a toggle that is off (aria-pressed="false" — a chip with no aria-pressed at all is not a toggle to a screen reader, so "off" has to be stated); true and it is on, which draws an inset ring from --gog-chip-selected-shadow. A ring rather than a fill because :hover and :active already own the chip's background and the selection has to survive both.

It is a two-way model, so the chip flips it on click, Enter and Space — a row of filters needs no click handler:

html
@for (f of filters; track f.label) {
<gog-chip [(selected)]="f.on">{{ f.label }}</gog-chip>
}

gogClick still fires, after the flip, so a handler reading selected() sees the new value. Drive the state from that handler instead and you want a one-way [selected], or the two writes cancel out. A disabled chip keeps the ring but drops aria-pressed, which needs the role="button" a disabled chip does not carry — "selected, and currently unavailable" is a real state and hiding it would leave it announced and invisible.

gog-alert

A persistent, in-flow message — the one gog-toast cannot be. No timer, no queue, no overlay, no service: it renders where you write it and stays until your app removes it.

Input Type Default
severity GogSeverity 'accent'
heading string | undefined undefined
dismissible boolean false
iconName GogIconName | null | undefined undefined
live GogAlertLive | undefined from severity
Output Type When
dismissed void the close button was pressed

Slot: <ng-template gogAlertIcon> for custom icon markup. Body is projected content.

html
<gog-alert severity="danger" heading="Payment failed" [dismissible]="true" (dismissed)="hide()">
  The card issuer declined the charge. No money has left your account.
</gog-alert>

dismissed means pressed, not removed. The alert stays in the DOM and your app decides what happens — hide it, retry, navigate. A component that deleted itself would take the focused element with it and drop a keyboard reader back onto <body>.

The severity picks both the edge colour and the glyph (success/error/warning/info; 'accent' borrows info's, because it claims nothing). iconName overrides the glyph and [iconName]="null" removes it for a message whose words already carry the meaning.

How it announces. live is 'assertive' | 'polite' | 'off', defaulting from the severity — danger and warning interrupt, the rest wait. Set 'off' for a message that is already on the page when it loads: that is the commonest case and the one the default gets wrong, because a reader arriving at a page does not need it interrupted about something that was already there.

The component cannot tell those apart for you, and that was measured rather than assumed: @angular/core exposes no stability member a component can read synchronously at construction, and afterNextRender reports its own first render, which every alert has whenever it mounts.

The announcement lives in a separate visually-hidden region, empty until one render after the alert mounts, which is the only way it works: a live region filled in the same pass as its own creation announces nothing — the trap gog-toast-container's permanently-mounted regions exist to avoid. Do not "simplify" this by putting aria-live on the alert itself.

GOG_CONFIG.labels.closeAlert names the dismiss button.

gog-tag

Input Type Default
variant GogTagVariant 'info'
size GogSize 'md'
shape GogTagShape 'rounded'
iconName GogIconName | null null
fullWidth boolean false

Slot: <ng-template gogTagIcon> for custom icon markup.

html
<gog-tag variant="success">Active</gog-tag>

gog-spinner / gog-spinner-overlay

Input Type Default
size GogSize 'md'
variant GogSpinnerVariant ('runic'|'ring'|'custom') unset — see below
ariaLabel string 'Loading'
overlay (spinner only) boolean false
loading (spinner-overlay only) boolean false — toggles the overlay + aria-busy

variant="custom" renders your own projected markup, still inheriting the size wrapper and --gog-spinner-color theming.

To replace the spinner everywhere at once, pass a component to GOG_CONFIG — including the three places you cannot reach with an input: gog-button's and gog-autocomplete's loading states, and the spinner gog-table draws in place of its rows.

ts
provideGogConfig({ spinner: { component: HouseLoaderComponent } });

It renders inside the same size wrapper as the built-ins, so it keeps the sizing, the overlay behaviour, role="status" and the accessible name — only the visual is yours. An instance's own variant still wins over it, so <gog-spinner variant="ring"> is a ring in an app that has set a component: a default does not overrule something asked for explicitly.

Neither component's variant has a default value, and on gog-spinner-overlay that is the whole of the 21.10.0 fix: the overlay forwards its variant to the spinner it wraps, so a default there would have been an instance overruling the config on every overlay ever rendered — which is exactly what happened before, leaving the one spinner that covers a whole region on the built-in look while every other spinner in the app was the house one. size and ariaLabel keep their defaults: neither has a config key to fall through to.

html
<gog-spinner-overlay [loading]="isLoading()">
  <app-content-that-loads />
</gog-spinner-overlay>

gog-skeleton

Input Type Default
shape GogSkeletonShape ('text'|'circle'|'rect') 'text'
size GogSize 'md'
animation GogSkeletonAnimation ('pulse'|'wave'|'none') 'pulse'
width, height string | null null
lines number 1 — shape="text" only, last line renders shorter
rounded boolean true
ariaLabel string | null null — decorative (no role) unless set
html
<gog-skeleton shape="text" [lines]="3" /> <gog-skeleton shape="circle" width="48px" />

gog-progressbar

Input Type Default
value, buffer number (0–100, clamped) 0
mode GogProgressbarMode ('determinate'|'indeterminate'|'buffer') 'determinate'
variant GogProgressbarVariant ('accent'|'success'|'danger'|'warning'|'info') 'accent'
size GogSize 'md'
showValue boolean false
ariaLabel string ''
html
<gog-progressbar mode="indeterminate" ariaLabel="Loading" />
<gog-progressbar mode="buffer" [value]="42" [buffer]="70" />

The fill's end is marked by two hairlines (21.10.0), --gog-progressbar-edge-color over --gog-progressbar-edge-backing-color, each --gog-progressbar-edge-width wide. That boundary is the value — showValue is off by default — and the fill and the track cannot carry it themselves: in every shipped theme the five fills straddle mid-luminance, so no one track colour clears WCAG 1.4.11's 3:1 against all of them. Two tones always do, and check:contrast gates the pair. Retint them per theme if you like; keep them a pair whose tones sit on opposite sides of the middle, or the marker disappears on whichever fill it happens to match.

gog-divider

Input Type Default
orientation GogOrientation 'horizontal'
variant GogDividerVariant ('solid'|'dashed'|'dotted') 'solid'
inset boolean false

Label is projected content, not an input — put an icon or a gog-tag inside it if needed.

html
<gog-divider>OR</gog-divider>

gogRipple — directive, not a component

A pointer-position wash that grows from where you pressed and fades when you let go. Drop it on any element you already have — it adds no wrapper and changes no layout.

Input Type Default
rippleDisabled boolean false
rippleCentred boolean false — start from the middle instead of from the pointer
html
<button gogRipple>Press me</button>
<div gogRipple rippleCentred class="tile">A tile</div>

Four things suppress it, none of which you have to wire up: rippleDisabled, a host carrying disabled, a host carrying aria-disabled="true", and prefers-reduced-motion: reduce — the last one suppressed outright, not shortened. Keyboard activation (Enter/Space) is always centred, because a key press carries no coordinates.

Put it on the element that paints the surface. The wash lives in its own layer that clips itself — the host is never given overflow: hidden, so a gogBadge on the same element is not clipped — and that layer takes its corner radius from its host with border-radius: inherit. On a wrapper whose child paints the rounded background, the layer inherits the wrapper's radius (very often 0) and the wash squares off at the corners.

Tokens: --gog-ripple-color (currentColor, so the wash reads as the surface's own foreground on a filled surface and a ghost one alike), --gog-ripple-opacity, --gog-ripple-enter-duration, --gog-ripple-exit-duration, --gog-ripple-easing. All five are ordinary inherited custom properties, so setting one anywhere above the host is the per-instance override.

Turning the ripple on for the library's own components

You do not add gogRipple to a gog-* component: each one already owns the element that paints its surface, so it wires its own. What you do is switch it on, once:

ts
provideGogConfig({ ripple: { enabled: true } });

That covers gog-button, [gogButton], gog-button-toggle-group, gog-chip, gog-tabs headers, gog-accordion headers, gogCollapsibleTrigger, gogMenuItem, and the options inside gog-select / gog-multiselect / gog-autocomplete. gog-paginator follows because its page buttons are gog-buttons.

Off by default, so adding the ripple to the library changed the look of nothing. Every one of those takes a ripple input that beats the config in both directions: [ripple]="false" opts one control out of an app-wide on, [ripple]="true" opts one in without switching the app over.

Not covered, and deliberately: gog-table rows and gogCardLink. A row and a card are hundreds of pixels wide, so the wave has to travel the whole surface and reads as a flash rather than as feedback at the point you pressed. An interactiveRows row answers a press with a colour instead, --gog-table-row-press-bg, which also survives prefers-reduced-motion. Put gogRipple on them yourself if you disagree.

A chip that is not clickable, or is disabled, never ripples whatever the config says: a label answering a press is a promise it cannot keep.

gogTooltip — directive, not a component

Drop on any element — a gog-* component's host tag or a plain native one.

Input Type Default
gogTooltip string | TemplateRef | null null — content
gogTooltipPosition GogTooltipPosition ('auto'|'top'|'bottom'|'left'|'right') 'auto'; via GOG_CONFIG.tooltip.position
gogTooltipShowDelay number | undefined 300; via GOG_CONFIG.tooltip.showDelay
gogTooltipHideDelay number | undefined 100; via GOG_CONFIG.tooltip.hideDelay
gogTooltipDisabled boolean false
gogTooltipClass string '' — class on the bubble itself, since it's portaled to <body>
html
<button gogTooltip="Save changes">💾</button> <gog-chip [gogTooltip]="hintTemplate">Beta</gog-chip>

Layout & navigation

gog-accordion

Input Type Default
items GogAccordionItem[] ({ id, title, disabled?, [key: string]: unknown }) []
size GogSize 'lg' (not 'md' — see conventions)
expandFirst, multi, loading boolean false
skeletonCount number 3 — rows shown while loading and items is still empty
showChevron boolean true
headingLevel 2|3|4|5|6 | undefined undefined — wraps headers in role="heading" when set
ripple boolean | undefined false; via GOG_CONFIG.ripple.enabled

Model: openIds: ReadonlySet<string | number>. Output: gogToggle: { item, open }.

Slots: <ng-template gogAccordionHeader let-item let-open="open">, <ng-template gogAccordionContent let-item>, <ng-template gogAccordionChevron let-item let-open="open">. This is the library's canonical example of the slot pattern — copy its shape for anything similar.

html
<gog-accordion [items]="faqItems" [multi]="true">
  <ng-template gogAccordionContent let-item>{{ item.answer }}</ng-template>
</gog-accordion>

gog-collapsible + gogCollapsibleTrigger / gogCollapsibleContent

Headless primitive — owns no markup at all, just open/close state plus two attribute directives you place on your own elements. Use this when gog-accordion's opinionated markup doesn't fit (e.g. a sidebar nav group).

Input (on gog-collapsible) Type Default
disabled boolean false
collapseOnFocusOut boolean false — close once focus leaves both trigger and content

Model: open: boolean.

html
<gog-collapsible [(open)]="isOpen">
  <button gogCollapsibleTrigger>Advanced options</button>
  <div gogCollapsibleContent>
    <!-- any markup -->
  </div>
</gog-collapsible>

The trigger can be any element. On a <button> or <a href> the directive adds only the ARIA wiring, because the browser already handles focus and keys. On anything else — a <div>, a <span> — it also supplies role="button", tabindex="0" and Enter/Space, so the control it announces is one a keyboard can actually reach. If you set role or tabindex yourself, the directive leaves both alone: you have said what the element is.

gogCollapsibleTrigger takes a ripple input of its own (boolean | undefined, false, via GOG_CONFIG.ripple.enabled) — the trigger is your element, but the directive owns the ripple so you do not have to add gogRipple beside it.

An open panel is as tall as its content — --gog-collapsible-max-height defaults to max-content. Set it to a length on an instance to cap one deliberately; the panel is overflow: hidden, so a cap clips rather than scrolls. (Before 21.4.4 that default was 480px, which clipped taller panels silently.)

gog-tabs + gog-tab

Input (on gog-tabs) Type Default
align GogTabsAlign ('start'|'center'|'end'|'stretch') 'start'
orientation GogOrientation 'horizontal'
size GogSize 'md'
fullWidth, ariaLabel false, ''
scrollActiveIntoView boolean true
showScrollTrack boolean | undefined follows scrollActiveIntoView (hidden when it's on)
ripple boolean | undefined false; via GOG_CONFIG.ripple.enabled

Model: activeIndex: number. Output: gogTabChange: number.

Input (on gog-tab) Type Default
label string ''
iconName GogIconName | null null
disabled boolean false

Slots: <ng-template gogTabHeader let-tab let-active="active"> on gog-tabs for custom header markup; <ng-template gogTabContent> inside a gog-tab to make that tab's content lazy (built on first activation, then kept alive) instead of the default (rendered immediately, hidden via [hidden] while inactive — preserves scroll/input state).

html
<gog-tabs [(activeIndex)]="tabIndex">
  <gog-tab label="Profile"><app-profile /></gog-tab>
  <gog-tab label="Report" iconName="info">
    <ng-template gogTabContent><app-expensive-report /></ng-template>
  </gog-tab>
</gog-tabs>

A surface for one self-contained thing — a product tile, a summary, a search result.

Input Type Default
variant GogSurfaceVariant ('outlined'|'elevated'|'filled') 'outlined'
size GogSize 'md' — drives padding and the row gap
disabled boolean (bare attribute works) false
loading boolean (bare attribute works) false
skeletonLines number 2 — body lines shown while loading

No outputs. Slots, all attribute directives on your own elements (not ng-template): gogCardHeader, gogCardMedia, gogCardFooter, gogCardLink. Layout order is fixed by the component — media, heading, body (the default slot), footer — not by the order you write them.

html
<gog-card>
  <img gogCardMedia [src]="person.photo" alt="" />
  <h3 gogCardHeader><a gogCardLink [routerLink]="['/people', person.id]">{{ person.name }}</a></h3>
  <p>{{ person.role }}</p>
  <div gogCardFooter>
    <gog-button size="xsm" (gogClick)="shortlist(person)">Shortlist</gog-button>
  </div>
</gog-card>
  • gogCardHeader names the card. The card reads that element's id (minting one if it has none) and points its own aria-labelledby at it, with role="group". A card with no header gets neither — an unnamed group is noise, not structure. The heading level is yours; the visual size comes from --gog-card-heading-font-size regardless of it.
  • There is no interactive input, and no gogClick output. A card becomes interactive by containing a gogCardLink, which stretches that link's hit area over the whole surface. The link stays yours: routerLink, href, target, middle-click, "open in new tab" and Enter all behave normally, and the focus ring is drawn around the card. gogCardLink only applies to <a> and <button> — on a <div> it does nothing, deliberately.
  • Other controls inside an interactive card still get their own clicks. A footer button, a checkbox, a second link: each sits above the stretched hit area automatically.
  • Two costs of the pattern, inherent to it: text in the card cannot be selected by dragging, and a second link is reachable by keyboard but not by clicking the surface around it.
  • loading replaces the content with a title bar plus skeletonLines text lines and sets aria-busy; disabled dims the card, sets aria-disabled, and takes the card link out of the tab order. Both make the link non-clickable. For a refresh of a card that already has content, project a gog-spinner-overlay instead — loading is the first-paint treatment.
  • gogCardMedia runs full-bleed to the card's edges, and rounds into its top corners when it is the first element in the card.

gog-panel + gogPanelHeader / gogPanelFooter

A titled region of a page — a settings section, a dashboard area, a form group.

Input Type Default
variant GogSurfaceVariant 'elevated'
size GogSize 'lg'
collapsible boolean (bare attribute works) false
disabled boolean (bare attribute works) false
loading boolean (bare attribute works) false
skeletonLines number 3

Model: open: boolean (default true, ignored while collapsible is off). No outputs beyond openChange. Slots: gogPanelHeader, gogPanelFooter — attribute directives on your elements.

html
<gog-panel [collapsible]="true" [(open)]="notificationsOpen">
  <h2 gogPanelHeader>Notifications</h2>
  <gog-checkbox label="Email digest" [(checked)]="emailDigest" />
  <div gogPanelFooter><gog-button size="xsm">Save</gog-button></div>
</gog-panel>
  • It is a landmark. With a gogPanelHeader it renders role="region" named by that heading — which is why the panel gets one and gog-card gets role="group": a handful of named regions is how a page is navigated, a landmark per card would bury that list.
  • Collapsing composes gog-collapsible, so the state, the id wiring and the animation are the library's existing ones. The heading stays a heading: the toggle is a separate <button> named by it through aria-labelledby, with its hit area stretched across the header row so clicking the title works for the pointer. Without a header the toggle falls back to GOG_CONFIG.labels.togglePanel (default 'Toggle section').
  • A non-collapsible panel does not clip. It undoes the collapse geometry it inherits, overflow included, so a dropdown or menu opened inside it escapes the panel's box. A collapsible one does clip while animating, exactly like gog-collapsible — prefer [appendToBody] for an overlay inside one.
  • loading keeps the heading and the footer and replaces only the body: a page section is titled before its content arrives, and blanking the title would move the layout twice.
  • The surface is never itself a link — there is no gogPanelLink. Controls live inside a panel, and a region that is a link cannot hold them. Use gog-card for that.

gog-paginator

Input Type Default
fullWidth, totalPages boolean, number true, 1
rangeMode GogPaginatorRangeMode ('window'|'ellipsis') 'window' — see note
visiblePages number 5 — 'window' mode only
showFirstPage, showLastPage boolean false — 'window' mode only
siblingCount number 2 — 'ellipsis' mode only
size GogSize 'sm'
disabled, ariaLabel false, 'Pagination'
totalRecords number | null null — see below
pageSize model<number> 10 — two-way bindable
showPageSizeSelect boolean | undefined false; via GOG_CONFIG.paginator
pageSizeOptions number[] | undefined [10, 20, 30, 40, 50]; via GOG_CONFIG.paginator

The step buttons ('Previous page'/'Next page') and the per-page names are configured, not input-driven: GOG_CONFIG.labels.previousPage/nextPage, and labels.page, a (page: number, isCurrent: boolean) => string formatter defaulting to `Page ${page}, current page` / `Go to page ${page}`.

Models: page: number (1-based, self-clamps) and pageSize: number.

Give it totalRecords instead of totalPages when you know the row count — it then derives the page count from pageSize itself, which is what removes the computed(() => Math.ceil(total / size)) a consumer would otherwise have to write and keep in sync with the rows-per-page select:

html
<gog-paginator
  [(page)]="page"
  [(pageSize)]="size"
  [totalRecords]="items().length"
  [showPageSizeSelect]="true"
/>

totalPages still works and is the right input when the server tells you a page count directly; totalRecords wins when both are set. Changing the page size always returns to page 1 — "page 5" of 10-row pages is not "page 5" of 50-row ones, so clamping alone would leave the user somewhere they never asked to be.

'window': a fixed number of page buttons that slides to keep the current page centered. 'ellipsis': first/last pinned, siblingCount around the current page, "…" fills the gap (what gog-table's built-in pagination uses).

html
<gog-paginator [(page)]="page" [totalPages]="totalPages" />

gog-table<T>

Import from @guildofgleks/ui/table — TableComponent, GogColumn and its template directives, defaultCompare, and the table's event and context types. The root does not export them (it did, deprecated, until 21.13.0), which is what lets a route that loads them lazily keep their code out of the initial bundle. Their dependencies from the root — buttons, icons, scroll, and for the table the paginator and select — still land wherever the root does.

Input Type Default
value T[] []
fullWidth boolean true
pageSize model<number> 0 (no pagination) — two-way
showPageSizeSelect boolean | undefined false; forwarded to the paginator
pageSizeOptions number[] | undefined [10, 20, 30, 40, 50]; forwarded
showRowNumbers, showTotal boolean true, false
emptyPlaceholder string '-'
paginatorPosition 'left'|'center'|'right' 'center'
totalPosition 'left'|'right'|'opposite' 'opposite'
loading boolean false
showColumnBorders boolean false
stickyHeader boolean false — pair with maxHeight
maxHeight string | null null — any CSS length
size GogSize 'lg' (row density — not 'md')
lazy boolean false — see below
totalRecords number | null null — lazy only
selectionMode GogTableSelectionMode 'none'
selection model<T[]> [] — two-way bindable
dataKey string '' — row identity field
showSelectionColumn boolean true (once selection is on)
interactiveRows boolean false
virtualize boolean false — needs maxHeight + fullWidth

Outputs: gogSortChange: GogTableSortEvent ({ field, direction }, { field: '', direction: null } when the third click clears it), gogPageChange: number (1-based; does not fire on first render, nor for the page reset a new sort causes — that reset belongs to the sort), gogRowClick: GogTableRowClickEvent<T> ({ row, index, originalEvent }).

fullWidth also picks the layout algorithm. Left at its default the table is 100% wide with table-layout: fixed; since 21.6.0 [fullWidth]="false" makes it fit-content with table-layout: auto, so the columns are measured against their content instead of splitting the total evenly. Before 21.6.0 that split clipped the widest header, and a width on the column was the workaround — under auto layout a stated width is a suggestion weighed against content rather than a hard split, so those can usually go.

virtualize renders only the rows in view, for a table long enough that stamping every row is the cost. It requires maxHeight and fullWidth, does nothing without either, and says which is missing in a dev-mode warning rather than half-working:

  • Without maxHeight the table never scrolls vertically on its own, so there is no viewport to window against — the same constraint stickyHeader has, below.
  • fullWidth="false" means table-layout: auto, and the browser then sizes columns from the rows that are rendered. Measured: rendering 2 of 24 rows moved columns by up to 7.8px, so a windowed table would shift its own columns as you scroll.

Unlike the three dropdowns' virtualize, this one deals in rows whose heights genuinely differ — a table row's height cannot be pinned, because height on a <tr> or <td> is a minimum in table layout, so a cell whose content wraps makes its row taller and no CSS stops it. The window measures each row as it renders and corrects itself, which means the scroll height is an estimate that sharpens as you scroll rather than an exact figure from the start.

What else changes while it is on: Ctrl+F finds only the rendered rows, CSS targeting :last-child matches the last rendered one, and with interactiveRows scrolling a focused row out of view moves focus to the scroll region, because the row it was on no longer exists. aria-rowcount and aria-rowindex keep the announced size and position honest, and every index the table hands out — gogRowClick's index, the showRowNumbers column, and GogColumnBodyContext.index — still counts from the top of the page rather than the top of the window. There is a ceiling: Chrome clamps an element at 33 554 426px, about 745 000 rows at 45px, and a dev-mode warning says so once when a table passes it — past that the rows stay correct while the scrollbar stops reaching the end of the data.

Where gog-table stops. No column resizing or reordering by the reader — a column's width/minWidth/maxWidth are set by whoever writes the template, not dragged by whoever reads it. No frozen columns, no expandable rows, no row grouping. [lazy]="true" keeps the fetch small, which is usually the half that hurts; virtualize above is the DOM half, and neither substitutes for the other. If a request needs one of the rest, it needs a data grid, and this is not one — say so rather than reaching for ::ng-deep.

stickyHeader is not any of them in disguise: it pins the header while rows scroll under it, which is the vertical axis. Freezing a first column against horizontal scroll is the thing that does not exist.

stickyHeader needs maxHeight (both since 21.6.0 for the pairing). A sticky element resolves against its nearest scroll container, and the table wraps itself in a gog-scroll; once that scroller moves on either axis it is a scroll container on both, because CSS coerces overflow-y: visible to auto beside a scrolling overflow-x (and clip to hidden). So the header can only ever stick to something inside the table — and without maxHeight that viewport is exactly as tall as its content and never scrolls, so there is nothing to stick to.

html
<gog-table [value]="rows" maxHeight="260px" [stickyHeader]="true">…</gog-table>

maxHeight takes any CSS length and is what makes the table own its vertical scrolling. Left null, the table grows to its content and an ancestor scrolls it — the header then follows that ancestor's scroll like everything else, which is the pre-21.6.0 behaviour and is fine as long as you are not asking for a sticky header.

Columns are declared as projected gog-column children, not an input array:

html
<gog-table [value]="rows">
  <gog-column field="name" header="Name" sortable="true" />
  <gog-column field="email" header="Email" />
  <gog-column field="status" header="Status">
    <ng-template gogColumnBody let-row let-value="value">
      <gog-tag [variant]="row.active ? 'success' : 'danger'">{{ value }}</gog-tag>
    </ng-template>
  </gog-column>
</gog-table>
Server-driven tables — lazy

By default the table owns the whole data set: it sorts value and slices the page itself. With [lazy]="true" it does neither — value is the current page, already sorted, and the table renders it untouched. Supply totalRecords (without it the table cannot know how many pages exist, so pagination stays hidden and it warns in dev), then refetch from the two outputs:

html
<gog-table
  [value]="page()"
  [lazy]="true"
  [totalRecords]="total()"
  [pageSize]="20"
  [loading]="loading()"
  dataKey="id"
  (gogSortChange)="sort.set($event); reload()"
  (gogPageChange)="pageNumber.set($event); reload()"
></gog-table>

Row numbers still count from the current page ((page - 1) * pageSize + i + 1), and showTotal reports totalRecords rather than value.length. Do not sort or slice value yourself in addition — that is what the flag turns off.

Rows per page

pageSize is a model, not an input: [pageSize]="20" works exactly as before, and [(pageSize)]="size" becomes possible. That is what makes the rows-per-page select work with no wiring — the table binds its own model straight to the paginator's, the select writes back through it, and there is no intermediate signal to keep in sync in either direction.

html
<!-- off by default; turn it on per table, or app-wide via GOG_CONFIG.paginator -->
<gog-table
  [value]="rows"
  [(pageSize)]="size"
  [showPageSizeSelect]="true"
  [pageSizeOptions]="[5, 10, 20]"
></gog-table>

Changing the size returns to page 1 and does not emit gogPageChange — the consumer already knows from pageSizeChange, and firing both would make a lazy table fetch twice. In lazy mode pageSizeChange is the refetch signal; bind [pageSize] + (pageSizeChange) rather than the banana-box if you need to act on it.

The footer stays visible at a single page whenever the select is on — hiding it would strand the user on whatever size produced that one page, with no control left to pick a smaller one.

Selection

selectionMode turns it on; [(selection)] is always a T[], including in 'single' mode where it holds zero or one row — one shape rather than a union to narrow on every read.

html
<gog-table
  [value]="rows"
  selectionMode="multiple"
  [(selection)]="selected"
  dataKey="id"
></gog-table>
  • Set dataKey. Without it rows are matched by object identity, so any refetch that produces new objects silently drops the selection. It is also the @for track key, which is what lets the DOM survive a refetch instead of being rebuilt.
  • The checkbox column renders automatically (showSelectionColumn to turn it off, e.g. for a table that selects by row click — pair that with interactiveRows).
  • The header select-all appears only in 'multiple' mode and covers the current page, never the whole data set: in lazy mode the table has never seen the other pages, and a control that behaved differently between the two modes would be worse than either.
Clickable rows

gogRowClick fires on a click regardless, but a <tr> is not focusable, so on its own that is a mouse-only affordance. interactiveRows makes rows focusable and styles them as clickable, and Enter/Space then activate the focused row. If the action is really "open this one thing", a link or button inside a cell is better than a whole-row target.

gog-column inputs: field (required, dot-paths ok), header, sortable (default false), width/minWidth/maxWidth, comparator (custom (a, b) => number, defaults to a locale-aware collator for strings). Slots inside a column: <ng-template gogColumnBody let-row let-value="value" let-index="index">, <ng-template gogColumnHeader let-header let-field="field">.

Sorting, empty/loading states and pagination are all built in — sortable columns toggle asc → desc → unsorted on click, loading shows a spinner in place of rows, an empty value shows emptyPlaceholder, and pageSize > 0 turns on the internal paginator automatically. You don't need to hand-roll any of this.

There is no typed row-selection API in the current version — if you need it, track selection yourself (e.g. a Set keyed by row id) and render a gogColumnBody checkbox column.

gog-scroll

Drop-in replacement for overflow: auto — content still scrolls natively (wheel, touch, keyboard); only the browser's own scrollbar chrome is replaced with a themeable overlay thumb. Used internally by several other components (gog-dialog's body, gog-select's panel, gog-tabs' header row) and equally usable directly in your own markup for any scrollable region — the library's official recommendation over a raw overflow-x/overflow-y.

Input Type Default
axis GogScrollAxis ('vertical'|'horizontal'|'both') 'vertical'
size GogScrollSize | undefined ('normal'|'thin') 'normal'; via GOG_CONFIG.scroll.size
autoHide boolean | undefined true; via GOG_CONFIG.scroll.autoHide
hideDelay number | undefined 800; via GOG_CONFIG.scroll.hideDelay
reachThreshold number 0
focusable boolean true — turn off when the parent already owns focus (a dialog with its own focus trap)
ariaLabel string ''
overscrollBehavior GogScrollOverscrollBehavior | undefined ('auto'|'contain'|'none') 'auto'; via GOG_CONFIG.scroll.overscrollBehavior
showTrack boolean | undefined true; via GOG_CONFIG.scroll.showTrack
horizontalWheel boolean | undefined false; via GOG_CONFIG.scroll.horizontalWheel

horizontalWheel turns a vertical wheel into horizontal scrolling (21.9.0), for the case a consumer hits first: hover a horizontal-only row, turn the wheel, and the page moves. That is the browser's own behaviour and the component deliberately did nothing about it until now.

It is off by default because it changes what an existing instance does with a gesture it currently passes on; provideGogConfig({ scroll: { horizontalWheel: true } }) turns it on app-wide. It only acts when the viewport cannot scroll vertically (checked against live geometry, so axis="both" scrolls down while there is down to go), the event carries no horizontal delta of its own (a trackpad swipe and Shift+wheel already work), ctrlKey is clear (pinch-zoom), and there is room left in the direction of the turn. That last condition is the point: at the content's end the event is left alone and the page picks it up, so the wheel never goes dead over a scrolled-to-the-end region. overscrollBehavior: 'contain' still contains — that boundary is the browser's and this never reaches past it.

Outputs: gogScroll: GogScrollMetrics, gogReachStart/gogReachEnd: 'vertical'|'horizontal'. Methods (via template ref): scrollTo(options), scrollToTop(), scrollToBottom(), scrollToLeft(), scrollToRight().

html
<gog-scroll size="thin" [focusable]="false" overscrollBehavior="contain" style="max-height: 320px">
  <!-- content that might overflow -->
</gog-scroll>

Overlays

Overlays and the viewport — the caveat that bites once per project. gog-dialog's backdrop, gog-toast-container and gog-spinner [overlay] are position: fixed, which covers the viewport only while no ancestor establishes a containing block. contain, transform, filter, backdrop-filter or will-change anywhere above retargets them to that element's box — and gog-scroll sets contain: layout style, so a dialog opened inside a scroller dims the scroller rather than the page. Place the dialog and toast outlets in the root component. The dropdown panels and gog-menu sidestep it by rendering into <body>.

gog-menu + gogMenuTrigger / gogMenuItem

A command menu. The trigger is a directive on your own button — usually the icon button you already styled — and the items are your own buttons too, so an item can hold an icon, a label and a shortcut hint without an input per piece:

html
<button gogButton variant="ghost" [gogMenuTrigger]="rowMenu" aria-label="Row actions">
  <gog-icon name="more-vertical" />
</button>

<gog-menu #rowMenu ariaLabel="Row actions">
  <button gogMenuItem (click)="edit(row)"><gog-icon name="check" /> Edit</button>
  <button gogMenuItem disabled>Transfer ownership</button>
  <button gogMenuItem (click)="remove(row)"><gog-icon name="close" /> Remove</button>
</gog-menu>
Input Type Default Notes
direction 'auto' | 'up' | 'down' 'auto' 'auto' drops down whenever the panel fits and flips up only when it cannot
ariaLabel string '' Names the panel itself

There is no appendToBody. The panel always renders into <body> and is placed from the trigger's measured rect, so a menu inside gog-scroll, gog-table or any overflow: hidden ancestor is not clipped and needs no configuration. It also takes the --gog-dropdown-z its trigger inherits, so a menu opened inside a gog-dialog stacks above the dialog.

gogMenuItem takes a ripple input (boolean | undefined, false, via GOG_CONFIG.ripple.enabled). The item is your own <button>, but the directive owns the ripple, so there is no gogRipple to add.

Output: gogClosed — fires after every close, whatever caused it.

Public methods, for driving it yourself: open(trigger, 'first' | 'last'), close(restoreFocus?), toggle(trigger), and the isOpen signal.

Keyboard, the WAI-ARIA menu button pattern: Enter/Space/ArrowDown open with the first item focused, ArrowUp opens with the last, arrows and Home/End move between items and step over disabled ones, Escape closes and returns focus to the trigger, Tab closes and lets focus move on. A press outside closes without pulling focus back.

Disabling an item is the native disabled attribute on your own button — static or bound, there is no input for it:

html
<button gogMenuItem disabled>Transfer ownership</button>
<button gogMenuItem [disabled]="isLocked()" (click)="edit()">Edit</button>

A disabled item stays in the list rather than disappearing (removing it would shift the others under the pointer), the arrow keys step over it, and clicking it does nothing.

A long menu scrolls itself, using gog-scroll — the same thin, auto-hiding scroller as everywhere else in the package, with overscrollBehavior="contain" so a wheel at the end of the list does not scroll the page behind it. Arrowing past the last visible item scrolls it into view.

The panel's height is the smallest of three: its own content, --gog-menu-max-height (320px by default), and the room between the trigger and the viewport edge. Lower the token to make a menu scroll sooner. In 21.5.0 the token did nothing — the measured room was written onto the panel as an inline max-height, which beat it; fixed in 21.5.1.

A closed menu renders nothing at all, so its commands are not in the accessibility tree until it opens.

gog-dialog

A single <gog-dialog /> renders every dialog DialogService.open(...) creates — place it once, typically in your root app component's template, not per-page and not per-dialog call:

html
<!-- app.html -->
<router-outlet />
<gog-dialog />

It has no inputs of its own — everything is driven through DialogService (see Services above). Supports nesting, dragging (when draggable !== false and the dialog has a title or close button), a focus trap for modal dialogs, Escape to close (when closable !== false), and click-outside-to-close on the backdrop.

gog-toast / gog-toast-container

Same pattern — place one <gog-toast-container />, typically in the root component:

html
<gog-toast-container [maxVisiblePerPosition]="5" />

maxVisiblePerPosition (default 5) caps how many toasts stack at once per corner; the rest queue. Individual gog-toast instances are rendered internally by the container from ToastService.toasts() — you don't place these yourself. Toasts auto-dismiss after their duration unless isSticky; hovering pauses the countdown (front-of-stack toast only).

Announcements come from two permanently-mounted, visually-hidden live regions the container owns — polite, and assertive for error/warning. The toasts themselves carry no role/aria-live: a live region created in the same tick as its text is routinely skipped by screen readers, and a second region would announce everything twice. Don't add either back.


Reading the deprecations at runtime — GOG_DEPRECATIONS

Everything the package currently deprecates, as data:

ts
import { GOG_DEPRECATIONS, type GogDeprecation } from '@guildofgleks/ui';

GOG_DEPRECATIONS; // []

kind is 'symbol' for an export or input and 'token' for a --gog-* custom property. The list is generated from the library's source — tags for symbols, stylesheets for tokens — so it matches what actually still resolves in the version you installed.

As of 21.7.0 the list is empty on both halves. Nothing in the TypeScript API is deprecated, and the three abbreviated token prefixes that used to fill the token half are gone rather than deprecated — see the removal table below. An empty list here means exactly that: nothing to migrate away from right now.

Removed in 21.7.0

Nothing in this table exists any more. Three CSS custom-property prefixes, abbreviations of a component's own name, are gone — each was honoured only as a fallback the spelled-out token wrapped (--gog-button-x: var(--gog-btn-x, value)), never declared on its own.

Removed Replacement
--gog-btn-* --gog-button-*
--gog-ms-* --gog-multiselect-*
--gog-confirm-* --gog-confirmation-dialog-*

A consumer's CSS that still sets one of the left-hand names doesn't fail their build — an unresolved var() just stops matching anything, silently. If a themed surface stopped picking up an override after upgrading to 21.7.0, this table is the first thing to check.

Removed in 21.5.0

Nothing in this table exists any more. It is here so that code written against 21.4.x — or generated from a stale copy of this file — can be migrated: each row names what a call site must become. If you are writing new code, ignore this section entirely and use the right-hand column, which is documented in full above.

Removed Replacement
gog-select/gog-multiselect chevronTemplate input <ng-template gogDropdownChevron>
gog-checkbox checkIconTemplate input <ng-template gogCheckboxIcon>
gog-tag iconTemplate input <ng-template gogTagIcon>
gog-multiselect clearIconTemplate input <ng-template gogMultiselectClearIcon>
gog-inputfield iconStartTemplate/iconEndTemplate/iconStartFn/iconEndFn/iconStartLabel/iconEndLabel <span gogInputAddonStart>/<span gogInputAddonEnd> (or a <button> with its own handler)
gog-table's [template] attribute (<ng-template template="field" type="body">) <ng-template gogColumnBody> / <ng-template gogColumnHeader> declared inside the matching <gog-column>
<column> selector / Column export <gog-column> / GogColumn
GogSelectOption / GogMultiselectOption types GogDropdownOption (the same type — they were aliases of it)
@guildofgleks/ui/src/styles/… asset path @guildofgleks/ui/styles/…

The general rule they all followed: a TemplateRef input or a string-keyed lookup was the old shape; a projected content directive with a typed context, declared where it's used, is the current one. If you're about to write fooTemplate next to an existing foo input, or key something off a string that has to match another string elsewhere, that's this exact anti-pattern — reach for a slot directive instead.

Full type reference

Shared enum-like types (import type { ... } from '@guildofgleks/ui'):

Type Values
GogSize 'xsm' | 'sm' | 'md' | 'lg' | 'slg'
GogVariant 'primary' | 'secondary' | 'outline' | 'ghost'
GogSurfaceVariant 'outlined' | 'elevated' | 'filled' — gog-card and gog-panel
GogAriaHasPopup boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' — gog-button's ariaHasPopup
GogTagVariant 'success' | 'danger' | 'warning' | 'info'
GogOrientation 'horizontal' | 'vertical'
GogTagShape 'rounded' | 'pill'
GogSpinnerVariant 'runic' | 'ring' | 'custom'
GogSkeletonShape 'text' | 'circle' | 'rect'
GogSkeletonAnimation 'pulse' | 'wave' | 'none'
GogPaginatorRangeMode 'window' | 'ellipsis'
GogScrollAxis 'vertical' | 'horizontal' | 'both'
GogScrollSize 'normal' | 'thin'
GogScrollOverscrollBehavior 'auto' | 'contain' | 'none'
GogTooltipPosition 'auto' | 'top' | 'bottom' | 'left' | 'right'
GogFloatLabelVariant 'none' | 'in' | 'on' | 'over'
GogDropdownFilterPosition 'top' | 'bottom'
GogDividerVariant 'solid' | 'dashed' | 'dotted'
GogBadgePosition 'top-end' | 'top-start' | 'bottom-end' | 'bottom-start'
GogProgressbarMode 'determinate' | 'indeterminate' | 'buffer'
GogProgressbarVariant 'accent' | 'success' | 'danger' | 'warning' | 'info'
GogButtonToggleAppearance 'joined' | 'separated'
GogTabsAlign 'start' | 'center' | 'end' | 'stretch'
GogDateSelectionMode 'single' | 'range'
GogHourFormat '12' | '24'
GogTextareaResize 'vertical' | 'horizontal' | 'both' | 'none'
GogInputType 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url' | 'date' | 'time' | 'datetime-local'
GogInputMode 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
GogTableSelectionMode 'none' | 'single' | 'multiple'
GogTableSortEvent { field: string; direction: SortDirection }
GogTableRowClickEvent<T> { row: T; index: number; originalEvent: MouseEvent | KeyboardEvent }
GogErrorDisplay 'auto' | 'manual'
GogDropdownDirection 'auto' | 'up' | 'down'
GogTooltipSide 'top' | 'bottom' | 'left' | 'right' (resolved form of GogTooltipPosition, no 'auto')
GogBuiltinIconName the 20 glyphs the package ships — see gog-icon
GogIconName GogBuiltinIconName | (string & {}) — built-ins plus anything registered via provideGogIcons