Skip to content

Theming

Vue Pick uses CSS custom properties for all visual styling. Overrides can be applied globally in your root stylesheet, scoped to a container, or set inline on a single instance.

Overriding variables

css
/* Global override in your main CSS file */
:root {
  --vpick-border-radius: 0;
  --vpick-border-color: #d1d5db;
}
vue
<!-- Scoped override, affects only instances inside this container -->
<style scoped>
.my-form {
  --vpick-border-radius: 0px;
  --vpick-bg: #f9fafb;
}
</style>
vue
<!-- Inline override on a single instance -->
<VPickNative
  :options="options"
  style="--vpick-border-radius: 9999px; --vpick-border-color: #6366f1;"
/>

Shared variables

These apply to both VPickNative and VPick.

VariableDefault
--vpick-font-familyinherit
--vpick-font-size0.875rem
--vpick-line-height1.25rem
--vpick-widthfit-content
--vpick-border-color#e5e5e5
--vpick-border-radius0.625rem
--vpick-bgtransparent
--vpick-text-colorinherit
--vpick-placeholder-color#737373
--vpick-icon-color#737373
--vpick-focus-border-color#a1a1a1
--vpick-focus-ring-colorrgba(161, 161, 161, 0.5)
--vpick-error-border-color#dc2626
--vpick-error-bgrgba(220, 38, 38, 0.05)
--vpick-error-ring-colorrgba(220, 38, 38, 0.2)
--vpick-disabled-opacity0.5
--vpick-height-default2rem
--vpick-shadow0 0 0 0 transparent

VPickNative variables

VariableDefault
--vpick-native-widthinherits from --vpick-width (fit-content)

--vpick-native-width is an optional override for native-only width when you need the native and custom triggers to be sized differently. By default it inherits from --vpick-width so a single override styles both.

VPick variables

VariableDefault
--vpick-option-radius0.5rem
--vpick-option-padding-block0.25rem
--vpick-option-gap0.125rem
--vpick-option-padding-inline-start0.375rem
--vpick-listbox-bg#fff
--vpick-listbox-shadow0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)
--vpick-listbox-ringrgba(0, 0, 0, 0.06)
--vpick-listbox-max-height16rem
--vpick-listbox-z-index50
--vpick-option-hover-bg#f5f5f5
--vpick-option-highlight-bg#f5f5f5
--vpick-option-selected-colorinherit
--vpick-option-selected-bg#e3f2fd
--vpick-option-selected-weightinherit
--vpick-option-check-colorcurrentColor
--vpick-option-empty-icon-colorinherit
--vpick-group-label-color#737373
--vpick-group-label-size0.75rem
--vpick-tree-indent1.375rem

--vpick-tree-indent is the width of one level of nesting in tree mode, applied per depth. The default matches the width of the expand chevron and its gap, so a child's chevron lines up under its parent's label.

--vpick-option-gap is the space between consecutive rows. Rows carry backgrounds for hover and selection, and stacked flush those backgrounds merge into one block; the gap keeps each row reading as its own object. Set it to 0 for a continuous list.

The selected row

In single-select the row holding the current value is tinted with --vpick-option-selected-bg, so it can be found at a glance rather than by looking for the check icon. It outranks the hover and keyboard highlight, so a selected row keeps its tint while hovered. multiple is excluded, since the checkboxes already carry that meaning.

--vpick-option-selected-weight is off by default, since bold text is wider and would nudge the selected row's label. Set it to 600 to bold the row as well, or set the tint to transparent to rely on the check icon alone.

vue
<VPick
  :options="options"
  style="--vpick-option-selected-bg: #f4f4f5; --vpick-option-selected-weight: 600"
/>

--vpick-option-padding-inline-start is the row's own padding, before any tree indent. Changing it shifts every row, including the placeholder under an empty branch, and leaves the nesting steps intact.

--vpick-option-empty-icon-color colors the icon in the no-children-icon slot, the one shown on the placeholder row under an empty branch. That icon is authored in your own template but renders inside the teleported panel, so scoped CSS cannot reach it. Set the variable on the component instead and have the icon draw with currentColor:

vue
<VPick :options="categories" style="--vpick-option-empty-icon-color: #f97316">
  <template #no-children-icon>
    <svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
      <circle cx="8" cy="8" r="6" />
    </svg>
  </template>
</VPick>

It defaults to inherit, so an icon left alone still takes the row's color.

Branch rows

Branch rows carry .vpick-option--branch and take two further variables. Both fall back to the row-wide value, so setting only the values above still moves branches with everything else.

VariableDefault
--vpick-option-branch-padding-blockvar(--vpick-option-padding-block)
--vpick-option-branch-weightinherit

Branch rows read as section headings once disableBranchNodes makes them unselectable, which is when these are usually worth setting:

