Skip to content

Buttons

Button

The action button, in three variants. In a modal footer, the primary action sits rightmost with neutral actions to its left (see ModalActions).

tsx
import { Button } from "@silo-code/sdk";

<Button onClick={cancel}>Cancel</Button>
<Button variant="primary" onClick={save}>Save</Button>
<Button variant="danger" onClick={remove}>Delete</Button>
<Button size="sm">Compact</Button>
PropTypeDefaultNotes
variant"normal" | "primary" | "danger""normal"primary = the accent-filled main action (one per footer); danger = destructive
size"normal" | "sm""normal"sm for inline/compact contexts (e.g. a footer start slot)
…restReact.ComponentProps<"button">disabled, onClick, etc. pass through

States (all built in — don't restyle):

  • Hover: variant fill shifts one shade (normal buttons shade toward black in dark themes and toward white in light themes; primary/danger always toward black).
  • Pressed: a further shade plus a slight scale-down.
  • Disabled: 50% opacity, inert — no hover, no press animation, no clicks.
  • Focus: the shared 1px inset accent ring. Never add your own.
ColorToken
Normal fill / text--silo-button-bg / --silo-button-text
Primary fill / text--silo-button-primary-bg (→ accent) / --silo-button-primary-text
Danger fill / text--silo-button-danger-bg (→ err) / --silo-button-danger-text
Hover / active fillsderived (color-mix of the variant fill)

IconButton

A square icon-only button — the kit's one answer for ✕, ⋮, ↻, ✏️, and friends. aria-label is required: the icon is the only visual, so the label is the accessible name.

tsx
import { IconButton } from "@silo-code/sdk";

<IconButton aria-label="Refresh" onClick={refresh}>
  <RefreshIcon size="1em" />
</IconButton>

// compact — e.g. in a ListRow's trailing slot (one common home, not the only one)
<ListRow
  trailing={
    <>
      <IconButton size="sm" aria-label="Pin" onClick={pin}>
        <PinIcon size="1em" />
      </IconButton>
      <IconButton size="sm" aria-label="More options" onClick={menu}>
        <MoreIcon size="1em" />
      </IconButton>
    </>
  }
>
  Agent Monitor
</ListRow>

// panel / breadcrumb toolbar (local-web-viewer tone)
<IconButton size="sm" variant="toolbar" aria-label="Back">
  <ArrowLeft size="1em" weight="bold" />
</IconButton>
PropTypeDefaultNotes
size"normal" | "sm""normal"2.5em / 2em of --silo-font-size-base (Zoom In/Out)
variant"normal" | "toolbar""normal"toolbar — B/W toolbar-text icons, hover fill only, no press-scale
aria-labelstringrequired
…restbutton props

States (normal): transparent at rest (icon at --silo-color-text-lo), bg-hover fill + brighter icon on hover, bg-active fill + scale-down when pressed. toolbar keeps toolbar-text on hover and uses an accent wash when aria-pressed / data-checked is set.

Multiple actions in one row

A ListRow's trailing slot takes several IconButton size="sm" side by side — don't wrap them in extra layout.

Pair it with a Tooltip

aria-label gives screen readers the name; sighted users get nothing from it. Wrap the button in Tooltip so everyone gets the label: <Tooltip content="Refresh"><IconButton aria-label="Refresh">…</IconButton></Tooltip>.

A labelled button that opens a menu — the counterpart to IconButton for cases where a bare doesn't tell anyone what they'd get. Renders its label with a trailing chevron, the standard signal that pressing it reveals more rather than performing something.

tsx
import { MenuButton } from "@silo-code/sdk";

<MenuButton
  label="More"
  onClick={(e) =>
    ctx.ui.showMenu({
      items: [
        { id: "disable", label: "Disable", run: disable },
        { id: "uninstall", label: "Uninstall", run: uninstall },
      ],
      at: e.currentTarget,
    })
  }
/>;

// variant="field" — a value picker inside a `Section`, when a rich menu
// (icons, checks) beats a native <select>
<Section label="Agent">
  <MenuButton
    variant="field"
    label={agent?.displayName ?? "Auto-detect from the command"}
    onClick={(e) => ctx.ui.showMenu({ anchor: e.currentTarget, items })}
  >
    <AgentIconGlyph icon={agent?.icon} mode="color" colorScheme={scheme} />
  </MenuButton>
</Section>;
PropTypeDefaultNotes
labelReactNoderequired — the whole point over IconButton
size"normal" | "sm""normal"sm for card footers and list rows
variant"bare" | "field""bare"field wears Input / Select chrome as a form field
childrenReactNodeoptional leading content, e.g. an icon
…restbutton propsaria-haspopup="menu" is applied for you

Quieter than Button on purpose: bare has no border and no fill at rest, bg-hover on hover. It reveals rather than commits, so it shouldn't compete with the real action beside it. Use variant="field" only when the trigger is the field — a value picker in a form that lines up with Input and Select.

MenuButton or a IconButton?

Both open a menu; they differ in who is expected to find it. Reach for MenuButton when the menu is somewhere a user is meant to go — a bare is discoverable only by people who already know to look, so anything a first-time user needs belongs behind a labelled trigger. Keep IconButton for dense rows and toolbars where a label genuinely won't fit and the actions are secondary.

Neither owns the menu. Open one from onClick with ctx.ui.showMenu, anchoring at: e.currentTarget so it lines up under the trigger.