Skip to content

feat(VSwitch): align with MD3 spec#22879

Merged
J-Sek merged 5 commits into
devfrom
feat/vswitch-md3
May 30, 2026
Merged

feat(VSwitch): align with MD3 spec#22879
J-Sek merged 5 commits into
devfrom
feat/vswitch-md3

Conversation

@J-Sek
Copy link
Copy Markdown
Contributor

@J-Sek J-Sek commented May 30, 2026

resolves #22164

  • opt-in - inset="material" guards the updated look
  • new thumb-color prop
image

Markup:

<template>
  <v-app class="bg-surface">
    <v-defaults-provider :defaults="{ VSwitch: { flat, inset, hideDetails: true } }">
      <v-container>
        <v-card min-height="360">
          <v-card-text class="d-flex">
            <div class="pa-2 flex-grow-1">
              <v-theme-provider theme="light">
                <v-sheet>
                  <v-row class="justify-center align-center py-12 border-md" gap="24">
                    <v-switch :model-value="false" />
                    <v-switch :model-value="true" />
                  </v-row>
                </v-sheet>
              </v-theme-provider>
              <v-theme-provider theme="dark">
                <v-sheet>
                  <v-row class="justify-center align-center py-12 border-md" gap="24">
                    <v-switch :model-value="false" />
                    <v-switch :model-value="true" />
                  </v-row>
                </v-sheet>
              </v-theme-provider>
            </div>
            <div class="pa-2 flex-grow-1">
              <v-theme-provider theme="light">
                <v-sheet>
                  <v-row class="justify-center align-center py-12 border-md" gap="24">
                    <v-switch :model-value="false" color="primary" />
                    <v-switch :model-value="true" color="primary" />
                  </v-row>
                </v-sheet>
              </v-theme-provider>
              <v-theme-provider theme="dark">
                <v-sheet>
                  <v-row class="justify-center align-center py-12 border-md" gap="24">
                    <v-switch :model-value="false" color="primary" />
                    <v-switch :model-value="true" color="primary" />
                  </v-row>
                </v-sheet>
              </v-theme-provider>
            </div>
            <v-defaults-provider :defaults="{ VSwitch: { trueIcon: '$complete', falseIcon: '$close' } }">
              <div class="pa-2 flex-grow-1">
                <v-theme-provider theme="light">
                  <v-sheet>
                    <v-row class="justify-center align-center py-12 border-md" gap="24">
                      <v-switch :model-value="false" color="primary" />
                      <v-switch :model-value="true" color="primary" />
                    </v-row>
                  </v-sheet>
                </v-theme-provider>
                <v-theme-provider theme="dark">
                  <v-sheet>
                    <v-row class="justify-center align-center py-12 border-md" gap="24">
                      <v-switch :model-value="false" color="primary" />
                      <v-switch :model-value="true" color="primary" />
                    </v-row>
                  </v-sheet>
                </v-theme-provider>
              </div>
            </v-defaults-provider>
            <v-defaults-provider :defaults="{ VSwitch: { trueIcon: '$complete', disabled: true } }">
              <div class="pa-2 flex-grow-1">
                <v-theme-provider theme="light">
                  <v-sheet>
                    <v-row class="justify-center align-center py-12 border-md" gap="24">
                      <v-switch :model-value="false" color="primary" />
                      <v-switch :model-value="true" color="primary" />
                    </v-row>
                  </v-sheet>
                </v-theme-provider>
                <v-theme-provider theme="dark">
                  <v-sheet>
                    <v-row class="justify-center align-center py-12 border-md" gap="24">
                      <v-switch :model-value="false" color="primary" />
                      <v-switch :model-value="true" color="primary" />
                    </v-row>
                  </v-sheet>
                </v-theme-provider>
              </div>
            </v-defaults-provider>
          </v-card-text>
        </v-card>
        <v-card class="mt-4">
          <v-card-text>
            <div class="pa-2 flex-grow-1">
              <v-row>
                <v-col v-for="[color, thumbColor] in colors" :key="color" cols="12" md="4" sm="6">
                  <div class="d-flex align-center ga-3">
                    <v-avatar :color="thumbColor ?? color" size="32" />
                    <v-switch
                      v-model="model"
                      :color="color"
                      :label="color"
                      :thumb-color="thumbColor"
                      :value="color"
                      hide-details
                    />
                  </div>
                </v-col>
              </v-row>
            </div>
          </v-card-text>
        </v-card>
      </v-container>
    </v-defaults-provider>
    <div class="d-flex flex-column ga-2 ma-3 position-absolute bottom-0 right-0">
      <v-btn
        class="align-self-end"
        icon="mdi-palette"
        variant="tonal"
      >
        <v-icon />
        <v-menu
          :close-on-content-click="false"
          activator="parent"
          offset="8"
        >
          <v-sheet
            class="d-flex flex-column ga-3 align-start pa-3 border"
            max-width="500"
            rounded="xl"
          >
            <v-btn
              :text="current.dark ? 'Dark' : 'Light'"
              prepend-icon="mdi-theme-light-dark"
              variant="tonal"
              rounded
              @click="$vuetify.theme.cycle()"
            />
            <span class="text-caption font-weight-medium">Primary</span>
            <v-item-group v-model="primaryColor" class="d-flex ga-2 flex-wrap" mandatory>
              <v-item
                v-for="[key, c] of swatchColors"
                :key="key"
                v-slot="{ isSelected, toggle }"
                :value="key"
              >
                <v-icon-btn
                  :color="c[current.dark ? 'lighten1' : 'darken1']"
                  :icon="isSelected ? '$complete' : ''"
                  @click="toggle"
                />
              </v-item>
            </v-item-group>
            <span class="text-caption font-weight-medium">Surface</span>
            <v-item-group v-model="surfaceColor" class="d-flex ga-2 flex-wrap" mandatory>
              <v-item
                v-for="[key, c] of Object.entries(surfaceColors)"
                :key="key"
                v-slot="{ isSelected, toggle }"
                :value="key"
              >
                <v-icon-btn
                  :color="c.base"
                  :icon="isSelected ? '$complete' : ''"
                  @click="toggle"
                />
              </v-item>
            </v-item-group>
          </v-sheet>
        </v-menu>
      </v-btn>
      <v-btn
        :text="`inset: ${inset || 'OFF'}`"
        variant="tonal"
        @click="inset = insets[(insets.indexOf(inset) + 1) % insets.length]"
      />
      <v-btn
        :text="`flat: ${flat ? 'ON' : 'OFF'}`"
        variant="tonal"
        @click="flat = !flat"
      />
    </div>
  </v-app>
