---
nav: Text fields
meta:
  title: Text field component
  description: The text field component accepts textual input from users.
  keywords: text fields, vuetify text field component, vue text field component
related:
  - /components/forms/
  - /components/selects/
  - /components/textarea/
---

# Text fields

Text field components are used for collecting user provided information.

<entry />

## Usage

A simple text field with placeholder and/or label.

<example file="v-text-field/usage" />

## API

<api-inline />

## Examples

### Props

#### Counter

Use a **counter** prop to inform a user of the character limit. The counter does not perform any validation by itself - you will need to pair it with either the internal validation system, or a 3rd party library. The counter can be customised with the **counter-value** prop and **counter** scoped slot.

<example file="v-text-field/prop-counter" />

#### Clearable

The **clearable** prop appends an icon that clears the `text field` when clicked. The clearable icon is easily customizable by using **clearable** in combination with the **clear-icon** prop.

<example file="v-text-field/prop-clearable" />

#### Custom colors

The **color** prop provides an easy way to change the color of your text field.

<example file="v-text-field/prop-custom-colors" />

#### Density

The **density** prop decreases the height of the text field based upon 1 of 3 levels of density; **default**, **comfortable**, and **compact**.

<example file="v-text-field/prop-dense" />

#### Disabled and readonly

The state of a text field can be changed by providing the **disabled** or **readonly** props.

<example file="v-text-field/prop-disabled-and-readonly" />

#### Hide details

When **hide-details** is set to `auto` messages will be rendered only if there's a message (hint, error message, counter value etc) to display.

<example file="v-text-field/prop-hide-details" />

#### Hint

The **hint** property on text fields adds the provided string beneath the text field. Using **persistent-hint** keeps the hint visible when the text field is not focused. Hint prop is _**not**_ supported in solo mode.

<example file="v-text-field/prop-hint" />

#### Icons

You can add icons to the text field with **prepend-icon**, **append-icon** and **append-outer-icon** props.

<example file="v-text-field/prop-icon" />

#### Prefixes and suffixes

The **prefix** and **suffix** properties allows you to prepend and append inline non-modifiable text next to the text field.

<example file="v-text-field/prop-prefixes-and-suffixes" />

#### Shaped

**shaped** text fields are rounded if they're **outlined** and have higher **border-radius** if **filled**.

<example file="v-text-field/prop-shaped" />

#### Single line

**single-line** text fields do not float their label on focus or with data.

<example file="v-text-field/prop-single-line" />

#### Validation

Vuetify includes simple validation through the **rules** prop. The prop accepts a mixed array of types `function`, `boolean` and `string`. When the input value changes, each element in the array will be validated. Functions pass the current v-model as an argument and must return either `true` / `false` or a `string` containing an error message.

<example file="v-text-field/prop-validation" />

#### Variant

The **variant** prop provides an easy way to customize the style of your text field. The following values are valid options: **solo**, **filled**, **outlined**, **plain**, and **underlined**.

<example file="v-text-field/prop-variant" />

### Events

#### Icon events

`click:prepend`, `click:append`, `click:append-outer`, and `click:clear` will be emitted when you click on the respective icon. Note that these events will not be fired if the slot is used instead.

<example file="v-text-field/event-icons" />

### Slots

<vuetify slug="vs-vue-3-slots" />

#### Icon slots

Instead of using `prepend`/`append`/`append-outer` icons you can use slots to extend input's functionality.

<example file="v-text-field/slot-icons" />

#### Label

Text field label can be defined in `label` slot - that will allow to use HTML content

<example file="v-text-field/slot-label" />

#### Progress

You can display a progress bar instead of the bottom line. You can use the default indeterminate progress having same color as the text field or designate a custom one using the `progress` slot

<example file="v-text-field/slot-progress" />

### Misc

#### Custom validation

While the built in `v-form` or 3rd party plugin such as [vuelidate](https://github.com/monterail/vuelidate) or [vee-validation](https://github.com/logaretm/vee-validate) can help streamline your validation process, you can choose to simply control it yourself.

<example file="v-text-field/misc-custom-validation" />

#### Full width with counter

Full width text fields allow you to create boundless inputs. In this example, we use a `v-divider` to separate the fields.

<example file="v-text-field/misc-full-width-with-counter" />

#### Password input

Using the HTML input **type** [password](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/password) can be used with an appended icon and callback to control the visibility.

<example file="v-text-field/misc-password" />

<backmatter />


/** if false, disabled buttons will be greyed out */

@use 'sass:math';
@use 'sass:map';
@use '../../styles/settings';
@use '../../styles/tools';

// Defaults
$button-app-bar-elevation: 0 !default;
$button-background: rgb(var(--v-theme-surface)) !default;
$button-color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !default;
$button-banner-actions-padding: 0 8px !default;
$button-pagination-active-overlay-opacity: var(--v-border-opacity) !default;
$button-pagination-border-radius: settings.$border-radius-root !default;
$button-border-color: settings.$border-color-root !default;
$button-border-radius: settings.$border-radius-root !default;
$button-border-style: settings.$border-style-root !default;
$button-border-thin-width: thin !default;
$button-border-width: 0 !default;
$button-card-actions-elevation: 0 !default;
$button-card-actions-margin: .5rem !default;
$button-card-actions-padding: 0 8px !default;
$button-content-transition: transform, opacity .2s settings.$standard-easing !default;
$button-disabled-opacity: 0.26 !default;
$button-disabled-overlay: 0.12 !default;
$button-elevation: ('default': 2, 'hover': 4, 'active': 8) !default;
$button-font-size: tools.map-deep-get(settings.$typography, 'button', 'size') !default;
$button-font-weight: tools.map-deep-get(settings.$typography, 'button', 'weight') !default;
$button-height: 36px !default;
$button-stacked-height: 72px !default;
$button-stacked-icon-margin: 4px !default;
$button-icon-border-radius: map.get(settings.$rounded, 'circle') !default;
$button-icon-font-size: 1rem !default;
$button-line-height: normal !default;
$button-stacked-line-height: 1.25 !default;
$button-plain-opacity: .62 !default;
$button-padding-ratio: 2.25 !default;
$button-stacked-padding-ratio: 4.5 !default;
$button-margin-start-multiplier: -9 !default;
$button-margin-end-multiplier: 4.5 !default;
$button-margin-start: calc(var(--v-btn-height) / #{$button-margin-start-multiplier}) !default;
$button-margin-end: calc(var(--v-btn-height) / #{$button-margin-end-multiplier}) !default;
$button-max-width: 100% !default;
$button-positions: absolute fixed !default;
$button-text-letter-spacing: tools.map-deep-get(settings.$typography, 'button', 'letter-spacing') !default;
$button-text-transform: tools.map-deep-get(settings.$typography, 'button', 'text-transform') !default;
$button-transition-property: box-shadow, transform, opacity, background !default;
$button-vertical-align: middle !default;
$button-width-ratio: math.div(16, 9) !default;
$button-snackbar-action-padding: 0 8px !default;
$button-stacked-width-ratio: 1 !default;
$button-rounded-border-radius: map.get(settings.$rounded, 'xl') !default;
$button-white-space: nowrap !default;

$button-density: ('default': 0, 'comfortable': -2, 'compact': -3) !default;
$button-stacked-density: ('default': 0, 'comfortable': -4, 'compact': -6) !default;
$button-icon-density: ('default': 3, 'comfortable': 0, 'compact': -2) !default;

$button-border: (
  $button-border-color,
  $button-border-style,
  $button-border-width,
  $button-border-thin-width
) !default;

$button-sizes: () !default;
$button-sizes: map.merge(
    (
      'height': $button-height,
      'font-size': $button-font-size,
      'width-ratio': $button-width-ratio,
      'padding-ratio': $button-padding-ratio
    ),
    $button-sizes
);

$button-stacked-sizes: () !default;
$button-stacked-sizes: map.merge(
    (
      'height': $button-stacked-height,
      'font-size': $button-font-size,
      'width-ratio': $button-stacked-width-ratio,
      'padding-ratio': $button-stacked-padding-ratio
    ),
    $button-stacked-sizes
);

$button-variants: (
  $button-background,
  $button-color,
  map.get($button-elevation, 'default'),
  $button-plain-opacity,
  'v-btn'
);


---
nav: Button toggles
meta:
  title: Button toggle component
  description: The button toggle component allows you to combine a series of selectable buttons together in a single element.
  keywords: button groups, vuetify button group component, vue button group component
related:
  - /components/buttons/
  - /components/icons/
  - /components/selection-controls/
---

# Button toggles

The `v-btn-toggle` component is a simple wrapper for `v-item-group` built specifically to work with `v-btn`.

![btn-groups Entry](https://cdn.vuetifyjs.com/docs/images/components-temp/v-btn-groups/v-btn-groups-entry.png)

---

## Usage

Toggle buttons allow you to create a styled group of buttons that can be selected or toggled under a single **v-model**.

<example file="v-btn-toggle/usage" />

<entry />

## API

<api-inline />

## Examples

### Props

#### Divided

You can add a visual divider between buttons with the **divided** prop.

<example file="v-btn-toggle/prop-divided" />

#### Variant

You can switch the button variant by using **variant** prop on `v-btn-toggle`.

<example file="v-btn-toggle/prop-variant" />

#### Mandatory

A `v-btn-toggle` with the **mandatory** prop will always have a value.

<example file="v-btn-toggle/prop-mandatory" />

#### Multiple

A `v-btn-toggle` with the **multiple** prop will allow a user to select multiple return values as an array.

<example file="v-btn-toggle/prop-multiple" />

#### Rounded

You can control the border radius with the **rounded** prop.

<example file="v-btn-toggle/prop-rounded" />

### Misc

<!-- #### Toolbar

Easily integrate customized button solutions with a `v-toolbar`

<example file="v-btn-toggle/misc-toolbar" /> -->

#### WYSIWYG

Group similar actions and design your own WYSIWYG component.

<example file="v-btn-toggle/misc-wysiwyg" />

<backmatter />


<template>
  <v-switch
    v-model="model"
    hide-details
    inset
    :label="`Switch: ${model.toString()}`"
  ></v-switch>
</template>

<script>
  export default {
    data () {
      return {
        model: true,
      }
    },
  }
</script>


export { VThemeProvider } from './VThemeProvider'


roles = _.pluck(Roles.getAllRoles().fetch(), 'name');

keep keep keep keep replace keep keep keep keep

Package.describe({
  name: 'rocketchat:integrations',
  version: '0.0.1',
  summary: 'Integrations with services and WebHooks',
  git: '',
  documentation: 'README.md'
});

Package.onUse(function(api) {
  api.versionsFrom('1.0');

  api.use('coffeescript');
  api.use('underscore');
  api.use('simple:highlight.js');
  api.use('rocketchat:lib@0.0.1');

  api.use('kadira:flow-router', 'client');
  api.use('templating', 'client');

  api.addFiles('lib/rocketchat.coffee', ['server','client']);
  api.addFiles('client/collection.coffee', ['client']);
  api.addFiles('client/startup.coffee', 'client');
  api.addFiles('client/route.coffee', 'client');

  // views
  api.addFiles('client/views/integrations.html', 'client');
  api.addFiles('client/views/integrations.coffee', 'client');
  api.addFiles('client/views/integrationsNew.html', 'client');
  api.addFiles('client/views/integrationsNew.coffee', 'client');
  api.addFiles('client/views/integrationsIncoming.html', 'client');
  api.addFiles('client/views/integrationsIncoming.coffee', 'client');
  api.addFiles('client/views/integrationsOutgoing.html', 'client');
  api.addFiles('client/views/integrationsOutgoing.coffee', 'client');

  // stylesheets
  api.addAssets('client/stylesheets/integrations.less', 'server');
  api.addFiles('client/stylesheets/load.coffee', 'server');

  api.addFiles('server/models/Integrations.coffee', 'server');

  // publications
  api.addFiles('server/publications/integrations.coffee', 'server');

  // methods
  api.addFiles('server/methods/incoming/addIncomingIntegration.coffee', 'server');
  api.addFiles('server/methods/incoming/updateIncomingIntegration.coffee', 'server');
  api.addFiles('server/methods/incoming/deleteIncomingIntegration.coffee', 'server');
  api.addFiles('server/methods/outgoing/addOutgoingIntegration.coffee', 'server');
  api.addFiles('server/methods/outgoing/updateOutgoingIntegration.coffee', 'server');
  api.addFiles('server/methods/outgoing/deleteOutgoingIntegration.coffee', 'server');

  // api
  api.addFiles('server/api/api.coffee', 'server');

  api.addFiles('server/triggers.coffee', 'server');

  var _ = Npm.require('underscore');
  var fs = Npm.require('fs');
  tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-integrations/i18n'), function(filename) {
    if (fs.statSync('packages/rocketchat-integrations/i18n/' + filename).size > 16) {
      return 'i18n/' + filename;
    }
  }));
  api.use('tap:i18n', ['client', 'server']);
  api.addFiles(tapi18nFiles, ['client', 'server']);
});


#!/bin/sh

export DDP_DEFAULT_CONNECTION_URL=http://localhost:3000
export MONGO_URL=mongodb://localhost:3001
meteor -p 5000


//! moment.js locale configuration
//! locale : hrvatski (hr)
//! author : Bojan Marković : https://github.com/bmarkovic

(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('../moment')) :
   typeof define === 'function' && define.amd ? define(['moment'], factory) :
   factory(global.moment)
}(this, function (moment) { 'use strict';

    function translate(number, withoutSuffix, key) {
        var result = number + ' ';
        switch (key) {
        case 'm':
            return withoutSuffix ? 'jedna minuta' : 'jedne minute';
        case 'mm':
            if (number === 1) {
                result += 'minuta';
            } else if (number === 2 || number === 3 || number === 4) {
                result += 'minute';
            } else {
                result += 'minuta';
            }
            return result;
        case 'h':
            return withoutSuffix ? 'jedan sat' : 'jednog sata';
        case 'hh':
            if (number === 1) {
                result += 'sat';
            } else if (number === 2 || number === 3 || number === 4) {
                result += 'sata';
            } else {
                result += 'sati';
            }
            return result;
        case 'dd':
            if (number === 1) {
                result += 'dan';
            } else {
                result += 'dana';
            }
            return result;
        case 'MM':
            if (number === 1) {
                result += 'mjesec';
            } else if (number === 2 || number === 3 || number === 4) {
                result += 'mjeseca';
            } else {
                result += 'mjeseci';
            }
            return result;
        case 'yy':
            if (number === 1) {
                result += 'godina';
            } else if (number === 2 || number === 3 || number === 4) {
                result += 'godine';
            } else {
                result += 'godina';
            }
            return result;
        }
    }

    var hr = moment.defineLocale('hr', {
        months : 'siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac'.split('_'),
        monthsShort : 'sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.'.split('_'),
        weekdays : 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'),
        weekdaysShort : 'ned._pon._uto._sri._čet._pet._sub.'.split('_'),
        weekdaysMin : 'ne_po_ut_sr_če_pe_su'.split('_'),
        longDateFormat : {
            LT : 'H:mm',
            LTS : 'LT:ss',
            L : 'DD. MM. YYYY',
            LL : 'D. MMMM YYYY',
            LLL : 'D. MMMM YYYY LT',
            LLLL : 'dddd, D. MMMM YYYY LT'
        },
        calendar : {
            sameDay  : '[danas u] LT',
            nextDay  : '[sutra u] LT',
            nextWeek : function () {
                switch (this.day()) {
                case 0:
                    return '[u] [nedjelju] [u] LT';
                case 3:
                    return '[u] [srijedu] [u] LT';
                case 6:
                    return '[u] [subotu] [u] LT';
                case 1:
                case 2:
                case 4:
                case 5:
                    return '[u] dddd [u] LT';
                }
            },
            lastDay  : '[jučer u] LT',
            lastWeek : function () {
                switch (this.day()) {
                case 0:
                case 3:
                    return '[prošlu] dddd [u] LT';
                case 6:
                    return '[prošle] [subote] [u] LT';
                case 1:
                case 2:
                case 4:
                case 5:
                    return '[prošli] dddd [u] LT';
                }
            },
            sameElse : 'L'
        },
        relativeTime : {
            future : 'za %s',
            past   : 'prije %s',
            s      : 'par sekundi',
            m      : translate,
            mm     : translate,
            h      : translate,
            hh     : translate,
            d      : 'dan',
            dd     : translate,
            M      : 'mjesec',
            MM     : translate,
            y      : 'godinu',
            yy     : translate
        },
        ordinalParse: /\d{1,2}\./,
        ordinal : '%d.',
        week : {
            dow : 1, // Monday is the first day of the week.
            doy : 7  // The week that contains Jan 1st is the first week of the year.
        }
    });

    return hr;

}));


