Components/Button

Button

The largest page in the kit. Two published components, 264 variants across three axes, plus two hidden icon layers that would have made it four times bigger.

Button · Components frameLive in the browser · drawn to match the file
Component
Variant
State
Size
Button
Variant=Default · State=Default · Size=default32px tall in the fileshown at 2×
Six by six by four, twice

Two hundred and sixty-four variants

Button is six variants across six states across four sizes: 144. Icon Button repeats it without Link, because an icon-only link is a ghost icon button: 120. The grid below is the colour half, at the default size - the sizes are in the table under it.

VariantDefaultHoverFocusPressedDisabledLoading
Button Button Button Button Button Button
Secondary Button Button Button Button Button Button
Outline Button Button Button Button Button Button
Ghost Button Button Button Button Button Button
Destructive Button Button Button Button Button Button
Link Button Button Button Button Button Button
Four sizes each

Height moves, padding mostly does not

Only xs has its own padding. sm, default and lg all sit at spacing.2-5, so the difference between them is height, radius, label size and icon size. lg and default differ by four pixels of height and nothing else at all.

SizeHeightPaddingGapRadiusLabelIconActual
xs24px8px4pxradius.md12px12pxButton
sm28px10px4pxradius.md12px12pxButton
32px10px6pxradius.lg14px16pxButton
lg36px10px6pxradius.lg14px16pxButton
icon-xs · 24px
icon-sm · 28px
icon · 32px
icon-lg · 36px
What is not an axis

Two hidden icons, one visible spinner

Every Button carries leftIcon and rightIcon layers that ship hidden. Reveal either on a copy - the checkboxes above do it. As axes they would have taken 264 variants to 1,056, and an icon at the end of a row moves nothing beside it.

Loading is different, and that is why it is a state: it adds a visible spinner before the label, which widens the button from 64 to 86. Something that changes the width has to be a real variant, or every copy would need resizing by hand.

As it ships Button
leftIcon revealed Button
rightIcon revealed Button
State = Loading Button
how Pressed and Disabled are drawn

Neither changes a colour. Disabled drops the whole button to 50% opacity, Pressed to 60%, and everything else stays where it was. One rule for six variants, instead of twelve more colours to keep in step - and it is why Disabled still reads as the variant it is rather than turning into a generic grey.

In the file

Two components, 264 variants

Read straight out of the Penpot file. The axes below are the property names you will see in the variant dropdown.

ButtonVariant × State × SizeDefault · Secondary · Outline · Ghost · Destructive · Link × Default · Hover · Focus · Pressed · Disabled · Loading × xs · sm · default · lg144
Icon ButtonVariant × State × Sizethe same, less Link × the same six states × icon-xs · icon-sm · icon · icon-lg120

Using the Button

Because the spinner changes the width. Loading widens the default button from 64 to 86, and anything that moves the geometry has to be a real variant or every copy needs resizing by hand. leftIcon and rightIcon sit at the ends of the row and move nothing, so they ship as hidden layers - as axes they would have taken 264 variants to 1,056.

Because an icon-only link is a ghost icon button. Link is a text treatment - it underlines on hover and carries no fill - and there is no text to underline. Dropping it is what makes Icon Button 120 rather than 144.

They change opacity instead: 60% pressed, 50% disabled, and nothing else moves. One rule covers all six variants, so a disabled destructive button still reads as destructive rather than turning into a generic grey - and there are no twelve extra colours to keep in step with the rest.

Four pixels of height, and nothing else. Both sit at spacing.2-5 padding, radius.lg, a 14px label and 16px icons. Only xs has its own padding. It is worth knowing before you reach for lg expecting more room inside it.

Forms · 264 variants