Button
Displays a button or a component that looks like a button.
- v1.0.0
- WAI-ARIA: Button
- SSR
- Zoneless
- 1.4 kB gzip
import { Component } from '@angular/core';
import { Button } from '@/ui/button/button';
/** Every Button variant, side by side. */
@Component({
selector: 'example-button-variants',
imports: [Button],
template: `
<div class="flex flex-wrap items-center gap-2">
<button ndButton>Default</button>
<button ndButton variant="secondary">Secondary</button>
<button ndButton variant="outline">Outline</button>
<button ndButton variant="ghost">Ghost</button>
<button ndButton variant="destructive">Destructive</button>
<button ndButton variant="link">Link</button>
</div>
`,
})
export class ButtonVariantsExample {}
Buttons start actions: submitting a form, opening a dialog, saving a change.
ndButton styles a native button, or an a when the action navigates, so
both keep their native keyboard behavior and semantics. Six variants set the
emphasis, from default for the main action to link for an action that
reads like text, and eight sizes cover text and icon-only buttons.
Installation
npx @ndop/cli add buttonpnpm dlx @ndop/cli add buttonyarn dlx @ndop/cli add buttonbunx @ndop/cli add buttonInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/button/button.tsimport { computed, Directive, input } from '@angular/core'; import { NdButton } from '@ndop/primitives/button'; import { cn, variants, type VariantOf } from '@/lib/utils'; /** * Class table for {@link Button}; also usable to style other elements like * a button. */ export const buttonVariants = variants({ base: "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-disabled:pointer-events-none data-disabled:opacity-50", variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40', outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-9 px-4 py-2 has-[>svg]:px-3', xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', icon: 'size-9', 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", 'icon-sm': 'size-8', 'icon-lg': 'size-10', }, }, defaults: { variant: 'default', size: 'default' }, }); /** Visual style of a {@link Button}. */ export type ButtonVariant = VariantOf<typeof buttonVariants, 'variant'>; /** Size of a {@link Button}. */ export type ButtonSize = VariantOf<typeof buttonVariants, 'size'>; /** * Styled button for a native `button` or an `a`, with the * `@ndop/primitives/button` behavior (safe default type, disabled links). * * @example * ```html * <button ndButton variant="outline" size="sm">Cancel</button> * <a ndButton variant="link" href="/docs">Docs</a> * ``` */ @Directive({ selector: 'button[ndButton], a[ndButton]', hostDirectives: [{ directive: NdButton, inputs: ['disabled'] }], host: { 'data-slot': 'button', '[attr.data-variant]': 'variant()', '[attr.data-size]': 'size()', '[class]': 'classes()', }, }) export class Button { /** Visual style. */ readonly variant = input<ButtonVariant>('default'); /** Height, padding and icon size. */ readonly size = input<ButtonSize>('default'); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( buttonVariants({ variant: this.variant(), size: this.size() }), this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Button } from '@/ui/button/button';<div class="flex flex-wrap items-center gap-2">
<button ndButton>Default</button>
<button ndButton variant="secondary">Secondary</button>
<button ndButton variant="outline">Outline</button>
<button ndButton variant="ghost">Ghost</button>
<button ndButton variant="destructive">Destructive</button>
<button ndButton variant="link">Link</button>
</div>Examples
Button anchor
Button styles on links, where shadcn uses asChild: an enabled link, and a disabled one that stays focusable and announces itself as disabled.
import { Component, signal } from '@angular/core';
import { Button } from '@/ui/button/button';
/**
* Button styles on links, where shadcn uses `asChild`: an enabled link, and
* a disabled one that stays focusable and announces itself as disabled.
*/
@Component({
selector: 'example-button-anchor',
imports: [Button],
template: `
<div class="flex flex-wrap items-center gap-2">
<a ndButton href="#login">Login</a>
<a ndButton variant="outline" href="#billing" [disabled]="locked()">
Billing
</a>
</div>
`,
})
export class ButtonAnchorExample {
protected readonly locked = signal(true);
}
Button link
The link variant: a button that looks like a text link.
import { Component } from '@angular/core';
import { Button } from '@/ui/button/button';
/** The `link` variant: a button that looks like a text link. */
@Component({
selector: 'example-button-link',
imports: [Button],
template: `<button ndButton variant="link">Link</button>`,
})
export class ButtonLinkExample {}
Button sizes
Button sizes, including an icon-only button with an accessible name.
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
/** Button sizes, including an icon-only button with an accessible name. */
@Component({
selector: 'example-button-sizes',
imports: [Button, NdIcon],
template: `
<div class="flex flex-wrap items-center gap-2">
<button ndButton variant="outline" size="xs">Extra small</button>
<button ndButton variant="outline" size="sm">Small</button>
<button ndButton variant="outline">Default</button>
<button ndButton variant="outline" size="lg">Large</button>
<button ndButton variant="outline" size="icon" aria-label="Dismiss">
<svg ndIcon="x"></svg>
</button>
</div>
`,
})
export class ButtonSizesExample {}
Accessibility
Implements the Button pattern of the WAI-ARIA Authoring Practices.
Keyboard
Keys the component handles.
| Key | Action |
|---|---|
| Enter | Activates the button, or follows the link; does nothing while disabled. |
| Space | Activates a button; does nothing while disabled. Links don't react to Space. |
Focus
- A disabled
buttonand a disabledaare removed from the tab order. - Activation does not move focus.
Right-to-left
Nothing mirrors.
API reference
Button
button[ndButton], a[ndButton]role native (button / link)Styled button for a native button or an a, with the
@ndop/primitives/button behavior (safe default type, disabled links).
Manages aria-disabled.
| Button binding | Type | Default |
|---|---|---|
[variant]Visual style. | 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'default' |
[size]Height, padding and icon size. | 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg' | 'default' |
[class]Extra classes, merged last so they win. | string | '' |
[disabled]Whether the button or link is disabled. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | button |
data-variant | — |
data-size | — |
data-disabled | — |
Agent notes
Do
- Use one
defaultbutton for the main action of a view, andoutline,secondaryorghostfor the others. - Put
ndButtonon anawith anhrefwhen the action navigates, so it opens in a new tab and shows its URL like any link. - Give icon-only buttons (
size="icon") anaria-label. - Use
destructivefor actions that delete or can't be undone, and confirm them in a dialog.
Don't
- Don't put
ndButtonon adivorspan; it only applies tobuttonanda, which keyboard and screen reader users can operate. - Don't disable a submit button to signal invalid input; keep it enabled and show the errors when it is pressed.
- Don't use
linkfor a primary action; it is meant to read like text.