Select
A closed field in six states, a panel in four colours, and a row that keeps the tick's width whether or not the tick is there.
The trigger draws twice for every situation: once with the placeholder in base.muted-foreground and once with a chosen value in base.foreground. That difference in colour is the only signal that a selection has been made, so it is worth having both in the file rather than typing over one and hoping the colour follows.
Filled (Focus) exists because the two are independent: a field can hold a value and still be the one you are on. Rather than make Filled a second axis, the file spells out the four combinations it needs, which keeps the dropdown to six readable names.
default is 32 and sm is 28, and both carry the same 4 / 12 padding and the same 14px type. The height is set on the board and the row centres inside it, so sm is a tighter box around identical contents rather than a smaller copy of the component.
A surface inside a border
324 by 172, and it is two boards deep on purpose. The outer one carries the base.foreground edge, the radius and the shadow; the inner Surface carries the fill and the 4 of padding. That split is what lets the Inverted colour change the fill without touching the edge.
Appearance drops the surface to 70 percent and lets what is behind it through, on either colour. The Solid fills carry tokens - base.popover and colors.popover-dark - while the Translucent ones are held as literals, because rebinding a colour token in Penpot resets its alpha to 1 and the whole point of these two is the alpha. That is the convention across the kit.
A tick that takes up space whether or not it is there
Every row carries a 20 by 16 Spacer at its right end. The check is an absolute child sitting inside that Spacer, so a chosen row and an unchosen one are exactly the same width and the labels do not shift by sixteen pixels the moment you pick something.
The name is about the shape, not the behaviour. Both Checkbox states draw the tick, so it is the variant you place on whichever row is currently selected; a Default row is any other row. In a multi-select every chosen row takes it, which is the arrangement the usage frame shows.
Hover fills with base.muted and belongs on a white menu; the label stays base.foreground, because a hover is a background change and nothing else. Hover (Translucent) fills with base.foreground and belongs on an Inverted panel, where a pale grey would disappear - that one has to flip its label, since the row has gone nearly black underneath it.
The difference is the Surface's 4 of padding on each side, plus the row's own inset. A row placed straight into a panel with no padding will sit against the edge and the rounded corner will clip its highlight, which is the usual reason a menu looks slightly wrong.
Open one and pick something
Trigger, panel, label, separator and rows, wired together. Click to open, hover a row, pick a value and watch the trigger's text change colour from base.muted-foreground to base.foreground. Arrow keys move through the list once it is open, escape closes it.
Seven arrangements
964 by 942. Four of them place real instances of the trigger; the last three draw their panels locally, because a component copy in Penpot cannot take new children and a menu is nothing but children.
The chevron changes ends, the tick follows it
Three things move at once: the value goes to the right of the trigger and the chevron to the left, the panel hangs off the other edge, and each row's tick crosses to the far side with its Spacer. None of it needs a variant - the chevron points down either way, and the tick is symmetrical enough to leave alone.
Worth knowing where this bites: the panel was pinned with left: 0, which looks right until the first RTL screen puts it under the wrong edge of a narrow trigger. inset-inline-start costs nothing and holds in both directions, and the same swap fixes the row's text alignment.
Five components, twenty-eight variants
Read straight out of the Penpot file. The axes below are the property names you will see in the variant dropdown.
Using the Select
Because the colour changes with it. A placeholder is base.muted-foreground and a chosen value is base.foreground, and that difference is the only thing telling you whether the field has been answered. Typing over the placeholder in an instance gives you the right words in the wrong grey, so the file draws both.
Because the two are independent and Penpot needs the combination spelled out. A field can hold a value and still be the one you are on. Making Filled a second axis would have doubled the container for one extra useful cell, so the six State names carry the four combinations that actually occur.
Keeping the width honest. Each row ends with a 20 by 16 Spacer, and the tick is an absolute child positioned inside it. A chosen row and an unchosen one are therefore exactly the same width, and picking a value does not shift every label in the list sideways by sixteen pixels.
Chosen. Both of its states draw the tick, so it is the variant you place on the currently selected row while everything else stays Default. In a multi-select every ticked row takes it. The name describes the mark, not a different kind of row.
One per surface. Hover fills with base.muted and reads on a white panel, keeping its label at base.foreground because a hover is a background change and nothing more. Hover (Translucent) fills with base.foreground and reads on the Inverted one, where a pale grey would vanish, and it does flip its label - the row has gone nearly black underneath it. The name is still misleading: the fill is fully opaque; it is the hover for the translucent panel, not a translucent hover.
So the colour and the edge can move independently. The outer board owns the base.foreground border, the radius.lg corner and the two drop shadows; the inner Surface owns the fill and the 4 of padding. Inverted changes the Surface's fill and the outer edge without either having to know about the other.