Button
<button class="btn">Primary</button><button class="btn btn-secondary">Secondary</button><button class="btn btn-outline">Outline</button><button class="btn btn-ghost">Ghost</button><button class="btn btn-destructive">Destructive</button><button class="btn btn-link">Link</button><button class="btn">Save changes</button><button class="btn btn-outline">Cancel</button>The classes style any element, but reach for a real <button> or <a> so keyboard activation,
focus order and disabled come from the platform.
Cursor
Section titled “Cursor”Tailwind v4 switched buttons from cursor: pointer
to cursor: default, and partialkit follows it. Add this to your own stylesheet to get the pointer
back:
@layer base { button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; }}<button class="btn btn-xs">Extra small</button><button class="btn btn-sm">Small</button><button class="btn">Default</button><button class="btn btn-lg">Large</button>
<button class="btn btn-outline btn-icon-xs" aria-label="Add"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"></path> </svg></button>
<button class="btn btn-outline btn-icon-sm" aria-label="Add"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"></path> </svg></button>
<button class="btn btn-outline btn-icon" aria-label="Add"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"></path> </svg></button>
<button class="btn btn-outline btn-icon-lg" aria-label="Add"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"></path> </svg></button>| Class | Height |
|---|---|
.btn-xs | 24px |
.btn-sm | 28px |
.btn | 32px |
.btn-lg | 36px |
.btn-icon-xs .btn-icon-sm .btn-icon .btn-icon-lg | Square, matching the text size of the same name |
Composing the sizes (.btn-icon.btn-sm) reaches the same place, for markup already written that way.
Default
Section titled “Default”<button class="btn">Save changes</button>The base class carries the default variant, applied when no other one is.
Outline
Section titled “Outline”<button class="btn btn-outline">Outline</button>Secondary
Section titled “Secondary”<button class="btn btn-secondary">Secondary</button><button class="btn btn-ghost">Ghost</button>No surface until it is hovered. The hover uses --muted, which is a 3% grey — deliberately quiet.
Destructive
Section titled “Destructive”<button class="btn btn-destructive">Delete</button>A wash of the danger colour rather than a solid fill, so the weight comes from the colour.
<button class="btn btn-link">Link</button>Underlines on hover and never takes a surface.
<button class="btn btn-icon" aria-label="Add item"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"></path> </svg></button>
<button class="btn btn-outline btn-icon" aria-label="Search"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="11" cy="11" r="7"></circle> <path d="m20 20-3.5-3.5"></path> </svg></button>
<button class="btn btn-ghost btn-icon" aria-label="More options"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="5" r="1"></circle> <circle cx="12" cy="12" r="1"></circle> <circle cx="12" cy="19" r="1"></circle> </svg></button>An icon-only button needs an aria-label or .sr-only text — there is no visible text to name it.
With Icon
Section titled “With Icon”Mark the icon with data-icon="inline-start" or data-icon="inline-end" so the padding tightens on
that side.
<button class="btn"> <svg data-icon="inline-start" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M12 5l7 7-7 7"></path> </svg> Continue</button>
<button class="btn btn-outline"> Open in new tab <svg data-icon="inline-end" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3"></path> </svg></button>Rounded
Section titled “Rounded”<button class="btn" style="border-radius: 9999px">Rounded</button>
<button class="btn btn-outline btn-icon" style="border-radius: 9999px" aria-label="Add"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 5v14M5 12h14"></path> </svg></button><button class="btn" style="border-radius: 9999px">Rounded</button>With Tailwind in your own build, rounded-full says the same thing.
Spinner
Section titled “Spinner”Put a spinner inside the button for a loading state, and disable the button
while it runs. Give the spinner a data-icon attribute too, so the spacing matches an icon.
<button class="btn" disabled> <svg class="spinner" data-icon="inline-start" role="status" aria-label="Loading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 12a9 9 0 1 1-6.219-8.56"></path> </svg> Saving</button>
<button class="btn btn-outline btn-icon" disabled aria-label="Loading"> <svg class="spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 12a9 9 0 1 1-6.219-8.56"></path> </svg></button>Button Group
Section titled “Button Group”Sit several controls flush with the button group.
<div class="button-group" role="group" aria-label="Text alignment"> <button class="btn btn-outline" type="button">Left</button> <button class="btn btn-outline" type="button">Center</button> <button class="btn btn-outline" type="button">Right</button></div>As Link
Section titled “As Link”<a class="btn" href="#as-link">Anchor as button</a>
<a class="btn btn-outline" href="#as-link">Outline anchor</a>An anchor takes the classes and keeps its own semantics — it navigates, it is in the tab order, and
a screen reader announces it as a link. Do not put role="button" on it to make it look right;
that is exactly the semantic the anchor should keep.
Set dir="rtl" on the page or on any subtree. partialkit is written with logical properties, so
padding, corners, absolute positions and menu placement all follow the reading direction — nothing
to configure.
<div dir="rtl" style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center"> <button class="btn"> <svg data-icon="inline-start" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M19 12H5M12 19l-7-7 7-7"></path> </svg> متابعة </button>
<button class="btn btn-outline" popovertarget="rtl-demo-menu">قائمة</button> <div class="dropdown-menu" id="rtl-demo-menu" popover style="width: 12rem"> <div class="dropdown-menu-label">الحساب</div> <hr class="dropdown-menu-separator" /> <button class="dropdown-menu-checkbox-item" type="button" aria-checked="true" data-pk-keep-open> <span class="dropdown-menu-item-indicator"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M20 6 9 17l-5-5"></path> </svg> </span> شريط الحالة </button> <button class="dropdown-menu-item" type="button"> الإعدادات <span class="dropdown-menu-shortcut kbd-group"><kbd class="kbd">⌘</kbd><kbd class="kbd">,</kbd></span> </button> </div>
<div class="button-group"> <button class="btn btn-outline" type="button">واحد</button> <button class="btn btn-outline" type="button">اثنان</button> </div></div>The pieces that are drawn rather than laid out — the select’s chevron and a submenu’s arrow — are
mirrored explicitly, and the menu positioner flips right-start to left-start on its own.
States
Section titled “States”<button class="btn" disabled>Disabled</button><button class="btn btn-outline" disabled>Disabled</button><button class="btn" aria-invalid="true">Invalid</button><button class="btn btn-outline" aria-expanded="true">Expanded</button>| State | How |
|---|---|
| Disabled | the native disabled attribute — dims it and stops pointer events |
| Invalid | aria-invalid="true" — recolours the border and ring |
| Pressed | automatic; the button shifts down 1px, except when it opens a menu |
| Focus ring | automatic, on :focus-visible — a pointer press does not ring |
| Expanded | aria-expanded="true" — outline, secondary and ghost keep their hover surface |