GUILD OF GLEKS UIv21.14.0

gog-button  ·  [gogButton]

Button

A debounced action button with four variants, five sizes, and built-in loading, disabled, and full-width states — plus, since 21.4.0, a [gogButton] directive that gives the same look to a link you own.

Overview

Import the component and drop it into a template.

typescript
import { ButtonComponent } from '@guildofgleks/ui';

@Component({
  // ...
  imports: [ButtonComponent],
})

Basic usage — a primary button that reacts to a click:

<gog-button variant="primary" (gogClick)="onClick($event)">
  Click me
</gog-button>

Examples

Variants & sizes

Every combination of the four variants and five sizes.

primary
secondary
outline
ghost
<gog-button variant="primary" size="md">Primary</gog-button>
<gog-button variant="secondary" size="md">Secondary</gog-button>
<gog-button variant="outline" size="md">Outline</gog-button>
<gog-button variant="ghost" size="md">Ghost</gog-button>

Severity 21.9.0

severity says what the action means; variant says how loudly it is drawn. The two are orthogonal, so this is not a fifth variant — every cell below is a real combination, and a ghost delete is still a delete. "accent" is the default and the absence of a claim, which is why every other button on this page needs no opt-out.

success
danger
warning
info

Now switch the theme with the toggle in the header and look again. This grid is the best demonstration of the token layering the Theming guide describes, because three separate decisions are visible in it at once. A filled button's label is the colour its own theme states for that status, and it is stated per status rather than per theme: under primeng, success, warning and info carry a near-black label because white would be unreadable on those three, while danger keeps white because on that hue it is fine. Hover and press deepen the fill away from that label, so a state always makes the label easier to read rather than harder. And the outlined and ghost labels are not the raw status colour at all but that hue mixed halfway toward the page's ink, because as body text the raw hue clears WCAG AA in only five of the eleven shipped themes.

@for (severity of severities; track severity) {
  @for (variant of variants; track variant) {
    <gog-button [variant]="variant" [severity]="severity">{{ variant }}</gog-button>
  }
}

Press feedback, with animations off 21.9.0

Hold any of these down. The background deepens one step past its own hover, so the press reads even while the pointer is already hovering the button — and it is a state rather than a movement, which is the whole point. Turn on prefers-reduced-motion: reduce (DevTools → Rendering → Emulate CSS media feature) and the scale() goes away while the colour stays.

Until 21.9.0 the scale was the whole press, so a reader with animations off pressed a button and nothing happened at all. The ripple does not cover that case and is not meant to: it is off by default, and it is suppressed under reduced motion on purpose, because a ripple really is decoration and a press state is not. Override the colour per instance with --gog-button-press-bg / --gog-button-press-color, per theme with --gog-button-<variant>-active-bg, and retime or remove the movement with --gog-button-active-scale. Every other pressable surface in the library — menu items, chips, tab and accordion headers, button-toggle options, the three dropdowns' option rows — gained the same treatment in the same release.

<!-- Nothing to wire up: every variant presses. The tokens are the knobs. -->
<gog-button variant="primary">primary</gog-button>

<!-- One instance, its own press colour -->
<gog-button
  variant="primary"
  style="--gog-button-press-bg: var(--gog-danger-color); --gog-button-press-color: #fff"
>
  primary
</gog-button>

Disabled

One disabled button per variant — disabled state must stay legible on every color.

<gog-button variant="primary" [disabled]="true">Primary</gog-button>

Loading

Loading swaps the label for a spinner and blocks clicks (via aria-disabled, not the native disabled attribute, so focus is preserved).

The spinner scales with the button size:

<gog-button
  variant="primary"
  [loading]="isLoading()"
  (gogClick)="simulateLoading()"
>
  Simulate loading
</gog-button>

Full width

Stretches to fill its container; wrapped here in a narrower box to show it.

<gog-button variant="outline" [fullWidth]="true">Full width</gog-button>

Icon-only

No visible label, so ariaLabel is required — it targets the inner <button>, unlike a plain aria-label attribute on <gog-button> itself.

<gog-button variant="primary" ariaLabel="Confirm">
  <gog-icon name="check" />
</gog-button>

ARIA state — a toggle and a disclosure

