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 button element |
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 element
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 inline-flex shrink-0 items-center justify-center gap-2
25 border-2 border-transparent cursor-pointer
26 font-medium whitespace-nowrap transition-colors
27 focus-visible:outline-focus focus-visible:border-focus-text focus-visible:outline focus-visible:outline-4
28 active:bg-focus active:text-focus-text active:border-focus
29 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50
30 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50
31' }}
32
33{{ _intents = [
34 'primary' => 'bg-primary text-primary-foreground border-primary rounded-full hover:opacity-85',
35 'secondary' => 'bg-secondary text-secondary-foreground border-secondary rounded-full hover:opacity-85',
36 'destructive' => 'bg-destructive text-destructive-foreground border-destructive rounded-full hover:opacity-85',
37 'ghost' => 'text-foreground rounded-full border-transparent hover:border-current',
38 'link' => 'text-primary focus-visible:bg-focus focus-visible:text-focus-text border-transparent underline decoration-1 underline-offset-[0.15em] hover:decoration-[3px] focus-visible:border-transparent focus-visible:no-underline focus-visible:[box-shadow:0_4px_var(--color-focus-text)] focus-visible:outline-none',
39] }}
40
41{{ _sizes = [
42 'sm' => 'h-8 px-3 text-sm',
43 'default' => 'h-10 px-4 text-sm',
44 'lg' => 'h-12 px-6 text-base',
45 'icon' => 'size-10 p-0',
46] }}
47
48{{ _class_intent = _intents[intent] ?? _intents['primary'] }}
49{{ _class_size = _sizes[size] ?? _sizes['default'] }}
50
51{{# format-ignore-end #}}
52<{{ _el }}
53 class="{{ '{_base} {_class_intent} {_class_size} {_class_loading} {class}' | tw_merge }}"
54 {{ if _el == 'a' }}
55 href="{{ href }}"
56 {{ if target }}target="{{ target }}"{{ /if }}
57 {{ if target == '_blank' }}rel="noopener noreferrer"{{ /if }}
58 {{ if disabled }}
59 aria-disabled="true"
60 tabindex="-1"
61 {{ /if }}
62 {{ elseif _el == 'button' }}
63 type="{{ type ?? 'button' }}"
64 {{ if disabled }}disabled{{ /if }}
65 {{ else }}
66 {{ if disabled }}aria-disabled="true"{{ /if }}
67 {{ /if }}
68 {{ if loading }}aria-busy="true"{{ /if }}
69 {{ attrs }}
70 {{ slot:attrs }}
71>
72 {{ if loading }}
73 <span class="animate-flip size-4 bg-current" aria-hidden="true"></span>
74 {{ else }}
75 {{ if icon_before }}
76 {{ svg src="icons/{icon_before}" class="size-4 shrink-0" aria-hidden="true" }}
77 {{ else }}
78 {{ slot:before }}
79 {{ /if }}
80 {{ /if }}
81 {{ label ?? slot }}
82 {{ unless loading }}
83 {{ if icon_after }}
84 {{ svg src="icons/{icon_after}" class="size-4 shrink-0" aria-hidden="true" }}
85 {{ else }}
86 {{ slot:after }}
87 {{ /if }}
88 {{ /unless }}
89</{{ _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 element
@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 = '
inline-flex shrink-0 items-center justify-center gap-2
border-2 border-transparent cursor-pointer
font-medium whitespace-nowrap transition-colors
focus-visible:outline-focus focus-visible:border-focus-text focus-visible:outline focus-visible:outline-4
active:bg-focus active:text-focus-text active:border-focus
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 text-primary-foreground border-primary rounded-full hover:opacity-85',
'secondary' => 'bg-secondary text-secondary-foreground border-secondary rounded-full hover:opacity-85',
'destructive' => 'bg-destructive text-destructive-foreground border-destructive rounded-full hover:opacity-85',
'ghost' => 'text-foreground rounded-full border-transparent hover:border-current',
'link' => 'text-primary focus-visible:bg-focus focus-visible:text-focus-text border-transparent underline decoration-1 underline-offset-[0.15em] hover:decoration-[3px] focus-visible:border-transparent focus-visible:no-underline focus-visible:[box-shadow:0_4px_var(--color-focus-text)] focus-visible:outline-none',
] }}
{{ _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="animate-flip size-4 bg-current" 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