Skip to content
kerfjsGitHub

Button

Runs an action. Use one main button per screen, and quieter ones around it.

SourcePackage @kerf-js/veneerTry it

Preview

tsx
import { Button } from '@kerf-js/veneer'export function Default() {  return (  <Button>Deploy</Button>  )}

Installation

npm install @kerf-js/veneer

Try it

Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.

Live preview
Controls
Presets
Emphasis
Size
Codeupdates as you change the controls
import { Button } from '@kerf-js/veneer'<Button>Deploy</Button>

Usage

tsx
import { Button } from '@kerf-js/veneer'<Button>Deploy</Button>

Variants

Use emphasis to set how loud the button is, from solid down to link.

tsx
import { Button } from '@kerf-js/veneer'export function Emphasis() {  return (  <>    <Button>Deploy</Button>    <Button emphasis="frame">Deploy</Button>    <Button emphasis="soft">Deploy</Button>    <Button emphasis="outline">Deploy</Button>    <Button emphasis="ghost">Deploy</Button>    <Button emphasis="link">Deploy</Button>  </>  )}

Sizes

Use size for sm, md or lg.

tsx
import { Button } from '@kerf-js/veneer'export function Sizes() {  return (  <>    <Button size="sm">Deploy</Button>    <Button>Deploy</Button>    <Button size="lg">Deploy</Button>  </>  )}

Tones

Use tone to say what the action means. critical is for anything destructive.

tsx
import { Button, Row, Stack } from '@kerf-js/veneer'export function Tones() {  return (  <Stack gap="related">    {TONES.map((tone) => (      <Row        key={tone}        gap="related"      >        <Button tone={tone}>Deploy</Button>        <Button          tone={tone}          emphasis="soft"        >          Deploy        </Button>        <Button          tone={tone}          emphasis="outline"        >          Deploy        </Button>        <Button          tone={tone}          emphasis="ghost"        >          Deploy        </Button>      </Row>    ))}  </Stack>  )}const TONES = [  'neutral',  'accent',  'positive',  'caution',  'critical',  'info',] as const

Fill that follows

By default the fill wipes in from the left and out to the right. Add follow and, at vivid, it spreads from where the pointer comes in instead.

tsx
import { Button } from '@kerf-js/veneer'export function Follow() {  return (  <>    <Button>Wipe</Button>    <Button follow>Follow</Button>    <Button      follow      emphasis="soft"    >      Follow    </Button>    <Button      follow      emphasis="outline"      tone="neutral"    >      Follow    </Button>  </>  )}

Icons

Use icon to draw one before the label.

tsx
import { Button } from '@kerf-js/veneer'export function WithIcon() {  return (  <>    <Button icon="rocket">Deploy</Button>    <Button      emphasis="outline"      tone="neutral"      icon="plus"    >      New project    </Button>    <Button      emphasis="ghost"      tone="critical"      icon="trash"    >      Delete    </Button>  </>  )}

Icon only

Needs label: it becomes the accessible name.

tsx
import { Button } from '@kerf-js/veneer'export function IconOnly() {  return (  <>    <Button      icon="plus"      label="New project"    />    <Button      emphasis="outline"      tone="neutral"      icon="settings"      label="Settings"    />    <Button      emphasis="ghost"      tone="neutral"      icon="trash"      label="Delete"    />  </>  )}

Full width

Use full to take the width of the container.

tsx
import { Button } from '@kerf-js/veneer'export function Full() {  return (  <Button full>Deploy to production</Button>  )}

Loading

The label stays the accessible name, presses are swallowed and focus stays put.

tsx
import { Button } from '@kerf-js/veneer'export function Loading() {  return (  <>    <Button loading>Deploying</Button>    <Button      emphasis="outline"      tone="neutral"      loading    >      Deploying    </Button>  </>  )}

Disabled

Prefer aria-disabled: the button stays focusable, so people can find out why.

tsx
import { Button } from '@kerf-js/veneer'export function Disabled() {  return (  <>    <Button aria-disabled="true">Deploy</Button>    <Button      emphasis="outline"      tone="neutral"      aria-disabled="true"    >      Deploy    </Button>    <Button disabled>Deploy</Button>  </>  )}

Delete confirmation

Give the confirming button tone="critical" and the way out a quiet ghost button.