Meteor.startup ->

	# Note:
	# 1.if we need to create a role that can only edit channel message, but not edit group message
	# then we can define edit-<type>-message instead of edit-message
	# 2. admin, moderator, and user roles should not be deleted as they are referened in the code.
	permissions = [

		{ _id: 'view-statistics',
		roles : ['admin', 'temp-role']}

		{ _id: 'view-privileged-setting',
		roles : ['admin']}

		{ _id: 'edit-privileged-setting',
		roles : ['admin']}

		{ _id: 'view-room-administration',
		roles : ['admin']}

		{ _id: 'view-user-administration',
		roles : ['admin']}

		{ _id: 'view-full-other-user-info',
		roles : ['admin']}

		{ _id: 'edit-other-user-info',
		roles : ['admin']}

		{ _id: 'edit-other-user-password',
		roles : ['admin']}

		{ _id: 'assign-admin-role',
		roles : ['admin']}

		{ _id: 'edit-other-user-active-status',
		roles : ['admin', 'site-moderator']}

		{ _id: 'delete-user',
		roles : ['admin']}

		{ _id: 'view-other-user-channels',
		roles : ['admin']}

		{ _id: 'add-oauth-service',
		roles : ['admin']}

		{ _id: 'run-migration',
		roles : ['admin']}

		{ _id: 'create-c',
		roles : ['admin', 'site-moderator', 'user']}

		{ _id: 'delete-c',
		roles : ['admin', 'site-moderator']}

		{ _id: 'edit-room',
		roles : ['admin', 'site-moderator', 'moderator']}

		{ _id: 'edit-message',
		roles : ['admin', 'site-moderator', 'moderator']}

		{ _id: 'delete-message',
		roles : ['admin', 'site-moderator', 'moderator']}

		{ _id: 'remove-user',
		roles : ['admin', 'site-moderator', 'moderator']}

		{ _id: 'mute-user',
		roles : ['admin', 'site-moderator', 'moderator']}

		{ _id: 'ban-user',
		roles : ['admin', 'site-moderator', 'moderator']}

		{ _id: 'create-p',
		roles : ['admin', 'site-moderator', 'user']}

		{ _id: 'delete-p',
		roles : ['admin', 'site-moderator']}

		{ _id: 'delete-d',
		roles : ['admin', 'site-moderator']}

		{ _id: 'bulk-register-user',
		roles : ['admin']}

		{ _id: 'bulk-create-c',
		roles : ['admin']}

		{ _id: 'view-c-room',
		roles : ['admin', 'site-moderator', 'user']}

		{ _id: 'view-p-room',
		roles : ['admin', 'site-moderator', 'user']}

		{ _id: 'view-d-room',
		roles : ['admin', 'site-moderator', 'user']}

		{ _id: 'access-permissions',
		roles : ['admin']}

		{ _id: 'manage-assets',
		roles : ['admin']}

		{ _id: 'manage-integrations',
		roles : ['admin']}
	]

	#alanning:roles
	roles = _.pluck(Roles.getAllRoles().fetch(), 'name');

	for permission in permissions
		RocketChat.models.Permissions.upsert( permission._id, {$setOnInsert : permission })
		for role in permission.roles
			unless role in roles
				Roles.createRole role
				roles.push(role)


@menu = new class
	init: ->
		@container = $("#rocket-chat")
		@list = $('.rooms-list')

	isOpen: ->
		return @container?.hasClass("menu-opened") is true

	open: ->
		if not @isOpen()
			@container?.removeClass("menu-closed").addClass("menu-opened")

	close: ->
		if @isOpen()
			@container?.removeClass("menu-opened").addClass("menu-closed")

	toggle: ->
		if @isOpen()
			@close()
		else
			@open()

	updateUnreadBars: _.throttle ->
		if not @list?
			return

		listOffset = @list.offset()
		listHeight = @list.height()

		showTop = false
		showBottom = false
		$('li.has-alert').each ->
			if $(this).offset().top < listOffset.top - $(this).height()
				showTop = true

			if $(this).offset().top > listOffset.top + listHeight
				showBottom = true

		if showTop is true
			$('.top-unread-rooms').removeClass('hidden')
		else
			$('.top-unread-rooms').addClass('hidden')

		if showBottom is true
			$('.bottom-unread-rooms').removeClass('hidden')
		else
			$('.bottom-unread-rooms').addClass('hidden')
	, 200


RocketChat.models.Users.update {_id: user._id}, updateObj

integration._id = RocketChat.models.Integrations.insert integration

Roles.addUsersToRoles user._id, 'bot', 'bot'


