Button
Polymorphic button with intent and size variants.
Requires Alpine.js
Intents
1{{ partial:components/primitives/button label="Primary" /}}
2{{ partial:components/primitives/button label="Secondary" intent="secondary" /}}
3{{ partial:components/primitives/button label="Destructive" intent="destructive" /}}
4{{ partial:components/primitives/button label="Ghost" intent="ghost" /}}
5{{ partial:components/primitives/button label="Link" intent="link" /}}
{{ partial:components/primitives/button label="Primary" /}}
{{ partial:components/primitives/button label="Secondary" intent="secondary" /}}
{{ partial:components/primitives/button label="Destructive" intent="destructive" /}}
{{ partial:components/primitives/button label="Ghost" intent="ghost" /}}
{{ partial:components/primitives/button label="Link" intent="link" /}}
Sizes
1{{ partial:components/primitives/button label="Small" size="sm" /}}
2{{ partial:components/primitives/button label="Default" /}}
3{{ partial:components/primitives/button label="Large" size="lg" /}}
4{{ partial:components/primitives/button icon_before="star" size="icon" attrs="aria-label=Favorite" /}}
{{ partial:components/primitives/button label="Small" size="sm" /}}
{{ partial:components/primitives/button label="Default" /}}
{{ partial:components/primitives/button label="Large" size="lg" /}}
{{ partial:components/primitives/button icon_before="star" size="icon" attrs="aria-label=Favorite" /}}
States
1{{ partial:components/primitives/button label="Disabled" disabled="true" /}}
2{{ partial:components/primitives/button label="Disabled Ghost" intent="ghost" disabled="true" /}}
3{{ partial:components/primitives/button label="Loading" loading="true" /}}
4{{ partial:components/primitives/button label="Disabled Link" href="#" disabled="true" /}}
5{{ partial:components/primitives/button label="Disabled Button" attrs="disabled" /}}
{{ partial:components/primitives/button label="Disabled" disabled="true" /}}
{{ partial:components/primitives/button label="Disabled Ghost" intent="ghost" disabled="true" /}}
{{ partial:components/primitives/button label="Loading" loading="true" /}}
{{ partial:components/primitives/button label="Disabled Link" href="#" disabled="true" /}}
{{ partial:components/primitives/button label="Disabled Button" attrs="disabled" /}}
Icons
1{{ partial:components/primitives/button label="Icon before" icon_before="star" intent="secondary" /}}
2{{ partial:components/primitives/button label="Icon after" icon_after="arrow-forward" intent="ghost" /}}
3{{ partial:components/primitives/button label="Both icons" icon_before="star" icon_after="arrow-forward" /}}
{{ partial:components/primitives/button label="Icon before" icon_before="star" intent="secondary" /}}
{{ partial:components/primitives/button label="Icon after" icon_after="arrow-forward" intent="ghost" /}}
{{ partial:components/primitives/button label="Both icons" icon_before="star" icon_after="arrow-forward" /}}
Slots & Polymorphism
As Link
As Span
1{{ partial:components/primitives/button label="With before slot" intent="secondary" }}
2 {{ slot:before }}
3 {{ svg src="icons/star" class="size-4 shrink-0" aria-hidden="true" }}
4 {{ /slot:before }}
5{{ /partial:components/primitives/button }}
6{{ partial:components/primitives/button intent="ghost" }}
7 {{ slot:attrs }}
8 aria-label="Button with attrs slot" data-test-id="button-slot-attrs"
9 {{ /slot:attrs }}
10 With attrs slot
11{{ /partial:components/primitives/button }}
12{{ partial:components/primitives/button label="As Link" href="/" /}}
13{{ partial:components/primitives/button label="As Span" as="span" class="border border-border" /}}
{{ partial:components/primitives/button label="With before slot" intent="secondary" }}
{{ slot:before }}
{{ svg src="icons/star" class="size-4 shrink-0" aria-hidden="true" }}
{{ /slot:before }}
{{ /partial:components/primitives/button }}
{{ partial:components/primitives/button intent="ghost" }}
{{ slot:attrs }}
aria-label="Button with attrs slot" data-test-id="button-slot-attrs"
{{ /slot:attrs }}
With attrs slot
{{ /partial:components/primitives/button }}
{{ partial:components/primitives/button label="As Link" href="/" /}}
{{ partial:components/primitives/button label="As Span" as="span" class="border border-border" /}}
Props
| Name | Type | Default | Description |
|---|---|---|---|
label
|
string
|
Button label text (falls back to default slot) | |
as
|
string
|
button
|
HTML element when href is not provided |
href
|
string
|
If present, renders as an anchor | |
target
|
string
|
Anchor target attribute (_self, _blank, ...) | |
type
|
string
|
button
|
Button type attribute when rendering as |
intent
|
string
|
primary
|
Visual intent: primary|secondary|destructive|ghost|link |
size
|
string
|
default
|
Size variant: sm|default|lg|icon |
disabled
|
boolean
|
|
Disabled state |
loading
|
boolean
|
|
Loading state with spinner indicator |
icon_before
|
string
|
Icon name rendered before the label (uses {{ svg }}; overrides slot:before) | |
icon_after
|
string
|
Icon name rendered after the label (uses {{ svg }}; overrides slot:after) | |
class
|
string
|
Additional classes merged via tw_merge (root element only) | |
attrs
|
string
|
Additional raw HTML attributes passed to the root element |
Slots
| Name | Fallback / Default | Description |
|---|---|---|
after
|
||
attrs
|
Raw root attributes slot for complex/dynamic bindings (e.g. Alpine :id / @click) | |
before
|
Source
1{{#
2 @name Button
3 @desc Polymorphic button with intent and size variants.
4 @param label string - Button label text (falls back to default slot)
5 @param as string [button] - HTML element when href is not provided
6 @param href string - If present, renders as an anchor
7 @param target string - Anchor target attribute (_self, _blank, ...)
8 @param type string [button] - Button type attribute when rendering as <button>
9 @param intent string [primary] - Visual intent: primary|secondary|destructive|ghost|link
10 @param size string [default] - Size variant: sm|default|lg|icon
11 @param disabled boolean [false] - Disabled state
12 @param loading boolean [false] - Loading state with spinner indicator
13 @param icon_before string - Icon name rendered before the label (uses {{ svg }}; overrides slot:before)
14 @param icon_after string - Icon name rendered after the label (uses {{ svg }}; overrides slot:after)
15 @param class string - Additional classes merged via tw_merge (root element only)
16 @param attrs string - Additional raw HTML attributes passed to the root element
17 @slot attrs - Raw root attributes slot for complex/dynamic bindings (e.g. Alpine :id / @click)
18#}}
19{{# format-ignore-start #}}
20{{ _el = href ? 'a' : (as ?? 'button') }}
21
22{{ _class_loading = loading ?= 'cursor-wait' }}
23{{ _base = '
24 rounded-default inline-flex shrink-0 items-center justify-center gap-2
25 font-medium whitespace-nowrap transition-colors
26 focus-visible:ring-ring focus-visible:ring-offset-background
27 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none
28 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50
29 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50
30' }}
31
32{{ _intents = [
33 'primary' => 'bg-primary hover:bg-primary/90 text-primary-foreground',
34 'secondary' => 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
35 'destructive' => 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
36 'ghost' => 'text-foreground hover:bg-secondary',
37 'link' => 'text-primary hover:text-primary/80 underline-offset-4 hover:underline',
38] }}
39
40{{ _sizes = [
41 'sm' => 'h-8 px-3 text-sm',
42 'default' => 'h-10 px-4 text-sm',
43 'lg' => 'h-12 px-6 text-base',
44 'icon' => 'size-10 p-0',
45] }}
46
47{{ _class_intent = _intents[intent] ?? _intents['primary'] }}
48{{ _class_size = _sizes[size] ?? _sizes['default'] }}
49
50{{# format-ignore-end #}}
51<{{ _el }}
52 class="{{ '{_base} {_class_intent} {_class_size} {_class_loading} {class}' | tw_merge }}"
53 {{ if _el == 'a' }}
54 href="{{ href }}"
55 {{ if target }}target="{{ target }}"{{ /if }}
56 {{ if target == '_blank' }}rel="noopener noreferrer"{{ /if }}
57 {{ if disabled }}
58 aria-disabled="true"
59 tabindex="-1"
60 {{ /if }}
61 {{ elseif _el == 'button' }}
62 type="{{ type ?? 'button' }}"
63 {{ if disabled }}disabled{{ /if }}
64 {{ else }}
65 {{ if disabled }}aria-disabled="true"{{ /if }}
66 {{ /if }}
67 {{ if loading }}aria-busy="true"{{ /if }}
68 {{ attrs }}
69 {{ slot:attrs }}
70>
71 {{ if loading }}
72 <span
73 class="size-4 animate-spin rounded-full border-2 border-current border-r-transparent"
74 aria-hidden="true"
75 ></span>
76 {{ else }}
77 {{ if icon_before }}
78 {{ svg src="icons/{icon_before}" class="size-4 shrink-0" aria-hidden="true" }}
79 {{ else }}
80 {{ slot:before }}
81 {{ /if }}
82 {{ /if }}
83 {{ label ?? slot }}
84 {{ unless loading }}
85 {{ if icon_after }}
86 {{ svg src="icons/{icon_after}" class="size-4 shrink-0" aria-hidden="true" }}
87 {{ else }}
88 {{ slot:after }}
89 {{ /if }}
90 {{ /unless }}
91</{{ _el }}>
{{#
@name Button
@desc Polymorphic button with intent and size variants.
@param label string - Button label text (falls back to default slot)
@param as string [button] - HTML element when href is not provided
@param href string - If present, renders as an anchor
@param target string - Anchor target attribute (_self, _blank, ...)
@param type string [button] - Button type attribute when rendering as <button>
@param intent string [primary] - Visual intent: primary|secondary|destructive|ghost|link
@param size string [default] - Size variant: sm|default|lg|icon
@param disabled boolean [false] - Disabled state
@param loading boolean [false] - Loading state with spinner indicator
@param icon_before string - Icon name rendered before the label (uses {{ svg }}; overrides slot:before)
@param icon_after string - Icon name rendered after the label (uses {{ svg }}; overrides slot:after)
@param class string - Additional classes merged via tw_merge (root element only)
@param attrs string - Additional raw HTML attributes passed to the root element
@slot attrs - Raw root attributes slot for complex/dynamic bindings (e.g. Alpine :id / @click)
#}}
{{# format-ignore-start #}}
{{ _el = href ? 'a' : (as ?? 'button') }}
{{ _class_loading = loading ?= 'cursor-wait' }}
{{ _base = '
rounded-default inline-flex shrink-0 items-center justify-center gap-2
font-medium whitespace-nowrap transition-colors
focus-visible:ring-ring focus-visible:ring-offset-background
focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none
disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50
aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50
' }}
{{ _intents = [
'primary' => 'bg-primary hover:bg-primary/90 text-primary-foreground',
'secondary' => 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
'destructive' => 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
'ghost' => 'text-foreground hover:bg-secondary',
'link' => 'text-primary hover:text-primary/80 underline-offset-4 hover:underline',
] }}
{{ _sizes = [
'sm' => 'h-8 px-3 text-sm',
'default' => 'h-10 px-4 text-sm',
'lg' => 'h-12 px-6 text-base',
'icon' => 'size-10 p-0',
] }}
{{ _class_intent = _intents[intent] ?? _intents['primary'] }}
{{ _class_size = _sizes[size] ?? _sizes['default'] }}
{{# format-ignore-end #}}
<{{ _el }}
class="{{ '{_base} {_class_intent} {_class_size} {_class_loading} {class}' | tw_merge }}"
{{ if _el == 'a' }}
href="{{ href }}"
{{ if target }}target="{{ target }}"{{ /if }}
{{ if target == '_blank' }}rel="noopener noreferrer"{{ /if }}
{{ if disabled }}
aria-disabled="true"
tabindex="-1"
{{ /if }}
{{ elseif _el == 'button' }}
type="{{ type ?? 'button' }}"
{{ if disabled }}disabled{{ /if }}
{{ else }}
{{ if disabled }}aria-disabled="true"{{ /if }}
{{ /if }}
{{ if loading }}aria-busy="true"{{ /if }}
{{ attrs }}
{{ slot:attrs }}
>
{{ if loading }}
<span
class="size-4 animate-spin rounded-full border-2 border-current border-r-transparent"
aria-hidden="true"
></span>
{{ else }}
{{ if icon_before }}
{{ svg src="icons/{icon_before}" class="size-4 shrink-0" aria-hidden="true" }}
{{ else }}
{{ slot:before }}
{{ /if }}
{{ /if }}
{{ label ?? slot }}
{{ unless loading }}
{{ if icon_after }}
{{ svg src="icons/{icon_after}" class="size-4 shrink-0" aria-hidden="true" }}
{{ else }}
{{ slot:after }}
{{ /if }}
{{ /unless }}
</{{ _el }}>
Dependencies
Packages
1composer require marcorieser/tailwind-merge-statamic
2npm install alpinejs
composer require marcorieser/tailwind-merge-statamic npm install alpinejs