tsx
import { Button, Row, Stack, Text } from '@kerf-js/veneer'export function DeleteProject() {  return (  <Stack gap="related">    <Text role="heading">Delete storefront?</Text>    <Text subtle>      Its 412 deploys and all its logs go too. This can't      be undone.    </Text>    <Row      gap="related"      justify="end"    >      <Button        emphasis="ghost"        tone="neutral"      >        Keep it      </Button>      <Button tone="critical">Delete project</Button>    </Row>  </Stack>  )}

Empty state

A single button with an icon gives an empty view one clear next step.

tsx
import { Button, Stack, Text } from '@kerf-js/veneer'export function EmptyState() {  return (  <Stack    gap="related"    align="center"  >    <Text role="heading">No projects yet</Text>    <Text subtle>      Connect a repository and your first deploy runs in      about a minute.    </Text>    <Button icon="plus">New project</Button>  </Stack>  )}

Save with progress

Set loading while the work runs, then confirm with a toast.

tsx
import { state } from '@kerf-js/core'import { Button, Toaster, toast } from '@kerf-js/veneer'export function SaveAsync() {  const saving = state(false)  const save = async () => {    saving.value = true    await new Promise((done) => {      setTimeout(done, 1500)    })    saving.value = false    toast.show({      title: 'Saved',      description: 'Your changes are live.',    })  }  return (    <>      <Button        loading={saving.value}        onClick={save}      >        {saving.value ? 'Saving' : 'Save changes'}      </Button>      <Toaster />    </>  )}

When to use

Use it for

  • The press changes something here and now
  • One main action per view, with quieter ones around it

Reach for something else

  • Link, when it goes to another page
  • Toggle, when it stays pressed
  • Menu, when there are several actions and little room

Accessibility

Enter
Presses the button
Space
Presses the button
  • It is a real <button>, so it works with every assistive technology and inside forms.
  • An icon-only button must have label.

Theming

Restyle it app-wide from veneer() in your kerf config. Every key takes colour, spacing, size, radius, shadow, type and motion; its own keys are listed. A state nests inside: hover: { bg: 'ink' }. Text on a fill you set is contrast-checked at build. How theming works

KeyOwn keysStates
parts.buttonbg, text, border, radiushover, focus, press, active, open, disabled
parts.button.softbg, text, borderhover, focus, press, active, open, disabled
parts.button.outlinebg, text, borderhover, focus, press, active, open, disabled
parts.button.ghostbg, text, borderhover, focus, press, active, open, disabled
parts.button.framebg, text, borderhover, focus, press, active, open, disabled
parts.button.linkbg, text, borderhover, focus, press, active, open, disabled
parts.iconButtonradius, iconhover, focus, press, active, open, disabled
ts
veneer({  colors: { brand: '#e8590c', ink: '#1b1b1f' },  parts: {    button: { bg: 'brand', text: 'ink', radius: 'pill', hover: { bg: 'ink', text: 'white' } },    iconButton: { radius: 8, icon: 18 },  },})

API Reference

PropTypeDefaultDescription
emphasisEmphasis'solid'solid (default) · soft · outline · ghost · frame (the signature) · link. Fixed at creation: it picks the anatomy.
toneTone—What the action means. critical for anything destructive.
size"sm" | "md" | "lg"—sm, md or lg. Each follows the density around it.
fillbooleanfalseA frame button with the solid fill sweeping in.
followbooleanfalseAt vivid, the hover fill spreads from where the pointer comes in instead of wiping in from the left.
direction"forward" | "back"—Which way the fill wipes: back comes in from the end edge and leaves by the start, for Prev, minus and undo.
fullbooleanfalseTakes the whole width of its container.
iconIconRef—An icon before the label. Alone, it needs label.
collapse"tablet" | "phone"—With icon and text: on a phone (or a tablet and a phone) only the icon shows, and the text stays the accessible name.
labelstring—The accessible name, for an icon-only button.
loadingbooleanfalseShows a spinner, keeps the button's width and swallows presses.
childrenunknown—What it contains.
EventTypeDefaultDescription
onClick((e: MouseEvent) => unknown)—Called when pressed.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="button"] [data-part="collapse"]

collapselabelspinnersweep

RelatedToggleMenuChip