default: null,
},
multiple: Boolean,
},
data () {
return {

// Libraries
import Vue from 'vue'

// Components
import VItem from '../VItem'
import VItemGroup from '../VItemGroup'

// Utilities
import {
  createLocalVue,
  mount,
  Wrapper,
} from '@vue/test-utils'
import { ExtractVue } from './../../../util/mixins'

const vm = new Vue()
const defaultSlot = ({ toggle }) => vm.$createElement('div', { on: { click: toggle } }, 'foobar')

const Mock = {
  name: 'test',

  render: h => h(VItem, {
    scopedSlots: {
      default: defaultSlot,
    },
  }),
}

describe('VItemGroup', () => {
  type Instance = ExtractVue<typeof VItemGroup>
  let mountFunction: (options?: object) => Wrapper<Instance>
  let localVue: typeof Vue

  beforeEach(() => {
    localVue = createLocalVue()

    mountFunction = (options = {}) => {
      return mount(VItemGroup, {
        localVue,
        ...options,
      })
    }
  })

  it('should warn if using multiple prop without an array value', () => {
    mountFunction({
      propsData: {
        multiple: true,
        value: '',
      },
    })

    expect('Model must be bound to an array if the multiple property is true').toHaveBeenTipped()
  })

  it('should return the correct value', () => {
    const wrapper = mountFunction()

    const getValue = wrapper.vm.getValue

    expect(getValue({ value: null }, 0)).toBe(0)
    expect(getValue({ value: undefined }, 1)).toBe(1)
    expect(getValue({ value: '' }, 2)).toBe(2)
    expect(getValue({ value: 'foo' }, 'foo')).toBe('foo')
  })

  it('should register elements', () => {
    const wrapper = mountFunction({
      slots: {
        default: [Mock],
      },
    })

    expect(wrapper.vm.items).toHaveLength(1)

    const item = wrapper.find(Mock)

    item.destroy()

    expect(wrapper.vm.items).toHaveLength(0)
  })

  it('should register and activate elements', () => {
    const wrapper = mountFunction({
      propsData: { value: 0 },
      slots: { default: [Mock] },
    })

    expect(wrapper.vm.items).toHaveLength(1)

    // Avoriaz doesn't like
    // components without
    // a render function
    const item = wrapper.find({
      name: 'v-item',
      render: () => null,
    })

    expect(item.vm.isActive).toBe(true)
  })

  it('should update state from child clicks', () => {
    const change = jest.fn()
    const wrapper = mountFunction({
      slots: {
        default: [
          Mock,
          Mock,
        ],
      },
    })

    wrapper.vm.$on('change', change)

    expect(wrapper.vm.items).toHaveLength(2)

    const [child1, child2] = wrapper.vm.$el.children

    child1.click()
    expect(change).toHaveBeenCalledWith(0)
    expect(wrapper.vm.internalValue).toBe(0)

    child2.click()
    expect(change).toHaveBeenCalledWith(1)
    expect(wrapper.vm.internalValue).toBe(1)

    child2.click()
    expect(wrapper.vm.internalValue).toBeUndefined()

    wrapper.setProps({
      value: [],
      multiple: true,
    })

    child1.click()
    expect(change).toHaveBeenCalledWith([0])

    child2.click()
    expect(change).toHaveBeenCalledWith([0, 1])

    child1.click()
    expect(change).toHaveBeenCalledWith([1])
  })

  it('should have a conditional method for toggling items', () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.toggleMethod(0)).toBe(false)

    wrapper.setProps({ value: 0 })

    expect(wrapper.vm.toggleMethod(0)).toBe(true)

    wrapper.setProps({
      multiple: true,
      value: [],
    })

    expect(wrapper.vm.toggleMethod(0)).toBe(false)

    wrapper.setProps({ value: [0] })

    expect(wrapper.vm.toggleMethod(0)).toBe(true)

    wrapper.setProps({ value: 0 })

    expect(wrapper.vm.toggleMethod(0)).toBe(false)
  })

  it('should select the first item if mandatory and no value', async () => {
    const wrapper = mountFunction({
      propsData: { mandatory: true },
      slots: {
        default: [Mock],
      },
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedItems).toHaveLength(1)
    expect(wrapper.vm.internalValue).toBe(0)

    wrapper.setProps({ multiple: true })

    // Manually update selected items
    wrapper.vm.updateItemsState()

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedItems).toHaveLength(1)
    expect(wrapper.vm.internalValue).toEqual([0])
  })

  it('should update a single item group', () => {
    const wrapper = mountFunction()

    // Toggling on and off
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBe('foo')
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBeUndefined()

    wrapper.setProps({ mandatory: true })

    // Toggling off single mandatory
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBe('foo')
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBe('foo')
  })

  it('should update a multiple item group', () => {
    const wrapper = mountFunction({
      propsData: { multiple: true },
    })

    // Toggling on and off
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual(['foo'])
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual([])

    wrapper.setProps({ mandatory: true })

    // Toggling off single mandatory
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual(['foo'])
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual(['foo'])

    wrapper.setProps({ max: 3 })

    // Should enforce maximum selection
    wrapper.vm.updateMultiple('bar')
    expect(wrapper.vm.internalValue).toEqual(['foo', 'bar'])
    wrapper.vm.updateMultiple('fizz')
    expect(wrapper.vm.internalValue).toEqual(['foo', 'bar', 'fizz'])
    wrapper.vm.updateMultiple('buzz')
    expect(wrapper.vm.internalValue).toEqual(['foo', 'bar', 'fizz'])
  })

  it('should update value if mandatory and dynamic items', async () => {
    const wrapper = mountFunction({
      propsData: {
        multiple: true,
        value: [3],
      },
      slots: {
        default: [
          Mock,
          Mock,
          Mock,
          Mock,
        ],
      },
    })

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    const [first, second, third, fourth] = wrapper.findAll(Mock).wrappers

    fourth.destroy()

    expect(change).toHaveBeenCalledWith([])

    wrapper.setProps({ mandatory: true, value: [2] })

    third.destroy()

    expect(change).toHaveBeenCalledWith([1])

    wrapper.setProps({ multiple: false, value: 1 })

    second.destroy()

    expect(change).toHaveBeenCalledWith(0)

    first.destroy()

    expect(change).toHaveBeenCalledWith(undefined)
  })

  // https://github.com/vuetifyjs/vuetify/issues/5384
  it('should not unregister children when is destroyed', () => {
    const wrapper = mountFunction({
      propsData: {
        value: 0,
      },
      slots: {
        default: [Mock],
      },
    })

    const change = jest.fn()

    wrapper.vm.$on('change', change)

    wrapper.destroy()

    expect(change).not.toHaveBeenCalled()
  })

  // https://github.com/vuetifyjs/vuetify/issues/5000
  it('should update mandatory to first non-disabled item', () => {
    const Mock2 = {
      name: 'mock2',

      render (h) {
        return h(VItem, {
          props: {
            disabled: true,
          },
          scopedSlots: {
            default: defaultSlot,
          },
        })
      },
    }

    const wrapper = mountFunction({
      propsData: {
        mandatory: true,
      },
      slots: {
        default: [
          Mock2,
          Mock,
          Mock,
        ],
      },
    })

    expect(wrapper.vm.internalValue).toBe(1)
  })

  // https://github.com/vuetifyjs/vuetify/issues/6278
  it('should infer index dynamically', () => {
    const wrapper = mount(VItemGroup, {
      propsData: { value: 0 },
      slots: {
        default: [
          Mock,
          Mock,
          Mock,
        ],
      },
    })

    const items = wrapper.findAll(Mock)
    const item2 = items.at(1)
    const item3 = items.at(2)

    item2.destroy()

    item3.trigger('click')

    expect(wrapper.vm.internalValue).toBe(1)
  })

  it('should have the correct selected index, item and items', () => {
    const wrapper = mountFunction({
      slots: {
        default: [Mock, Mock, Mock],
      },
    })

    expect(wrapper.vm.items).toHaveLength(3)

    wrapper.setProps({ value: 1 })

    expect(wrapper.vm.selectedIndex).toBe(1)
    expect(wrapper.vm.selectedItem).toEqual(wrapper.vm.items[1])

    wrapper.setProps({ value: 2 })

    expect(wrapper.vm.selectedIndex).toBe(2)
    expect(wrapper.vm.selectedItem).toEqual(wrapper.vm.items[2])
  })
})


<template>
  <v-toolbar color="orange accent-1">
    <v-app-bar-nav-icon class="hidden-sm-and-down"></v-app-bar-nav-icon>
    <v-toolbar-title class="title mr-6 hidden-sm-and-down">
      Cryptocurrency
    </v-toolbar-title>
    <v-autocomplete
      v-model="model"
      :items="items"
      :loading="isLoading"
      :search-input.sync="search"
      chips
      clearable
      hide-details
      hide-selected
      item-text="name"
      item-value="symbol"
      label="Search for a coin..."
      solo
    >
      <template v-slot:no-data>
        <v-list-item>
          <v-list-item-title>
            Search for your favorite
            Cryptocurrency
          </v-list-item-title>
        </v-list-item>
      </template>
      <template v-slot:selection="{ attr, on, item, selected }">
        <v-chip
          v-bind="attr"
          :input-value="selected"
          color="blue-grey"
          class="white--text"
          v-on="on"
        >
          <v-icon left>
            mdi-bitcoin
          </v-icon>

        </v-chip>
      </template>
      <template v-slot:item="{ item }">
        <v-list-item-avatar
          color="indigo"
          class="headline font-weight-light white--text"
        >
          {{ item.name.charAt(0) }}
        </v-list-item-avatar>
        <v-list-item-content>
          <v-list-item-title v-text="item.name"></v-list-item-title>
          <v-list-item-subtitle v-text="item.symbol"></v-list-item-subtitle>
        </v-list-item-content>
        <v-list-item-action>
          <v-icon>mdi-bitcoin</v-icon>
        </v-list-item-action>
      </template>
    </v-autocomplete>
    <template v-slot:extension>
      <v-tabs
        v-model="tab"
        :hide-slider="!model"
        color="blue-grey"
        slider-color="blue-grey"
      >
        <v-tab :disabled="!model">
          News
        </v-tab>
        <v-tab :disabled="!model">
          Trading
        </v-tab>
        <v-tab :disabled="!model">
          Blog
        </v-tab>
      </v-tabs>
    </template>
  </v-toolbar>
</template>


<template>
  <v-container class="fill-height">
    <v-data-iterator
      :footer-props="{ itemsPerPageOptions }"
      :items="items"
      :items-per-page.sync="itemsPerPage"
      :page="page"
      :search="search"
      :sort-by="sortBy.toLowerCase()"
      :sort-desc="sortDesc"
      v-bind="$attrs"
    >
      <template v-slot:header>
        <v-toolbar
          dark
          color="blue darken-3"
          class="mb-1"
        >
          <v-text-field
            v-model="search"
            clearable
            flat
            solo-inverted
            hide-details
            prepend-inner-icon="mdi-magnify"
            label="Search"
          ></v-text-field>
          <template v-if="$vuetify.breakpoint.mdAndUp">
            <v-spacer></v-spacer>
            <v-select
              v-model="sortBy"
              class="mx-2"
              flat
              solo-inverted
              hide-details
              :items="keys"
              prepend-inner-icon="mdi-magnify"
              label="Sort by"
            ></v-select>
            <v-spacer></v-spacer>
            <v-btn-toggle
              v-model="sortDesc"
              mandatory
            >
              <v-btn
                large
                depressed
                color="blue"
                :value="false"
              >
                <v-icon>mdi-arrow-up</v-icon>
              </v-btn>
              <v-btn
                large
                depressed
                color="blue"
                :value="true"
              >
                <v-icon>mdi-arrow-down</v-icon>
              </v-btn>
            </v-btn-toggle>
          </template>
        </v-toolbar>
      </template>
      <template v-slot:default="props">
        <v-row>
          <v-col
            v-for="item in props.items"
            :key="item.name"
            cols="12"
            sm="6"
          >
            <v-card>
              <v-card-title>
                <h5>{{ item.name }}</h5>
              </v-card-title>
              <v-divider></v-divider>
              <v-list dense>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Calories:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.calories }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Fat:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.fat }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Carbs:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.carbs }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Protein:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.protein }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Sodium:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.sodium }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Calcium:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.calcium }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
                <v-list-item>
                  <v-row justify="space-between">
                    <v-col cols="auto">
                      <v-list-item-content>Iron:</v-list-item-content>
                    </v-col>
                    <v-col cols="auto">
                      <v-list-item-content>{{ item.iron }}</v-list-item-content>
                    </v-col>
                  </v-row>
                </v-list-item>
              </v-list>
            </v-card>
          </v-col>
        </v-row>
      </template>
    </v-data-iterator>
  </v-container>
