Kern Component Library GitHub GitHub

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" /}}

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" /}}

States

Disabled Link
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" /}}

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" /}}

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" /}}

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 }}>

Dependencies

Packages

1composer require marcorieser/tailwind-merge-statamic
2npm install alpinejs