Same trap as ariaLabel, one step further: the component hides the real <button>, so [attr.aria-pressed] written on <gog-button> lands on the custom-element host — which has no role — and reaches no assistive tech at all. It compiles, throws nothing, and looks right, which is what makes it worth a demo. Use the inputs: ariaPressed, ariaExpanded, ariaControls and ariaHasPopup.

A toggle button now looks toggled21.9.0 — press "Mirror layout" and it keeps an inset ring (--gog-button-<variant>-toggled-shadow, width from --gog-button-toggled-ring-width) for as long as aria-pressed is "true" or "mixed". A ring rather than a fill, because hover and press already own the background and the state has to survive both. Until 21.9.0 it did not exist at all: a button could announce itself as on to a screen reader and look identical to an off one.

Inspect either button: the attribute sits on the inner <button>, never on <gog-button>. The off state renders aria-pressed="false" rather than dropping the attribute — null means "not a toggle button", false means "a toggle button that is off", and to a screen reader those are two different controls. [gogButton] needs none of these inputs: it styles an element you own, so write the attributes on your own <button> directly — and it draws the same toggled ring off the attribute you wrote.

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

<gog-button
  [ariaExpanded]="areFiltersOpen()"
  ariaControls="filters"
  ariaHasPopup="dialog"
  (gogClick)="toggleFilters()"
>
  Filters
</gog-button>

<div id="filters" [hidden]="!areFiltersOpen()">…</div>

Debounce guard

Clicks are throttled leading-edge: the first click fires immediately, further clicks are dropped for debounce ms (default 300). Click rapidly and watch the counter lag behind your clicks.

Accepted clicks: 0

<gog-button variant="primary" [debounce]="300" (gogClick)="onSpamClick()">Click me fast</gog-button>

Native type semantics

type is forwarded to the native <button>.

Neither button pressed yet.

<form (submit)="onFormSubmit($event)" (reset)="onFormReset()">
  <gog-button variant="primary" type="submit">Submit</gog-button>
  <gog-button variant="outline" type="reset">Reset</gog-button>
</form>

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

typescript
import { GogButtonDirective } from '@guildofgleks/ui';

@Component({
  // ...
  imports: [GogButtonDirective],
})
<a gogButton routerLink="/general/theming">See theming</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="/components/table">Checkout</a>

The selector is a[gogButton], button[gogButton] — deliberately not a bare [gogButton]. On a <div> the result would look like a button while being invisible to the keyboard and to assistive technology.

Which one to reach for

The component when the button 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. The directive when the element must be a link, or must keep directives of its own. routerLink, href, target, download, type="submit" and the rest 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 — a design point, not an omission. A component that took a routerLink input would force the router on every app that installs the package.

Two things the directive 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, which a directive cannot add without taking over the element's content).

Its styles live in the global styles/button.css, pulled in by index.css, because Angular's emulated encapsulation could never reach an element declared in your template. Nothing changes in your setup — see Theming.

[gogButton] — Inputs

NameTypeDefaultDescription
variant'primary' | 'secondary' | 'outline' | 'ghost''primary'Visual style — the same four the component offers.
severity'accent' | 'success' | 'danger' | 'warning' | 'info''accent'What the action means, as opposed to how loudly it is drawn. Orthogonal to variant, so every combination is real: a ghost delete is still a delete. accent is the absence of a claim. The same input the component takes.
size'xsm' | 'sm' | 'md' | 'lg' | 'slg''md'Also settable app-wide via GOG_CONFIG.control.size.
fullWidthbooleanfalseStretches the element to fill its container. A bare attribute works.

API Reference

Inputs