</template>

<script>
  export default {
    name: 'Usage',

    inheritAttrs: false,

    data: () => ({
      itemsPerPageOptions: [4, 8, 12],
      search: '',
      sortDesc: false,
      page: 1,
      itemsPerPage: 4,
      sortBy: 'name',
      keys: [
        'Name',
        'Calories',
        'Fat',
        'Carbs',
        'Protein',
        'Sodium',
        'Calcium',
        'Iron',
      ],
      items: [
        {
          name: 'Frozen Yogurt',
          calories: 159,
          fat: 6.0,
          carbs: 24,
          protein: 4.0,
          sodium: 87,
          calcium: '14%',
          iron: '1%',
        },
        {
          name: 'Ice cream sandwich',
          calories: 237,
          fat: 9.0,
          carbs: 37,
          protein: 4.3,
          sodium: 129,
          calcium: '8%',
          iron: '1%',
        },
        {
          name: 'Eclair',
          calories: 262,
          fat: 16.0,
          carbs: 23,
          protein: 6.0,
          sodium: 337,
          calcium: '6%',
          iron: '7%',
        },
        {
          name: 'Cupcake',
          calories: 305,
          fat: 3.7,
          carbs: 67,
          protein: 4.3,
          sodium: 413,
          calcium: '3%',
          iron: '8%',
        },
        {
          name: 'Gingerbread',
          calories: 356,
          fat: 16.0,
          carbs: 49,
          protein: 3.9,
          sodium: 327,
          calcium: '7%',
          iron: '16%',
        },
        {
          name: 'Jelly bean',
          calories: 375,
          fat: 0.0,
          carbs: 94,
          protein: 0.0,
          sodium: 50,
          calcium: '0%',
          iron: '0%',
        },
        {
          name: 'Lollipop',
          calories: 392,
          fat: 0.2,
          carbs: 98,
          protein: 0,
          sodium: 38,
          calcium: '0%',
          iron: '2%',
        },
        {
          name: 'Honeycomb',
          calories: 408,
          fat: 3.2,
          carbs: 87,
          protein: 6.5,
          sodium: 562,
          calcium: '0%',
          iron: '45%',
        },
        {
          name: 'Donut',
          calories: 452,
          fat: 25.0,
          carbs: 51,
          protein: 4.9,
          sodium: 326,
          calcium: '2%',
          iron: '22%',
        },
        {
          name: 'KitKat',
          calories: 518,
          fat: 26.0,
          carbs: 65,
          protein: 7,
          sodium: 54,
          calcium: '12%',
          iron: '6%',
        },
      ],
      defaults: {
        'disable-filtering': false,
        'disable-pagination': false,
        'disable-sort': false,
        'hide-default-footer': false,
      },
      options: {
        booleans: [
          'disable-filtering',
          'disable-pagination',
          'disable-sort',
          'hide-default-footer',
        ],
      },
      tabs: [],
    }),
  }
</script>


<template>
  <div class="ma-12 pa-12">
    <v-card>
      <v-navigation-drawer
        permanent
        expand-on-hover
      >
        <v-list>
          <v-list-item class="px-2">
            <v-list-item-avatar>
              <v-img src="https://randomuser.me/api/portraits/women/85.jpg"></v-img>
            </v-list-item-avatar>
          </v-list-item>

          <v-list-item link>
            <v-list-item-content>
              <v-list-item-title class="title">
                Sandra Adams
              </v-list-item-title>
              <v-list-item-subtitle>sandra_a88@gmail.com</v-list-item-subtitle>
            </v-list-item-content>
          </v-list-item>
        </v-list>

        <v-divider></v-divider>

        <v-list
          nav
          dense
        >
          <v-list-item link>
            <v-list-item-icon>
              <v-icon>mdi-folder</v-icon>
            </v-list-item-icon>
            <v-list-item-title>My Files</v-list-item-title>
          </v-list-item>
          <v-list-item link>
            <v-list-item-icon>
              <v-icon>mdi-account-multiple</v-icon>
            </v-list-item-icon>
            <v-list-item-title>Shared with me</v-list-item-title>
          </v-list-item>
          <v-list-item link>
            <v-list-item-icon>
              <v-icon>mdi-star</v-icon>
            </v-list-item-icon>
            <v-list-item-title>Starred</v-list-item-title>
          </v-list-item>
        </v-list>
      </v-navigation-drawer>
    </v-card>
  </div>
</template>

<script>
  export default {
    //
  }
</script>


keep keep keep keep keep replace keep keep keep keep keep

return h(this.tag, this.genData(), this.$slots.default)

// Styles
import './VItemGroup.sass'

// Mixins
import Groupable from '../../mixins/groupable'
import Proxyable from '../../mixins/proxyable'
import Themeable from '../../mixins/themeable'

// Utilities
import mixins from '../../util/mixins'
import { consoleWarn } from '../../util/console'

// Types
import { VNode } from 'vue/types'

export type GroupableInstance = InstanceType<typeof Groupable> & {
  id?: string
  to?: any
  value?: any
 }

export const BaseItemGroup = mixins(
  Proxyable,
  Themeable
).extend({
  name: 'base-item-group',

  props: {
    activeClass: {
      type: String,
      default: 'v-item--active',
    },
    mandatory: Boolean,
    max: {
      type: [Number, String],
      default: null,
    },
    multiple: Boolean,
  },

  data () {
    return {
      // As long as a value is defined, show it
      // Otherwise, check if multiple
      // to determine which default to provide
      internalLazyValue: this.value !== undefined
        ? this.value
        : this.multiple ? [] : undefined,
      items: [] as GroupableInstance[],
    }
  },

  computed: {
    classes (): Record<string, boolean> {
      return {
        'v-item-group': true,
        ...this.themeClasses,
      }
    },
    selectedIndex (): number {
      return (this.selectedItem && this.items.indexOf(this.selectedItem)) || -1
    },
    selectedItem (): GroupableInstance | undefined {
      if (this.multiple) return undefined

      return this.selectedItems[0]
    },
    selectedItems (): GroupableInstance[] {
      return this.items.filter((item, index) => {
        return this.toggleMethod(this.getValue(item, index))
      })
    },
    selectedValues (): any[] {
      if (this.internalValue == null) return []

      return Array.isArray(this.internalValue)
        ? this.internalValue
        : [this.internalValue]
    },
    toggleMethod (): (v: any) => boolean {
      if (!this.multiple) {
        return (v: any) => this.internalValue === v
      }

      const internalValue = this.internalValue
      if (Array.isArray(internalValue)) {
        return (v: any) => internalValue.includes(v)
      }

      return () => false
    },
  },

  watch: {
    internalValue: 'updateItemsState',
    items: 'updateItemsState',
  },

  created () {
    if (this.multiple && !Array.isArray(this.internalValue)) {
      consoleWarn('Model must be bound to an array if the multiple property is true.', this)
    }
  },

  methods: {

    genData (): object {
      return {
        class: this.classes,
      }
    },
    getValue (item: GroupableInstance, i: number): unknown {
      return item.value == null || item.value === ''
        ? i
        : item.value
    },
    onClick (item: GroupableInstance) {
      this.updateInternalValue(
        this.getValue(item, this.items.indexOf(item))
      )
    },
    register (item: GroupableInstance) {
      const index = this.items.push(item) - 1

      item.$on('change', () => this.onClick(item))

      // If no value provided and mandatory,
      // assign first registered item
      if (this.mandatory && !this.selectedValues.length) {
        this.updateMandatory()
      }

      this.updateItem(item, index)
    },
    unregister (item: GroupableInstance) {
      if (this._isDestroyed) return

      const index = this.items.indexOf(item)
      const value = this.getValue(item, index)

      this.items.splice(index, 1)

      const valueIndex = this.selectedValues.indexOf(value)

      // Items is not selected, do nothing
      if (valueIndex < 0) return

      // If not mandatory, use regular update process
      if (!this.mandatory) {
        return this.updateInternalValue(value)
      }

      // Remove the value
      if (this.multiple && Array.isArray(this.internalValue)) {
        this.internalValue = this.internalValue.filter(v => v !== value)
      } else {
        this.internalValue = undefined
      }

      // If mandatory and we have no selection
      // add the last item as value
      /* istanbul ignore else */
      if (!this.selectedItems.length) {
        this.updateMandatory(true)
      }
    },
    updateItem (item: GroupableInstance, index: number) {
      const value = this.getValue(item, index)

      item.isActive = this.toggleMethod(value)
    },
    // https://github.com/vuetifyjs/vuetify/issues/5352
    updateItemsState () {
      this.$nextTick(() => {
        if (this.mandatory &&
          !this.selectedItems.length
        ) {
          return this.updateMandatory()
        }

        // TODO: Make this smarter so it
        // doesn't have to iterate every
        // child in an update
        this.items.forEach(this.updateItem)
      })
    },
    updateInternalValue (value: any) {
      this.multiple
        ? this.updateMultiple(value)
        : this.updateSingle(value)
    },
    updateMandatory (last?: boolean) {
      if (!this.items.length) return

      const items = this.items.slice()

      if (last) items.reverse()

      const item = items.find(item => !item.disabled)

      // If no tabs are available
      // aborts mandatory value
      if (!item) return

      const index = this.items.indexOf(item)

      this.updateInternalValue(
        this.getValue(item, index)
      )
    },
    updateMultiple (value: any) {
      const defaultValue = Array.isArray(this.internalValue)
        ? this.internalValue
        : []
      const internalValue = defaultValue.slice()
      const index = internalValue.findIndex(val => val === value)

      if (
        this.mandatory &&
        // Item already exists
        index > -1 &&
        // value would be reduced below min
        internalValue.length - 1 < 1
      ) return

      if (
        // Max is set
        this.max != null &&
        // Item doesn't exist
        index < 0 &&
        // value would be increased above max
        internalValue.length + 1 > this.max
      ) return

      index > -1
        ? internalValue.splice(index, 1)
        : internalValue.push(value)

      this.internalValue = internalValue
    },
    updateSingle (value: any) {
      const isSame = value === this.internalValue

      if (this.mandatory && isSame) return

      this.internalValue = isSame ? undefined : value
    },
  },

  render (h): VNode {
    return h('div', this.genData(), this.$slots.default)
  },
})

export default BaseItemGroup.extend({
  name: 'v-item-group',

  provide (): object {
    return {
      itemGroup: this,
    }
  },
})


Designates that the avatar is on the left side of a component. This is hooked into by components such as [v-chip](/components/chips) and [v-btn](/components/buttons).

Designates that the avatar is on the right side of a component. This is hooked into by components such as [v-chip](/components/chips) and [v-btn](/components/buttons).


@import '../../styles/styles.sass';