vue
<VPick
  :options="categories"
  multiple
  disable-branch-nodes
  style="--vpick-option-branch-weight: 600; --vpick-option-branch-padding-block: 0.4375rem"
/>

Set them inline like this rather than in a global stylesheet. The panel is teleported out of the component, so a scoped rule cannot reach it and a plain class rule would restyle every VPick on the page. Variables set on the component are forwarded to the panel, which keeps the change to that one instance.

Multiselect checkbox variables

multiple renders a checkbox on each option row. These style it.

VariableDefault
--vpick-checkbox-bgtransparent
--vpick-checkbox-bg-checked#18181b
--vpick-checkbox-bordervar(--vpick-border-color)
--vpick-checkbox-border-checked#18181b
--vpick-checkbox-color#fff
--vpick-checkbox-radius0.25rem

Clear button and empty state variables

VariableDefault
--vpick-clear-colorvar(--vpick-icon-color)
--vpick-clear-hover-bgvar(--vpick-option-hover-bg)
--vpick-icon-button-hover-bgvar(--vpick-option-hover-bg)
--vpick-empty-colorvar(--vpick-placeholder-color)
--vpick-empty-padding0.5rem 0.625rem

Each inherits from a more general variable, so overriding the general one styles these too and the specific one is only needed to break that link.

Multiselect chip variables

multiple renders each selected value as a chip in the trigger.

VariableDefault
--vpick-chip-bg#f5f5f5
--vpick-chip-colorinherit
--vpick-chip-bordertransparent
--vpick-chip-radius0.375rem
--vpick-chip-font-size0.75rem
--vpick-chip-remove-colorcurrentColor
--vpick-chip-remove-hover-bgrgba(0, 0, 0, 0.1)
--vpick-chip-transition-duration150ms

Chips scale up as they are added and shrink away as they are removed, with the remaining chips sliding across to close the gap. This variable tunes how fast that runs:

vue
<VPick
  v-model="selected"
  :options="options"
  multiple
  style="--vpick-chip-transition-duration: 80ms"
/>

To switch the motion off entirely, use the animate prop rather than a zero duration. A duration of zero still runs the transition, just with no time to run it in, which leaves the chips mid-reflow for a frame.

Inside another container

Dropping a control into a popover, card or panel brings up three things worth knowing before you spend an evening on them.

Do not clip the container. The trigger's focus ring is drawn 3px outside its box, so overflow: hidden on the container cuts it off and a keyboard user loses the only cue telling them where they are. If square list corners are poking over a rounded frame, round the list itself instead:

css
.my-panel .vpick-listbox {
  border-end-start-radius: 0.3rem;
  border-end-end-radius: 0.3rem;
}

An edge-to-edge trigger wants an inset ring. When the trigger runs the full width of its container there is no room outside it for a ring to sit, so draw it inward:

css
.my-panel .vpick-trigger:focus,
.my-panel .vpick-trigger:focus-within {
  box-shadow: inset 0 0 0 2px var(--vpick-focus-ring-color);
}

Percentages resolve against --vpick-width. Once you pin it, width: 100% inside the control means 100% of that pinned value, not of the container. To bleed the list a pixel past the edge you need both the offset and the width:

css
.my-panel .vpick-listbox {
  margin-inline: -1px;
  width: calc(100% + 2px);
}

Dark mode

There is no dark palette yet. The defaults above are a light theme, and nothing switches on prefers-color-scheme. This site's own appearance toggle is disabled for that reason.

Everything visual is a variable, though, so a dark theme is a block of overrides under whatever selector your app already uses:

css
.dark {
  --vpick-border-color: #3f3f46;
  --vpick-bg: #18181b;
  --vpick-text-color: #fafafa;
  --vpick-placeholder-color: #a1a1aa;
  --vpick-icon-color: #a1a1aa;
  --vpick-listbox-bg: #18181b;
  --vpick-listbox-ring: rgba(255, 255, 255, 0.08);
  --vpick-option-hover-bg: #27272a;
  --vpick-option-highlight-bg: #27272a;
  --vpick-option-selected-bg: #1e3a5f;
  --vpick-chip-bg: #27272a;
  --vpick-checkbox-bg-checked: #fafafa;
  --vpick-checkbox-border-checked: #fafafa;
  --vpick-checkbox-color: #18181b;
}

Set these on :root or a wrapper rather than on the component, so the listbox picks them up too. The panel is teleported, and only variables set on the component are forwarded to it; ones set higher up are inherited normally.

Reduced motion

Every transition is disabled automatically for visitors whose system asks for less motion, via prefers-reduced-motion: reduce. The loading spinner keeps turning, since it reports progress rather than decorating.

Released under the MIT License.