NameTypeDefaultDescription
variant'primary' | 'secondary' | 'outline' | 'ghost''primary'Visual style of the button.
severity21.9.0'accent' | 'success' | 'danger' | 'warning' | 'info''accent'What the action means, as opposed to how loudly it is drawn. Orthogonal to variant, so every combination is real: a ghost delete is still a delete. accent is the absence of a claim and leaves the button exactly as it was.
size'xsm' | 'sm' | 'md' | 'lg' | 'slg''md'Button size.
disabledbooleanfalseFully non-interactive: excluded from tab order via the native disabled attribute.
fullWidthbooleanfalseStretches the button to fill its container.
type'button' | 'submit' | 'reset''button'Forwarded to the native <button> type attribute.
loadingbooleanfalseShows a spinner in place of the label and blocks activation. Uses aria-disabled rather than the native disabled attribute, so the button stays focusable.
debouncenumber300Minimum time, in ms, between accepted clicks. Leading-edge throttle: the first click fires immediately, further clicks are dropped until the window elapses.
ariaLabelstring | nullnullAccessible name forwarded to the native <button>. Required for icon-only buttons — a plain aria-label attribute on <gog-button> lands on the host element, not the inner button, so assistive tech never sees it.
ariaPressed21.8.0boolean | 'mixed' | nullnullMarks the button as a toggle and reports its state. null omits the attribute entirely; false renders aria-pressed="false", which is what an off toggle has to say — a button with no aria-pressed is not a toggle button.
ariaExpanded21.8.0boolean | nullnullFor a disclosure or popup trigger: whether the thing it controls is currently open. Like ariaPressed, false is a real state and null means "this button expands nothing".
ariaControls21.8.0string | nullnullId of the element this button controls. Pairs with ariaExpanded; point it at an element that is actually in the document.
ariaHasPopup21.8.0GogAriaHasPopup | nullnullboolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' — what kind of popup the button opens.
ripple21.6.1boolean | undefinedundefinedPress ripple on the inner <button>. Unset, falls back to GOG_CONFIG.ripple.enabled, which is off by default; setting it here wins over the app-wide value in both directions.

Outputs

NameTypeDescription
gogClickEventEmitter<MouseEvent>Emitted on each accepted click, after debounce throttling.

Global Configuration

Set any of these once with provideGogConfig() instead of repeating them on every instance — an instance's own input still wins when it sets one itself. See Global Configuration for the full reference.

  • GOG_CONFIG.control.size
  • GOG_CONFIG.button.debounce
  • GOG_CONFIG.ripple.enabled
  • GOG_CONFIG.spinner.component — the spinner loading draws, which has no input of its own
  • GOG_CONFIG.spinner.variant — the same spinner, when no component is set

This site is not on the default here. The library ships ripple.enabled as false; these docs set it to true app-wide so the demos above actually show the press feedback. In a fresh app you get no ripple until you ask for one — the droplet button in the header switches this site between the two, and it is on right now.

Styling Tokens

Every CSS custom property the button paints with. Override any of them — on a single instance, a subtree, or a theme — to restyle it. See the Theming guide for the full token-layering model, or the Theme Generator to tweak these live.

TokenDescription
--gog-button-font-family / -font-weight / -letter-spacing / -text-transformLabel typography.
--gog-button-radiusCorner radius.
--gog-button-border-width / -styleBorder.
--gog-button-transition-durationHover / active transition timing.
--gog-button-{variant}-bg / -color / -border / -shadowFill, text, border and shadow per variant (primary/secondary/outline/ghost).
--gog-button-{variant}-hover-bg / -hover-color / -hover-shadowHover state per variant.
--gog-button-{variant}-press-bg / -press-colorPressed state per variant. The press is a colour and not only the scale below, so it survives prefers-reduced-motion.
--gog-button-focus-ring-color / -focus-ring-width / -focus-ring-offsetThe keyboard focus ring. Its colour is its own token since 21.13.0 and reads the accent; it used to follow each variant’s hover wash, which on ghost and the severity outline buttons made the ring invisible in four themes (1.07:1 at worst).
--gog-button-active-scaleHow far a press shrinks the button. Dropped under prefers-reduced-motion, where the press colour carries the state on its own.
--gog-button-{variant}-toggled-shadow / --gog-button-toggled-ring-widthThe inset ring a button with aria-pressed="true" (or "mixed") draws. A ring rather than a fill, because hover and press already own the background.
--gog-button-{status}-fill / -fill-hover / -fill-press / -on-fill / -ink / -washThe severity palette, per status (danger/success/warning/info). fill and on-fill are a filled button and its label; ink is the label of a transparent one, the status hue mixed halfway toward the page ink; wash is the hover background under it.
--gog-button-{variant}-spinner-colorThe loading spinner, per variant.
--gog-button-bg / -color / -border / -padding / -font-size / -press-bg / -press-color / -toggled-shadowUndeclared by default — the escape hatch for styling a single button instance.