---
title: Button
description: The pill and the ghost - one action and its alternative, with no third variant on purpose.
source: https://adamjurek.com/components/button
---

## Home


Button is one action and its alternative: `pill` for the one thing a section
wants done, `ghost` for everything else, with no third variant. Pass `href`
and it renders an anchor instead of a `<button>`, because an action that
navigates is a link, not a click handler pretending to be one.

<!-- ::start:showcase demo="button" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

There is no third variant because a row of three button styles is a menu
wearing costumes - `pill` and `ghost` are a hierarchy, not a palette. `href`
switches the rendered element from `<button>` to `<a>`, not the look: the
reader cannot tell a link-shaped action from a button-shaped one, and should
not have to work that out from the styling.

## What it does not do

It does not accept both `href` and `onClick` at once - passing `href` renders
an anchor and `onClick` is dropped, because an action that both navigates and
runs a handler is usually two actions wearing one button.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/button.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/button.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Actions |
| Files | `button.tsx` |
| Dependencies | None |
| Tags | action, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Button } from "@sushindustries/ui";

export function Example() {
	return (
		<>
			<Button href="/get-started">Get started</Button>
			<Button variant="ghost" onClick={() => console.log("clicked")}>
				Learn more
			</Button>
		</>
	);
}
```

## What you should see

A solid pill with a shadow that lifts slightly on hover, next to an
outlined pill with no fill. Both are the same height and shape - `variant`
changes weight, not size. Because the first has `href`, it renders as an
`<a>`; the second has no `href`, so it renders as a real `<button>`.

## If nothing happens

`onClick` is silently dropped whenever `href` is also set - the anchor
navigates and the handler never runs, which is intentional rather than a
bug to work around. A button that looks disabled but still responds to
clicks means `disabled` was set on a link: `href` cannot be disabled, and
the prop only reaches the `<button>` element.


## Guides


## When not to use it

There are exactly two variants and no way to add a third - a row that
wants a primary, a secondary and a tertiary action is a row that has not
decided what actually matters on that screen. If a project genuinely
needs a third weight, that is a sign the section has too many equally
important actions, not a missing prop.

## href decides the element, not just the look

`href` swaps the rendered tag from `<button>` to `<a>` - it is not a
style switch. That matters for anything reading the DOM rather than the
pixels: a link-shaped action is in the browser's navigation history and
works with open-in-new-tab, a button-shaped one is not. Pass `href` for
anything that changes the URL, and `onClick` for anything that does not,
rather than reaching for `onClick` with a manual `window.location`
inside it.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - |  |
| `href?` | `string` | - | Renders an anchor instead. A button that navigates is a link. |
| `onClick?` | `MouseEventHandler<HTMLButtonElement>` | - | Dropped when `href` is set - the anchor navigates instead. |
| `variant?` | `"pill" \| "ghost"` | `"pill"` | `pill` is the one action a section wants taken; `ghost` the alternative. |
| `type?` | `"button" \| "submit"` | `"button"` | `submit` is the only reason a button in a form should be anything else. |
| `disabled?` | `boolean` | - | Reaches the button only. An `href` cannot be disabled - do not render it. |

<!-- /generated:api -->

## Notes

`onClick` is accepted but ignored whenever `href` is also set - the
anchor navigates and no handler runs, so the two are effectively
exclusive even though the types allow passing both. `disabled` only
reaches the `<button>` element; passing it alongside `href` has no
effect, since an anchor has no disabled state to set.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="button" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Button } from "@sushindustries/ui";

export function CtaRow() {
	return (
		<div className="flex gap-3">
			<Button href="/packages">Browse packages</Button>
			<Button variant="ghost" href="https://github.com/sushindustries">
				View source
			</Button>
		</div>
	);
}
```

## What this example is not

Both buttons here are links, not handlers. A form's submit button is a
different shape entirely - `<Button type="submit">` with no `href`, so it
posts the form instead of navigating - and that case belongs with the
form it submits, not repeated here on its own.