$pagination-border-radius: $border-radius-root !default;
$pagination-disabled-opacity: 0.6 !default;
$pagination-item-font-size: map-deep-get($headings, 'subtitle-1', 'size') !default;
$pagination-item-height: 34px !default;
$pagination-item-margin: .3rem !default;
$pagination-item-min-width: 34px !default;
$pagination-item-padding: 0 5px !default;
$pagination-more-height: 32px !default;
$pagination-more-margin: .3rem !default;
$pagination-more-width: 32px !default;
$pagination-navigation-disabled-opacity: 0.6 !default;
$pagination-navigation-height: 32px !default;
$pagination-navigation-margin: .3rem 10px !default;
$pagination-navigation-width: 32px !default;


.git
.vscode
.devbots
.github
node_modules


it('should render with a specified tag when the tag prop is provided with a value', () => {


// Directives
import { Scroll } from '../../directives'

// Utilities
import { consoleWarn } from '../../util/console'

// Types
import Vue from 'vue'

/**
 * Scrollable
 *
 * Used for monitoring scrolling and
 * invoking functions based upon
 * scrolling thresholds being
 * met.
 */
/* @vue/component */
export default Vue.extend({
  name: 'scrollable',

  directives: { Scroll },

  props: {
    scrollTarget: String,
    scrollThreshold: [String, Number],
  },

  data: () => ({
    currentScroll: 0,
    currentThreshold: 0,
    isActive: false,
    isScrollingUp: false,
    previousScroll: 0,
    savedScroll: 0,
    target: null as Element | null,
  }),

  computed: {
    /**
     * A computed property that returns
     * whether scrolling features are
     * enabled or disabled
     */
    canScroll (): boolean {
      return typeof window !== 'undefined'
    },
    /**
     * The threshold that must be met before
     * thresholdMet function is invoked
     */
    computedScrollThreshold (): number {
      return this.scrollThreshold
        ? Number(this.scrollThreshold)
        : 300
    },
  },

  watch: {
    isScrollingUp () {
      this.savedScroll = this.savedScroll || this.currentScroll
    },
    isActive () {
      this.savedScroll = 0
    },
  },

  mounted () {
    if (this.scrollTarget) {
      this.target = document.querySelector(this.scrollTarget)

      if (!this.target) {
        consoleWarn(`Unable to locate element with identifier ${this.scrollTarget}`, this)
      }
    }
  },

  methods: {
    onScroll () {
      if (!this.canScroll) return

      this.previousScroll = this.currentScroll
      this.currentScroll = this.target
        ? this.target.scrollTop
        : window.pageYOffset

      this.isScrollingUp = this.currentScroll < this.previousScroll
      this.currentThreshold = Math.abs(this.currentScroll - this.computedScrollThreshold)

      this.$nextTick(() => {
        if (
          Math.abs(this.currentScroll - this.savedScroll) >
          this.computedScrollThreshold
        ) this.thresholdMet()
      })
    },
    /**
     * The method invoked when
     * scrolling in any direction
     * has exceeded the threshold
     */
    thresholdMet () { /* noop */ },
  },
})


module.exports = {
  'v-lazy': {
    slots: [
      {
        name: 'default',
        props: undefined,
      },
    ],
  },
}


import { install } from './install'

// Types
import Vue from 'vue'
import {
  UserVuetifyPreset,
  VuetifyPreset,
} from 'vuetify/types/services/presets'
import {
  VuetifyService,
  VuetifyServiceContract,
} from 'vuetify/types/services'

// Services
import * as services from './services'

export default class Vuetify {
  static install = install

  static installed = false

  static version = __VUETIFY_VERSION__

  static config = {
    silent: false,
  }

  public framework: Dictionary<VuetifyServiceContract> = {
    isHydrating: false,
  } as any

  public installed: string[] = []

  public preset = {} as VuetifyPreset

  public userPreset: UserVuetifyPreset = {}

  constructor (userPreset: UserVuetifyPreset = {}) {
    this.userPreset = userPreset

    this.use(services.Presets)
    this.use(services.Application)
    this.use(services.Breakpoint)
    this.use(services.Goto)
    this.use(services.Icons)
    this.use(services.Lang)
    this.use(services.Theme)
  }

  // Called on the new vuetify instance
  // bootstrap in install beforeCreate
  // Exposes ssrContext if available
  init (root: Vue, ssrContext?: object) {
    this.installed.forEach(property => {
      const service = this.framework[property]

      service.framework = this.framework

      service.init(root, ssrContext)
    })

    // rtl is not installed and
    // will never be called by
    // the init process
    this.framework.rtl = Boolean(this.preset.rtl) as any
  }

  // Instantiate a VuetifyService
  use (Service: VuetifyService) {
    const property = Service.property

    if (this.installed.includes(property)) return

    // TODO maybe a specific type for arg 2?
    this.framework[property] = new Service(this.preset, this as any)
    this.installed.push(property)
  }
}


import Mouse from '../mouse'

import {
  mount,
  Wrapper,
  MountOptions,
} from '@vue/test-utils'
import { ExtractVue } from '../../../../util/mixins'

const Mock = Mouse.extend({
  render: h => h('div'),
})

describe('mouse.ts', () => {
  type Instance = ExtractVue<typeof Mock>
  let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>
  beforeEach(() => {
    mountFunction = (options?: MountOptions<Instance>) => {
      return mount(Mock, options)
    }
  })

  it('should generate mouse event handlers', async () => {
    const noop = e => e
    const wrapper = mount(Mock, {
      listeners: {
        click: noop,
      },
    })

    expect(typeof wrapper.vm.getMouseEventHandlers({ click: { event: 'click' } }, noop).click).toBe('function')
  })

  it('should generate default mouse event handlers', async () => {
    const noop = e => e
    const wrapper = mount(Mock, {
      listeners: {
        click: noop,
      },
    })

    expect(typeof wrapper.vm.getDefaultMouseEventHandlers('', noop).click).toBe('function')
    expect(Object.keys(typeof wrapper.vm.getDefaultMouseEventHandlers('', noop))).toHaveLength(6)
  })

  it('should emit events', async () => {
    const fn = jest.fn()
    const wrapper = mount(Mock, {
      listeners: {
        click: fn,
      },
    })

    const { click } = wrapper.vm.getMouseEventHandlers({ click: { event: 'click' } }, () => {})
    Array.isArray(click) ? click[0](null) : click(null)
    expect(fn).toHaveBeenCalledTimes(1)
  })

  it('should handle prevent modifier', async () => {
    const fn = jest.fn()
    const wrapper = mount(Mock, {
      listeners: {
        click: fn,
      },
    })
    const event = { preventDefault: () => {} }
    const spy = jest.spyOn(event, 'preventDefault')

    const { click } = wrapper.vm.getMouseEventHandlers({ click: { event: 'click', prevent: true } }, () => {})
    Array.isArray(click) ? click[0](event as MouseEvent) : click(event as MouseEvent)
    expect(fn).toHaveBeenCalledTimes(1)
    expect(spy).toHaveBeenCalledTimes(1)
  })

  it('should handle stop modifier', async () => {
    const fn = jest.fn()
    const wrapper = mount(Mock, {
      listeners: {
        click: fn,
      },
    })
    const event = { stopPropagation: () => {} }
    const spy = jest.spyOn(event, 'stopPropagation')

    const { click } = wrapper.vm.getMouseEventHandlers({ click: { event: 'click', stop: true } }, () => {})
    Array.isArray(click) ? click[0](event as MouseEvent) : click(event as MouseEvent)
    expect(fn).toHaveBeenCalledTimes(1)
    expect(spy).toHaveBeenCalledTimes(1)
  })
})


const shell = require('shelljs')
const fs = require('fs')
const path = require('path')
const resolve = target => path.resolve(__dirname, target)
const devTargetFolder = '../packages/vuetify/dev'
const devTargetFile = `${devTargetFolder}/Playground.vue`

if (!fs.existsSync(resolve(devTargetFile))) {
  shell.cp(
    resolve(`${devTargetFolder}/Playground.template.vue`),
    resolve(devTargetFile)
  )
}


<template>
  <v-input
    :error-messages="['Fatal error']"
    error
    disabled
  >
    Input
  </v-input>
</template>


<template>

    <v-alert
      dense
      type="info"
    >
      I'm a dense alert with a type of info
    </v-alert>
    <v-alert
      dense
      text
      type="success"
    >
      I'm a dense alert with the text prop and a type of success
    </v-alert>
    <v-alert
      dense
      border="left"
      type="warning"
    >
      I'm a dense alert with the border prop and a type of warning
    </v-alert>
    <v-alert
      dense
      outlined
      type="error"
    >
      I'm a dense alert with the outlined prop and a type of error
    </v-alert>

</template>


keep keep keep replace keep keep keep keep keep

import Vue from 'vue'

export type MouseHandler = (e: MouseEvent | TouchEvent) => any

export type MouseEvents = {
  [event: string]: {
    event: string
    passive?: boolean
    capture?: boolean
    once?: boolean
    stop?: boolean
    prevent?: boolean
    button?: number
    result?: any
  }
}

export type MouseEventsMap = {
  [event: string]: MouseHandler | MouseHandler[]
}

export default Vue.extend({
  name: 'mouse',

  methods: {
    getDefaultMouseEventHandlers (suffix: string, getEvent: MouseHandler): MouseEventsMap {
      return this.getMouseEventHandlers({
        ['click' + suffix]: { event: 'click' },
        ['contextmenu' + suffix]: { event: 'contextmenu', prevent: true, result: false },
        ['mousedown' + suffix]: { event: 'mousedown' },
        ['mousemove' + suffix]: { event: 'mousemove' },
        ['mouseup' + suffix]: { event: 'mouseup' },
        ['mouseenter' + suffix]: { event: 'mouseenter' },
        ['mouseleave' + suffix]: { event: 'mouseleave' },
        ['touchstart' + suffix]: { event: 'touchstart' },
        ['touchmove' + suffix]: { event: 'touchmove' },
        ['touchend' + suffix]: { event: 'touchend' },
      }, getEvent)
    },
    getMouseEventHandlers (events: MouseEvents, getEvent: MouseHandler): MouseEventsMap {
      const on: MouseEventsMap = {}

      for (const event in events) {
        const eventOptions = events[event]

        if (!this.$listeners[event]) continue

        // TODO somehow pull in modifiers

        const prefix = eventOptions.passive ? '&' : ((eventOptions.once ? '~' : '') + (eventOptions.capture ? '!' : ''))
        const key = prefix + eventOptions.event

        const handler: MouseHandler = e => {
          const mouseEvent: MouseEvent = e as MouseEvent
          if (eventOptions.button === undefined || (mouseEvent.buttons > 0 && mouseEvent.button === eventOptions.button)) {
            if (eventOptions.prevent) {
              e.preventDefault()
            }
            if (eventOptions.stop) {
              e.stopPropagation()
            }

            // Due to TouchEvent target always returns the element that is first placed
            // Even if touch point has since moved outside the interactive area of that element
            // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Touch/target
            // This block of code aims to make sure touchEvent is always dispatched from the element that is being pointed at
            if (e && 'touches' in e) {
              const classSeparator = ' '

              const eventTargetClasses = (e.currentTarget as HTMLElement)?.className.split(classSeparator)
              const currentTargets = document.elementsFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY)

              // Get "the same kind" current hovering target by checking
              // If element has the same class of initial touch start element (which has touch event listener registered)
              const currentTarget = currentTargets.find(t => t.className.split(classSeparator).some(c => eventTargetClasses.includes(c)))

              if (currentTarget &&
                !(e.target as HTMLElement)?.isSameNode(currentTarget)
              ) {
                currentTarget.dispatchEvent(new TouchEvent(e.type, {
                  changedTouches: e.changedTouches as unknown as Touch[],
                  targetTouches: e.targetTouches as unknown as Touch[],
                  touches: e.touches as unknown as Touch[],
                }))
                return
              }
            }

            this.$emit(event, getEvent(e), e)
          }

          return eventOptions.result
        }

        if (key in on) {
          /* istanbul ignore next */
          if (Array.isArray(on[key])) {
            (on[key] as MouseHandler[]).push(handler)
          } else {
            on[key] = [on[key], handler] as MouseHandler[]
          }
        } else {
          on[key] = handler
        }
      }

      return on
    },
  },
})


<template>
  <v-row justify="space-around">
    <v-date-picker
      v-model="date1"
      :show-current="false"
    ></v-date-picker>
    <v-date-picker
      v-model="date2"
      show-current="2013-07-13"
    ></v-date-picker>
  </v-row>
</template>

<script>
  export default {
    data () {
      return {
        date1: (new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)).toISOString().substr(0, 10),
        date2: '2013-07-29',
      }
    },
  }