</template>

<script setup>
  import { useTheme } from 'vuetify'
  import { ref, shallowRef, watch } from 'vue'
  import paletteColors from '../src/util/colors'

  // toggle in the top-right corner cycles inset off → tonal (legacy) → material (MD3)
  const insets = [false, 'tonal', 'material']
  const inset = ref('material')

  // toggle in the top-right corner to compare default vs flat (no thumb shadow) switches
  const flat = ref(false)

  const { themes, current, change } = useTheme()

  change('dark')

  // palette swatches in the top-right menu (skip the shades bucket — no darken/lighten1)
  const swatchColors = Object.entries(paletteColors).filter(([key]) => key !== 'shades')

  const primaryColor = shallowRef('deepPurple')
  watch(primaryColor, v => {
    themes.value.light.colors.primary = paletteColors[v].darken1
    themes.value.dark.colors.primary = paletteColors[v].lighten4
    themes.value.dark.colors['on-primary'] = paletteColors[v].darken3
  }, { immediate: true })

  const surfaceColors = {
    slate: { base: '#64748b', lighten: '#e2e8f0', darken: '#0f172a' },
    gray: { base: '#6b7280', lighten: '#e5e7eb', darken: '#111827' },
    zinc: { base: '#71717a', lighten: '#e4e4e7', darken: '#18181b' },
    neutral: { base: '#737373', lighten: '#e5e5e5', darken: '#171717' },
    stone: { base: '#78716c', lighten: '#e7e5e4', darken: '#1c1917' },
  }
  const surfaceColor = shallowRef('stone')
  watch(surfaceColor, v => {
    themes.value.light.colors.surface = surfaceColors[v].lighten
    themes.value.dark.colors.surface = surfaceColors[v].darken
  }, { immediate: true })

  // [color, thumbColor?] — thumbColor overrides the selected thumb (defaults to the
  // contrast color, which can look poor, e.g. orange)
  const colors = [
    ['red'],
    ['red-darken-3'],
    ['indigo'],
    ['indigo-darken-3'],
    ['orange-lighten-3', 'orange-darken-2'],
    ['orange-darken-3'],
    ['primary'],
    ['secondary'],
    ['success'],
    ['info'],
    ['warning'],
    ['error'],
  ]

  const model = ref(colors.map(([color]) => color))
</script>

@J-Sek J-Sek self-assigned this May 30, 2026
@J-Sek J-Sek marked this pull request as ready for review May 30, 2026 14:03
@J-Sek J-Sek added this to the v4.1.0 milestone May 30, 2026
@J-Sek J-Sek force-pushed the feat/vswitch-md3 branch from 6b1bc30 to 3c289c8 Compare May 30, 2026 14:20
@J-Sek J-Sek linked an issue May 30, 2026 that may be closed by this pull request
@J-Sek J-Sek force-pushed the feat/vswitch-md3 branch from 544cf8a to ce57f55 Compare May 30, 2026 22:46
@J-Sek J-Sek merged commit 010c8e7 into dev May 30, 2026
11 checks passed
@J-Sek J-Sek deleted the feat/vswitch-md3 branch May 30, 2026 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Inset VSwitch with Colored Thumb

1 participant