Skip to content

Button

A control that runs an action. Use it for anything the user deliberately triggers: submitting a form, saving, deleting.

tsx
import { Button } from 'neba';

<Button onClick={save}>Save</Button>;

Props

PropTypeDefaultDescription
variantshared'solid' | 'outline' | 'text''solid'Weight of the surface: filled, hairline, or none
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale. xs 22px · sm 26px · md 32px · lg 40px · xl 48px
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding only: never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 is flat; hover adds a level and pressing removes one
startIconReactNodeContent before the label. Sized in em, so it tracks the label
endIconReactNodeContent after the label
loadingbooleanfalseSpinner in place of startIcon; stops activation but keeps focus
readOnlybooleanfalseInert but not dimmed: the action exists, it just is not available here
disabledbooleanfalseUnavailable. Drops the colour family for neutral grey and leaves the tab order
fullWidthbooleanfalseStretches to the width of the container
renderuseRender.RenderPropRenders something other than a button (an <a href>, a router Link). A link stays a link, so crawlers and screen readers still see one
childrenReactNodeThe label. Omit it and the button goes square for an icon

Every native <button> attribute passes straight through. The one exception is color, omitted because it collides with the color in the table above.

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

Examples

variant

solid is the primary action, outline a secondary one, text a low-weight action for a list or a toolbar. Keep one solid per screen.

color

Six role colours only; arbitrary colour values are not accepted.

size

Sets the height and the type scale together: xs 22px · sm 26px · md 32px · lg 40px · xl 48px. md is the desktop default.

density

density changes horizontal padding and nothing else. Two buttons of the same size are the same height whatever their density, so a mixed row keeps its baseline.

startIcon and endIcon

Icons are drawn at 1.2em, so they track the label and never need a size of their own. With icons but no children the button goes square, and then it needs an aria-label: for an icon-only control, IconButton requires label instead.

loading · readOnly · disabled

propAppearanceFocusNative disabled
loadingUnchanged; a spinner takes the startIcon slotKeptNo
readOnlyKeeps its colour, goes flat, drains saturationKeptNo
disabledDrops the colour family for neutral greyLostYes

None of the three let a click reach the parent.

elevation

Drop shadow depth. The default 0 means no shadow at all. Hovering adds a level and pressing removes one, so even a 0 button answers a press.

fullWidth

Stretches to the width of the container.

render

Renders something other than a <button>. An action that navigates should be an <a href>: a crawler follows it, it appears in a screen reader's list of links, and the browser's own behaviour (open in a new tab, copy the address) keeps working. A router's Link goes in the same way.

The surface, the sizes and the press signature are unchanged. An <a> has no disabled, so a button that has to be unavailable stays a <button>.

Accessibility

  • Renders a native <button> by default. type passes through, so type="submit" works inside a form.
  • Changing the element with render keeps that element's semantics: an <a href> stays a link rather than being covered by role="button".
  • Give icon-only buttons an aria-label.
  • The focus ring only appears on :focus-visible, so a mouse click never draws one.
  • loading and readOnly keep focus: dropping out of the tab order costs keyboard users their sense of the page.
  • Every colour combination meets 4.5:1 for text on the fill.

Released under the MIT License