</script>


// Components
import VToolbar from './VToolbar'

// Utilities
import { createSimpleFunctional } from '../../util/helpers'

const VToolbarTitle = createSimpleFunctional('v-toolbar__title')
const VToolbarItems = createSimpleFunctional('v-toolbar__items')

export {
  VToolbar,
  VToolbarItems,
  VToolbarTitle,
}

export default {
  $_vuetify_subcomponents: {
    VToolbar,
    VToolbarItems,
    VToolbarTitle,
  },
}


<template>
  <app-btn
    :to="{
      name: 'Documentation',
      params: {
        category: 'introduction',
        page: 'enterprise'
      }
    }"
    path="enterprise"
    @click="onClick"
  />
</template>

<script>
  // Utilities
  import { get } from 'vuex-pathify'

  export default {
    name: 'EnterpriseLink',

    computed: {
      name: get('route/name'),
    },

    methods: {
      onClick () {
        this.$gtag.event('click', {
          event_category: 'toolbar',
          event_label: 'enterprise',
          value: this.name,
        })
      },
    },
  }
</script>


getDefaultMouseEventHandlers (suffix: string, getEvent: MouseHandler): MouseEventsMap {

const listeners = Object.keys(this.$listeners)

acc[key] = { event: key.slice(0, -suffix.length) }


FROM mhart/alpine-node:12

COPY . .

EXPOSE 8095

RUN yarn
RUN yarn build
CMD ["yarn", "start"]


<template>
  <v-container>
    <v-row>
      <v-col
        cols="12"
        lg="6"
      >
        <v-menu
          v-model="menu1"
          :close-on-content-click="false"
          max-width="290"
        >
          <template v-slot:activator="{ on, attrs }">
            <v-text-field
              :value="computedDateFormattedMomentjs"
              clearable
              label="Formatted with Moment.js"
              readonly
              v-bind="attrs"
              v-on="on"
              @click:clear="date = null"
            ></v-text-field>
          </template>
          <v-date-picker
            v-model="date"
            @change="menu1 = false"
          ></v-date-picker>
        </v-menu>
      </v-col>

      <v-col
        cols="12"
        lg="6"
      >
        <v-menu
          v-model="menu2"
          :close-on-content-click="false"
          max-width="290"
        >
          <template v-slot:activator="{ on, attrs }">
            <v-text-field
              :value="computedDateFormattedDatefns"
              clearable
              label="Formatted with datefns"
              readonly
              v-bind="attrs"
              v-on="on"
              @click:clear="date = null"
            ></v-text-field>
          </template>
          <v-date-picker
            v-model="date"
            @change="menu2 = false"
          ></v-date-picker>
        </v-menu>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
  import moment from 'moment'
  import { format, parseISO } from 'date-fns'

  export default {
    data: () => ({
      // https://github.com/date-fns/date-fns/blob/master/docs/upgradeGuide.md#string-arguments
      date: format(parseISO(new Date().toISOString()), 'yyyy-MM-dd'),
      menu1: false,
      menu2: false,
    }),

    computed: {
      computedDateFormattedMomentjs () {
        return this.date ? moment(this.date).format('dddd, MMMM Do YYYY') : ''
      },
      computedDateFormattedDatefns () {
        return this.date ? format(parseISO(this.date), 'EEEE, MMMM do yyyy') : ''
      },
    },
  }
</script>

<codepen-resources lang="json">
  {
    "js": [
      "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js",
      "https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.29.0/date_fns.min.js"
    ]
  }
</codepen-resources>


<template>
  <v-card class="overflow-hidden">
    <v-app-bar
      absolute
      color="white"
      elevate-on-scroll
      scroll-target="#scrolling-techniques-7"
    >
      <v-app-bar-nav-icon></v-app-bar-nav-icon>

      <v-toolbar-title>Title</v-toolbar-title>

      <v-spacer></v-spacer>

      <v-btn icon>
        <v-icon>mdi-magnify</v-icon>
      </v-btn>

      <v-btn icon>
        <v-icon>mdi-heart</v-icon>
      </v-btn>

      <v-btn icon>
        <v-icon>mdi-dots-vertical</v-icon>
      </v-btn>
    </v-app-bar>
    <v-sheet
      id="scrolling-techniques-7"
      class="overflow-y-auto"
      max-height="600"
    >
      <v-container style="height: 1500px;">
      </v-container>
    </v-sheet>
  </v-card>
</template>


['contextmenu' + suffix]: { event: 'contextmenu', prevent: true, result: false },


@import './_variables.scss'

@mixin firefox()
  @-moz-document url-prefix()
    @content

.v-main
  +bootable()

  display: flex
  flex: 1 0 auto
  max-width: 100%
  transition: $main-transition

  &__wrap
    flex: 1 1 auto
    max-width: 100%
    position: relative

  +firefox()
    @media print
      display: block


// Types
import Vue, { VNode } from 'vue'

/* @vue/component */
export default Vue.extend({
  name: 'v-list-item-icon',

  functional: true,

  render (h, { data, children }): VNode {
    data.staticClass = (`v-list-item__icon ${data.staticClass || ''}`).trim()

    return h('div', data, children)
  },
})


export { ClickOutside } from './click-outside'
export { Intersect } from './intersect'
export { Mutate } from './mutate'
export { Resize } from './resize'
export { Ripple } from './ripple'
export { Scroll } from './scroll'
export { Touch } from './touch'


['mousedown' + suffix]: { event: 'mousedown' },

['mousemove' + suffix]: { event: 'mousemove' },

['mouseup' + suffix]: { event: 'mouseup' },

['mouseenter' + suffix]: { event: 'mouseenter' },

['mouseleave' + suffix]: { event: 'mouseleave' },

['touchstart' + suffix]: { event: 'touchstart' },

['touchmove' + suffix]: { event: 'touchmove' },

['touchend' + suffix]: { event: 'touchend' },

getMouseEventHandlers (events: MouseEvents, getEvent: MouseHandler): MouseEventsMap {

keep replace replace replace replace replace replace replace replace keep keep keep


import VAvatar from './VAvatar'

export { VAvatar }
export default VAvatar


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VDialog.ts should only set tabindex if active 1`] = `

`;

exports[`VDialog.ts should only set tabindex if active 2`] = `

`;

exports[`VDialog.ts should render a disabled component and match snapshot 1`] = `

`;

exports[`VDialog.ts should render a eager component and match snapshot 1`] = `

`;

exports[`VDialog.ts should render a fullscreen component and match snapshot 1`] = `

`;

exports[`VDialog.ts should render a persistent component and match snapshot 1`] = `

`;

exports[`VDialog.ts should render a scrollable component and match snapshot 1`] = `

`;

exports[`VDialog.ts should render component and match snapshot 1`] = `

`;

exports[`VDialog.ts should render component with custom origin and match snapshot 1`] = `

`;

exports[`VDialog.ts should render component with custom transition and match snapshot 1`] = `

`;

exports[`VDialog.ts should render component with custom width (max-width) and match snapshot 1`] = `

`;

exports[`VDialog.ts should render component with custom width and match snapshot 1`] = `

`;


---
meta:
  title: Theme configuration
  description: Setup your application's theme and supplemental colors in a flash.
  keywords: themes
related:
  - /styles/colors/
  - /styles/transitions/
  - /getting-started/wireframes/
nav: Theme
---

# Theme configuration

Easily change the colors of your application programmatically. Rebuild the default stylesheet and customize various aspects of the framework for your particular needs.

<promoted-ad slug="vuemastery-themes" />

## Theme generator

Discover and generate new color themes for your **Vuetify** applications using our [Theme Generator](https://theme-generator.vuetifyjs.com) tool.

## Light and dark

Vuetify supports both **light** and **dark** variants of the Material Design spec. This designation starts at the root application component, `v-app` and is supported by majority of components. By default, your application will use the **light** theme, but this can be easily overwritten by adding the **dark** option in the theme service.

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  theme: { dark: true },
})
```

When you designate a component as light or dark, all of its children will inherit and apply the same unless otherwise specified. You can manually turn **dark** on and off by changing `this.$vuetify.theme.dark` to **true** or **false**.

## Customizing

By default, Vuetify has a standard theme applied for all components.

```js
{
  primary: '#1976D2',
  secondary: '#424242',
  accent: '#82B1FF',
  error: '#FF5252',
  info: '#2196F3',
  success: '#4CAF50',
  warning: '#FFC107',
}
```

This can be easily changed. Simply pass a **theme** property to the Vuetify constructor. You can choose to modify all or only some of the theme properties, with the remaining inheriting from the default.

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

const vuetify = new Vuetify({
  theme: {
    themes: {
      light: {
        primary: '#3f51b5',
        secondary: '#b0bec5',
        accent: '#8c9eff',
        error: '#b71c1c',
      },
    },
  },
})
```

You can also use the pre-defined material colors.

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

import colors from 'vuetify/lib/util/colors'

const vuetify = new Vuetify({
  theme: {
    themes: {
      light: {
        primary: colors.purple,
        secondary: colors.grey.darken1,
        accent: colors.shades.black,
        error: colors.red.accent3,
      },
      dark: {
        primary: colors.blue.lighten3,
      },
    },
  },
})
```

By default, the theme service will use your application's primary color for **anchor** tags. You can override this by adding an anchor property to the theme:

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

const vuetify = new Vuetify({
  theme: {
    themes: {
      light: {
        primary: '#3f51b5',
        secondary: '#b0bec5',
        anchor: '#8c9eff',
      },
    },
  },
})

export default vuetify
```

Under the hood, Vuetify will generate css classes based upon these values that will be accessible in the DOM. These classes will follow the same markup as other helper classes, `primary` or `secondary--text` for example. If you supply an entire color object (as in `colors.purple` above), the lighten/darken variations will be used directly instead of being generated.

These values will also be made available on the instance **$vuetify** object under the **theme** property. This allows you to _dynamically_ modify your theme. Behind the scenes, Vuetify will regenerate and update your theme classes, seamlessly updating your application.

```js
// Light theme
this.$vuetify.theme.themes.light.primary = '#4caf50'

// Dark theme
this.$vuetify.theme.themes.dark.primary = '#4caf50'
```

### Custom theme variants

While Vuetify automatically generates _lighten_ and _darken_ variants for theme colors, you may want to control this yourself. Simply pass a theme object that contains the variants that you wish to modify. Anything not provided will still be generated for you.

```js
// src/plugins/vuetify/theme.js

import colors from 'vuetify/lib/util/colors'

export default {
  primary: {
    base: colors.purple.base,
    darken1: colors.purple.darken2,
  },
  secondary: colors.indigo,
  // All keys will generate theme styles,
  // Here we add a custom `tertiary` color
  tertiary: colors.pink.base,
}
```

You can now import your custom theme object and apply it to Vuetify

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import light from './theme'

Vue.use(Vuetify)

export default new Vuetify({
  theme: {
    themes: { light },
  },
})
```

Below is a full list of the overwritable keys on the theme object:

```ts
interface ParsedThemeItem {
  base: string
  lighten5: string
  lighten4: string
  lighten3: string
  lighten2: string
  lighten1: string
  darken1: string
  darken2: string
  darken3: string
  darken4: string

  [name: string]: string
}
```

### Disable theme

You can disable theme functionality by using the **disable** property with a value of **true**. This will prevent the creation of the Vuetify stylesheet.

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

const vuetify = new Vuetify({
  theme: { disable: true },
})
```

## Options

Vuetify generates theme styles at run-time for SPA's and server side for SSR applications. The generated styles will be placed in a `<style>` tag with an **id** of **vuetify-theme-stylesheet**.

### Minification

The `minifyTheme` option allows you to provide a custom minification implementation. This helps to reduce the initial page size and is suggested to be paired with [`options.themeCache`](#caching). In this example we use the [minify-css-string](https://github.com/bentatum/minify-css-string) package to minify the *generated theme styles*.

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import minifyTheme from 'minify-css-string'

Vue.use(Vuetify)

export default new Vuetify({
  theme: {
    options: { minifyTheme },
  },
})
```

### Caching

You have the option to pass a custom `themeCache` implementation. This allows you to skip the need to recalculate the theme object. Below is an example using the [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) property:

```js
// src/plugins/vuetify.js

export default new Vuetify({
  theme: {
    options: {
      themeCache: {
        get: key => localStorage.getItem(key),
        set: (key, value) => localStorage.setItem(key, value),
      },
    },
  },
})
```

<alert type="warning">

  The provided `themeCache` object **must** contain a `get` and `set` method. Use them for retrieving and setting the *generated css* string.

</alert>

Caching can also be done through [lru-cache](https://github.com/isaacs/node-lru-cache). This is specifically useful for SSR (Server Side Rendered) applications.

```js
// src/plugins/vuetify.js

const themeCache = new LRU({
  max: 10,
  maxAge: 1000 * 60 * 60, // 1 hour
})

export default new Vuetify({
  theme: {
    options: { themeCache },
  },
})
```

### Custom properties

Enabling `customProperties` will also generate a [css variable](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) for each theme color, which you can then use in your components' `<style>` blocks.

<alert type="warning">

  Custom properties are not natively supported in Internet Explorer. Polyfills are available—**with some limitations**—for Internet Explorer 11:

- [Custom properties polyfill](https://github.com/nuxodin/ie11CustomProperties)
- [CSS vars polyfill](https://github.com/jhildenbiddle/css-vars-ponyfill)

</alert>

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

export default new Vuetify({
  theme: {
    options: { customProperties: true },
  },
})
```

```html
<style scoped>
  .something {
    color: var(--v-primary-base);
    background-color: var(--v-accent-lighten2);
  }
</style>
```

### CSP nonce

Pages with the `script-src` or `style-src` CSP rules enabled may require a **nonce** to be specified for embedded style tags.

```html
<!-- Use with script-src -->
Content-Security-Policy: script-src 'self' 'nonce-dQw4w9WgXcQ'

<!-- Use with style-src -->
Content-Security-Policy: style-src 'self' 'nonce-dQw4w9WgXcQ'
```

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

export default new Vuetify({
  theme: {
    options: { cspNonce: 'dQw4w9WgXcQ' },
  },
})
```

### Variations

When Vuetify generates your *application's theme*, it creates **9 variants** for each color. For majority of users, these variants are rarely used. This is an **opt in** feature that will be __false by default__ in the next major version.

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

export default new Vuetify({
  theme: {
    options: { variations: false },
  },
})
```

## Theme Provider

The Vuetify theme system is propagated through the [provide](https://vuejs.org/v2/api/#provide-inject) functionality in Vue. There may be situations in which you need to manually change the provided theme (dark or light).

### API

- [v-theme-provider](/api/v-theme-provider)

<inline-api page="features/theme" />

### Example

Use the `v-theme-provider` to manually overwrite all children component's current theme **(light/dark)**. In the following example, the root `v-card` is explicitly set to `dark` with 2 children lists. The first one inherits from the parent `v-card` while the second is explicitly set to match the **root** Vuetify theme.

<example file="theme/misc-provider" />

<backmatter />


// Styles
import './VSlideGroup.sass'

// Components
import { VFadeTransition } from '@/components/transitions'
import { VIcon } from '@/components/VIcon'

// Composables
import { makeGroupProps, useGroup } from '@/composables/group'
import { makeTagProps } from '@/composables/tag'
import { useDisplay } from '@/composables'
import { useResizeObserver } from '@/composables/resizeObserver'
import { useRtl } from '@/composables/rtl'

// Utilities
import { bias, calculateCenteredOffset, calculateUpdatedOffset } from './helpers'
import { clamp, defineComponent, useRender } from '@/util'
import { computed, ref, watch, watchEffect } from 'vue'

// Types
import type { GroupProvide } from '@/composables/group'
import type { InjectionKey } from 'vue'

export const VSlideGroupSymbol: InjectionKey<GroupProvide> = Symbol.for('vuetify:v-slide-group')

export const VSlideGroup = defineComponent({
  name: 'VSlideGroup',

  props: {
    activeClass: {
      type: String,
      default: 'v-slide-item--active',
    },
    centerActive: Boolean,
    direction: {
      type: String,
      default: 'horizontal',
    },
    symbol: {
      type: null,
      default: VSlideGroupSymbol,
    },
    nextIcon: {
      type: String,
      default: '$next',
    },
    prevIcon: {
      type: String,
      default: '$prev',
    },
    showArrows: {
      type: [Boolean, String],
      validator: (v: any) => (
        typeof v === 'boolean' || [
          'always',
          'desktop',
          'mobile',
        ].includes(v)
      ),
    },
    ...makeTagProps(),
    ...makeGroupProps(),
  },

  emits: {
    'update:modelValue': (value: any) => true,
  },

  setup (props, { slots }) {
    const { isRtl } = useRtl()
    const { mobile } = useDisplay()
    const group = useGroup(props, props.symbol)
    const isOverflowing = ref(false)
    const scrollOffset = ref(0)
    const containerSize = ref(0)
    const contentSize = ref(0)
    const isHorizontal = computed(() => props.direction === 'horizontal')

    const { resizeRef: containerRef, contentRect: containerRect } = useResizeObserver()
    const { resizeRef: contentRef, contentRect } = useResizeObserver()

    watchEffect(() => {
      if (!containerRect.value || !contentRect.value) return

      const sizeProperty = isHorizontal.value ? 'width' : 'height'

      containerSize.value = containerRect.value[sizeProperty]
      contentSize.value = contentRect.value[sizeProperty]

      isOverflowing.value = containerSize.value + 1 < contentSize.value
    })

    watch(group.selected, selected => {
      if (!selected.length || !contentRef.value) return

      const index = group.items.value.findIndex(item => item.id === selected[selected.length - 1])

      // TODO: Is this too naive? Should we store element references in group composable?
      const selectedElement = contentRef.value.children[index] as HTMLElement

      if (index === 0 || !isOverflowing.value) {
        scrollOffset.value = 0
      } else if (props.centerActive) {
        scrollOffset.value = calculateCenteredOffset({
          selectedElement,
          containerSize: containerSize.value,
          contentSize: contentSize.value,
          isRtl: isRtl.value,
          isHorizontal: isHorizontal.value,
        })
      } else if (isOverflowing.value) {
        scrollOffset.value = calculateUpdatedOffset({
          selectedElement,
          containerSize: containerSize.value,
          contentSize: contentSize.value,
          isRtl: isRtl.value,
          currentScrollOffset: scrollOffset.value,
          isHorizontal: isHorizontal.value,
        })
      }
    })

    const disableTransition = ref(false)

    let startTouch = 0
    let startOffset = 0

    function onTouchstart (e: TouchEvent) {
      const sizeProperty = isHorizontal.value ? 'clientX' : 'clientY'
      startOffset = scrollOffset.value
      startTouch = e.touches[0][sizeProperty]
      disableTransition.value = true
    }

    function onTouchmove (e: TouchEvent) {
      if (!isOverflowing.value) return

      const sizeProperty = isHorizontal.value ? 'clientX' : 'clientY'
      scrollOffset.value = startOffset + startTouch - e.touches[0][sizeProperty]
    }

    function onTouchend (e: TouchEvent) {
      const maxScrollOffset = contentSize.value - containerSize.value

      if (isRtl.value) {
        if (scrollOffset.value > 0 || !isOverflowing.value) {
          scrollOffset.value = 0
        } else if (scrollOffset.value <= -maxScrollOffset) {
          scrollOffset.value = -maxScrollOffset
        }
      } else {
        if (scrollOffset.value < 0 || !isOverflowing.value) {
          scrollOffset.value = 0
        } else if (scrollOffset.value >= maxScrollOffset) {
          scrollOffset.value = maxScrollOffset
        }
      }

      disableTransition.value = false
    }

    function onScroll () {
      containerRef.value && (containerRef.value.scrollLeft = 0)
    }

    const isFocused = ref(false)
    function onFocusin (e: FocusEvent) {
      isFocused.value = true

      if (!isOverflowing.value || !contentRef.value) return

      // Focused element is likely to be the root of an item, so a
      // breadth-first search will probably find it in the first iteration
      for (const el of e.composedPath()) {
        for (const item of contentRef.value.children) {
          if (item === el) {
            scrollOffset.value = calculateUpdatedOffset({
              selectedElement: item as HTMLElement,
              containerSize: containerSize.value,
              contentSize: contentSize.value,
              isRtl: isRtl.value,
              currentScrollOffset: scrollOffset.value,
              isHorizontal: isHorizontal.value,
            })
            return
          }
        }
      }
    }

    function onFocusout (e: FocusEvent) {
      isFocused.value = false
    }

    function onFocus (e: FocusEvent) {
      if (
        !isFocused.value &&
        !(e.relatedTarget && contentRef.value?.contains(e.relatedTarget as Node))
      ) focus()
    }

    function onKeydown (e: KeyboardEvent) {
      if (!contentRef.value) return

      if (e.key === (isHorizontal.value ? 'ArrowRight' : 'ArrowDown')) {
        focus('next')
      } else if (e.key === (isHorizontal.value ? 'ArrowLeft' : 'ArrowUp')) {
        focus('prev')
      } else if (e.key === 'Home') {
        focus('first')
      } else if (e.key === 'End') {
        focus('last')
      }
    }

    function focus (location?: 'next' | 'prev' | 'first' | 'last') {
      if (!contentRef.value) return

      if (!location) {
        contentRef.value.querySelector('[tabindex]')
        const focusable = [...contentRef.value.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        )].filter(el => !el.hasAttribute('disabled')) as HTMLElement[]
        focusable[0]?.focus()
      } else if (location === 'next') {
        const el = contentRef.value.querySelector(':focus')?.nextElementSibling as HTMLElement | undefined
        if (el) el.focus()
        else focus('first')
      } else if (location === 'prev') {
        const el = contentRef.value.querySelector(':focus')?.previousElementSibling as HTMLElement | undefined
        if (el) el.focus()
        else focus('last')
      } else if (location === 'first') {
        (contentRef.value.firstElementChild as HTMLElement)?.focus()
      } else if (location === 'last') {
        (contentRef.value.lastElementChild as HTMLElement)?.focus()
      }
    }

    function scrollTo (location: 'prev' | 'next') {
      const sign = isRtl.value ? -1 : 1
      const newAbosluteOffset = sign * scrollOffset.value +
        (location === 'prev' ? -1 : 1) * containerSize.value

      scrollOffset.value = sign * clamp(newAbosluteOffset, 0, contentSize.value - containerSize.value)
    }

    const contentStyles = computed(() => {
      const scrollAmount = scrollOffset.value <= 0
        ? bias(-scrollOffset.value)
        : scrollOffset.value > contentSize.value - containerSize.value
          ? -(contentSize.value - containerSize.value) + bias(contentSize.value - containerSize.value - scrollOffset.value)
          : -scrollOffset.value

      return {
        transform: `translate${isHorizontal.value ? 'X' : 'Y'}(${scrollAmount}px)`,
        transition: disableTransition.value ? 'none' : '',
        willChange: disableTransition.value ? 'transform' : '',
      }
    })

    const slotProps = computed(() => ({
      next: group.next,
      prev: group.prev,
      select: group.select,
      isSelected: group.isSelected,
    }))

    const hasAffixes = computed(() => {
      switch (props.showArrows) {
        // Always show arrows on desktop & mobile
        case 'always': return true

        // Always show arrows on desktop
        case 'desktop': return mobile.value

        // Show arrows on mobile when overflowing.
        // This matches the default 2.2 behavior
        case true: return isOverflowing.value || Math.abs(scrollOffset.value) > 0

        // Always show on mobile
        case 'mobile': return (
          mobile.value ||
          (isOverflowing.value || Math.abs(scrollOffset.value) > 0)
        )

        // https://material.io/components/tabs#scrollable-tabs
        // Always show arrows when
        // overflowed on desktop
        default: return (
          !mobile.value &&
          (isOverflowing.value || Math.abs(scrollOffset.value) > 0)
        )
      }
    })

    const hasPrev = computed(() => {
      return hasAffixes.value && scrollOffset.value > 0
    })

    const hasNext = computed(() => {
      if (!hasAffixes.value) return false

      // Check one scroll ahead to know the width of right-most item
      return contentSize.value > Math.abs(scrollOffset.value) + containerSize.value
    })

    useRender(() => (
      <props.tag
        class={[
          'v-slide-group',
          {
            'v-slide-group--vertical': !isHorizontal.value,
            'v-slide-group--has-affixes': hasAffixes.value,
            'v-slide-group--is-overflowing': isOverflowing.value,
          },
        ]}
        tabindex={ (isFocused.value || group.selected.value.length) ? -1 : 0 }
        onFocus={ onFocus }
      >
        { hasAffixes.value && (
          <div
            class={[
              'v-slide-group__prev',
              { 'v-slide-group__prev--disabled': !hasPrev.value },
            ]}
            onClick={ () => scrollTo('prev') }
          >
            { slots.prev?.(slotProps.value) ?? (
              <VFadeTransition>
                <VIcon icon={ props.prevIcon }></VIcon>
              </VFadeTransition>
            ) }
          </div>
        ) }

        <div
          ref={ containerRef }
          class="v-slide-group__container"
          onScroll={ onScroll }
        >
          <div
            ref={ contentRef }
            class="v-slide-group__content"
            style={ contentStyles.value }
            onTouchstart={ onTouchstart }
            onTouchmove={ onTouchmove }
            onTouchend={ onTouchend }
            onFocusin={ onFocusin }
            onFocusout={ onFocusout }
            onKeydown={ onKeydown }
          >
            { slots.default?.(slotProps.value) }
          </div>
        </div>

        { hasAffixes.value && (
          <div
            class={[
              'v-slide-group__next',
              { 'v-slide-group__next--disabled': !hasNext.value },
            ]}
            onClick={ () => scrollTo('next') }
          >
            { slots.next?.(slotProps.value) ?? (
              <VFadeTransition>
                <VIcon icon={ props.nextIcon }></VIcon>
              </VFadeTransition>
            ) }
          </div>
        ) }
      </props.tag>
    ))

    return {
      selected: group.selected,
      scrollTo,
      scrollOffset,
      focus,
    }
  },
})

export type VSlideGroup = InstanceType<typeof VSlideGroup>


<template>
  <v-container fluid>
    <v-row class="light--text">
      <v-col cols="4">
        on
      </v-col>
      <v-col cols="4">
        off
      </v-col>
      <v-col cols="4">
        indeterminate
      </v-col>
    </v-row>
    <v-row>
      <v-col cols="4">
        <v-checkbox
          :model-value="true"
        ></v-checkbox>
      </v-col>
      <v-col cols="4">
        <v-checkbox :model-value="false"></v-checkbox>
      </v-col>
      <v-col cols="4">
        <v-checkbox
          indeterminate
        ></v-checkbox>
      </v-col>
    </v-row>
    <v-row class="light--text">
      <v-col cols="4">
        on disabled
      </v-col>
      <v-col cols="4">
        off disabled
      </v-col>
    </v-row>
    <v-row>
      <v-col cols="4">
        <v-checkbox
          :model-value="true"
          disabled
        ></v-checkbox>
      </v-col>
      <v-col cols="4">
        <v-checkbox
          :model-value="false"
          disabled
        ></v-checkbox>
      </v-col>
      <v-col cols="4">
        <v-checkbox
          disabled
          indeterminate
        ></v-checkbox>
      </v-col>
    </v-row>
  </v-container>
</template>


<template>
  <v-container class="team-members">
    <v-row>
      <template v-for="(member, i) in members" :key="i">
        <v-col
          cols="12"
        >
          <team-member v-bind="{ member }" />
        </v-col>

        <v-divider
          v-if="i < members.length - 1"
          :key="`divider-${i}`"
          class="mb-1 flex-1-1-100"
        />
      </template>
    </v-row>
  </v-container>
</template>

<script lang="ts">
  // Utilities
  import { computed, defineComponent } from 'vue'
  import { useTeamStore } from '@/store/team'

  import TeamMember from './TeamMember.vue'

  export default defineComponent({
    name: 'TeamMembers',

    components: {
      TeamMember,
    },

    props: { team: String },

    setup (props) {
      const team = useTeamStore()
      const members = computed(() => team.members.filter(member => member.team === props.team))

      return { members }
    },
  })
</script>

<style lang="sass">
  .team-members
    .v-markdown
      > p
        margin: 0

      a
        text-decoration: none
</style>


<template>
  <v-app id="inspire">
    <v-app-bar
      app
      shrink-on-scroll
    >
      <v-app-bar-nav-icon></v-app-bar-nav-icon>

      <v-toolbar-title>Application</v-toolbar-title>

      <v-spacer></v-spacer>

      <v-btn icon>
        <v-icon>mdi-dots-vertical</v-icon>
      </v-btn>
    </v-app-bar>

    <v-main>
      <v-container>
        <v-row>
          <v-col
            v-for="n in 24"
            :key="n"
            cols="4"
          >
            <v-card height="200"></v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-main>
  </v-app>
</template>

<script>
  export default {
    //
  }
</script>


if (!containerRect.value || !contentRect.value) return

const sizeProperty = isHorizontal.value ? 'width' : 'height'

if (!containerRect.value || !contentRef.value) return

// Styles
import './VTabs.sass'

// Components
import { VSlideGroup } from '@/components/VSlideGroup'
import { VTab } from './VTab'

// Composables
import { makeDensityProps, useDensity } from '@/composables/density'
import { makeTagProps } from '@/composables/tag'
import { provideDefaults } from '@/composables/defaults'

// Utilities
import { computed, toRef } from 'vue'
import { defineComponent } from '@/util'

// Types
import type { GroupProvide } from '@/composables/group'
import type { InjectionKey, PropType } from 'vue'

export type TabItem = string | Record<string, any>

function parseItems (items: TabItem[] | undefined) {
  if (!items) return []

  return items.map(item => {
    if (typeof item === 'string') return { title: item, value: item }

    return item
  })
}

export const VTabsSymbol: InjectionKey<GroupProvide> = Symbol.for('vuetify:v-tabs')

export const VTabs = defineComponent({
  name: 'VTabs',

  props: {
    alignWithTitle: Boolean,
    color: String,
    direction: {
      type: String as PropType<'horizontal' | 'vertical'>,
      default: 'horizontal',
    },
    fixedTabs: Boolean,
    items: {
      type: Array as PropType<TabItem[]>,
      default: () => ([]),
    },
    stacked: Boolean,
    backgroundColor: String,
    centered: Boolean,
    grow: Boolean,
    height: {
      type: [Number, String],
      default: undefined,
    },
    hideSlider: Boolean,
    optional: Boolean,
    right: Boolean,
    sliderColor: String,

    ...makeDensityProps(),
    ...makeTagProps(),
  },

  setup (props, { slots, attrs }) {
    const parsedItems = computed(() => parseItems(props.items))
    const { densityClasses } = useDensity(props)

    provideDefaults({
      VTab: {
        color: toRef(props, 'color'),
        direction: toRef(props, 'direction'),
        stacked: toRef(props, 'stacked'),
        fixed: toRef(props, 'fixedTabs'),
        sliderColor: toRef(props, 'sliderColor'),
        hideSlider: toRef(props, 'hideSlider'),
      },
    })

    return () => (
      <VSlideGroup
        class={[
          'v-tabs',
          `v-tabs--${props.direction}`,
          {
            'v-tabs--align-with-title': props.alignWithTitle,
            'v-tabs--centered': props.centered,
            'v-tabs--fixed-tabs': props.fixedTabs,
            'v-tabs--grow': props.grow,
            'v-tabs--right': props.right,
            'v-tabs--stacked': props.stacked,
          },
          densityClasses.value,
        ]}
        role="tablist"
        symbol={ VTabsSymbol }
        mandatory="force"
        direction={ props.direction }
        { ...attrs }
      >
        { slots.default ? slots.default() : parsedItems.value.map(item => (
          <VTab { ...item } key={ item.title } />
        )) }
      </VSlideGroup>
    )
  },
})

export type VTabs = InstanceType<typeof VTabs>