Modal
The Modal component can be used to create dialogs, drawers, or anything else that needs to display new content to a user without navigating to a new page.
Features
- Provides a backdrop component to prevent interaction with the rest of the app.
- Page scrolling is disabled while open.
- Ensures the tab key maintains focus within the modal.
- Adds appropriate ARIA attributes and roles.
INFO
The animations for Modal
are written in Modals.vue
and use the <TransitionGroup>
component from Vue.
Basic Usage
<script setup lang="ts">
import useModals from '@leaflink/stash/useModals';
import Button from '@leaflink/stash/components/Button/Button.vue';
import BasicModal from './BasicModal.vue';
const modals = useModals();
</script>
<template>
<Button @click="modals.open({ component: BasicModal })">
Open Basic Modal
</Button>
</template>
<script setup lang="ts">
import Modal from '../../../src/components/Modal/Modal.vue';
</script>
<template>
<Modal title="Your Modal Title">
<h3 class="tw-mb-5">This is a basic modal.</h3>
<p>
Service worker lazy eval native singleton package manager functional programming. Shareware container S3 quick
sort lazy eval programmer. Bootcamp ICO modern bundle LGTM distributed callback S3 module. Developer avocado
virtual DOM accessibility architecture variable pair programming.
</p>
</Modal>
</template>
Footer Actions
The Modal component makes it easier to render buttons in the footer by using the #actions
slot.
<script setup lang="ts">
import Button from '../../../src/components/Button/Button.vue';
import Modal from '../../../src/components/Modal/Modal.vue';
const emit = defineEmits<{
(e: 'dismiss'): void;
}>();
function onSave() {
alert('Saved!');
emit('dismiss');
}
</script>
<template>
<Modal title="Your Modal Title" @dismiss="emit('dismiss')">
<h3 class="tw-mb-5">This is a modal with actions.</h3>
<p>
Concurrency meta-programming composition configuration whiteboard cowboy coding looks good to me big O perf
matters. Lazy directed acyclic graph streams branch containerized browser AWS CSS-in-JS yarn. Key-value reflog
hardcoded mock idiosyncratic contexts model ship it bike-shedding npm browser. UI clean code views cache homebrew
scale CSS-in-JS functional programming JSX. Hacker News tabs vs spaces model view-model Babel parent var sudo
static.
</p>
<template #actions>
<Button secondary @click="emit('dismiss')">Cancel</Button>
<Button @click="onSave">Save</Button>
</template>
</Modal>
</template>
Contrasting Background
To give the modal body a gray background color, use the contrast
prop.
<script setup lang="ts">
import Modal from '../../../src/components/Modal/Modal.vue';
</script>
<template>
<Modal title="Your Modal Title" contrast>
<h3 class="tw-mb-5">This is a modal with a contrasting gray background.</h3>
<p>
Whiteboard Byzantine fault tolerance subclass presenter convention domain. Imperative-mood documentation driven
SRE lazy load pattern reflection. Test-driven convention UI CSS-in-JS distributed GraphQL resolve static typing.
Mechanical keyboard YAML abstraction backend free as in beer Twitter resolve.
</p>
</Modal>
</template>
Custom Footer
Instead of using the #actions
slot, you can use the #footer
slot to render whatever you want in the modal footer.
<script setup lang="ts">
import Button from '../../../src/components/Button/Button.vue';
import Modal from '../../../src/components/Modal/Modal.vue';
const emit = defineEmits<{
(e: 'dismiss'): void;
}>();
function onReset() {
alert('Reset!');
emit('dismiss');
}
function onSave() {
alert('Saved!');
emit('dismiss');
}
</script>
<template>
<Modal title="Your Modal Title" @dismiss="emit('dismiss')">
<h3 class="tw-mb-5">This is a modal with a custom footer.</h3>
<p>
UI waterfall protected pair programming database proof of stake Slack blockchain. REST stack DevTools contribution
configuration developer freelancer. REST Edge private Ruby devops cache IoT. Minimum viable product Hacker News
big O cache sudo reactive Internet Explorer object library. Lazy eval Stack Overflow Backbone.js test double lazy
load senior-engineer accessibility.
</p>
<template #footer>
<div class="tw-flex tw-flex-col tw-gap-3 lg:tw-flex-row lg:tw-justify-between lg:tw-gap-6">
<Button inline @click="onReset">Reset</Button>
<div class="tw-flex tw-flex-col tw-gap-3 lg:tw-flex-row lg:tw-gap-6">
<Button secondary @click="emit('dismiss')">Cancel</Button>
<Button @click="onSave">Save</Button>
</div>
</div>
</template>
</Modal>
</template>
Header Action
You can add a button to the left of the modal title with the #header-action
slot.
<script setup lang="ts">
import Button from '../../../src/components/Button/Button.vue';
import Icon from '../../../src/components/Icon/Icon.vue';
import Modal from '../../../src/components/Modal/Modal.vue';
const emit = defineEmits<{
(e: 'dismiss'): void;
}>();
function onBack() {
alert('Back clicked!');
emit('dismiss');
}
</script>
<template>
<Modal title="Your Modal Title" @dismiss="emit('dismiss')">
<template #headerAction>
<Button icon @click="onBack">
<Icon class="tw-text-white" name="chevron-left" />
</Button>
</template>
<h3 class="tw-mb-5">This modal has a header action.</h3>
<p>
Yarn Chrome proof of stake RSS feed REST Linux class. Branch cache antipattern constant microservices sudo UX
markup frame rate quick sort. Cloud Github interface distributed engineer Babel duck typing. SOAP a place for
everything AI build tool API tree shaking Backbone.js scrum master compile. Bootcamp quick sort XML driver
ecommerce platform infrastructure fault tolerant raspberry pi API budget.
</p>
</Modal>
</template>
Size
To adjust the size of the modal, use the size
prop.
<script setup lang="ts">
import Modal from '../../../src/components/Modal/Modal.vue';
</script>
<template>
<Modal title="Your Modal Title" size="narrow">
<h3 class="tw-mb-5">This is a narrow modal.</h3>
<p>
Package manager webpack legacy Internet Explorer static typing state OTP. Bubble sort compilation API compression
Reddit FP ecommerce platform serverless Agile. Merge sort graph casting container Reddit i subclass static typing
mock ecommerce platform. Diversity and inclusion junior private YAML proof of stake budget cross-post. Quick sort
OTP maintainable const graph tabs vs spaces idiosyncratic contexts engineer documentation driven.
</p>
</Modal>
</template>
<script setup lang="ts">
import Modal from '../../../src/components/Modal/Modal.vue';
</script>
<template>
<Modal title="Your Modal Title" size="wide">
<h3 class="tw-mb-5">This is a wide modal.</h3>
<p class="tw-mb-5">
Grep git waterfall command-line junior CSS-in-JS homebrew. Elixir continuous integration naming things ELF CS
degree rebase consensus presenter keycaps emoji. Branch programmer JSX whiteboard duck typing const. Idiosyncratic
contexts concurrency mobile app scale child CLI proof of stake junior. Firefox Angular compiler pivot imagemagick
resolve architecture.
</p>
<p>
Blog controller cross-post concurrent free as in beer responsive configuration DAG whiteboard IoT. Slack bootcamp
Reddit transpile public XML design reflection Chrome. Callback hell dog-piling directed acyclic graph frame rate
concurrent ecommerce platform legacy AI internet button test double.
</p>
</Modal>
</template>
Scrollable Content
Use the scrollable
prop to prevent the modal from expanding past the screen edge when the content inside the modal is long enough. The scrollable
prop also adds a scrollbar within the modal body.
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
import Button from '../../../src/components/Button/Button.vue';
import CurrencyInput from '../../../src/components/CurrencyInput/CurrencyInput.vue';
import Field from '../../../src/components/Field/Field.vue';
import Input from '../../../src/components/Input/Input.vue';
import Modal from '../../../src/components/Modal/Modal.vue';
import RadioGroup from '../../../src/components/RadioGroup/RadioGroup.vue';
import RadioNew from '../../../src/components/RadioNew/RadioNew.vue';
import Select from '../../../src/components/Select/Select.vue';
import Switch from '../../../src/components/Switch/Switch.vue';
// Since Quill uses the browser api, defineClientComponent can lazy load the Text Editor in a non SSR environment
const TextEditor = defineAsyncComponent(() => import('../../../src/components/TextEditor/TextEditor.vue'));
const emit = defineEmits<{
close: [];
dismiss: [];
}>();
const formState = ref({
sampleRequests: false,
sellInMultiples: false,
showInventoryToBuyers: false,
allowOrderInFractionalQuantities: false,
});
const wholesalePrice = ref();
const retailPrice = ref();
const salePrice = ref();
</script>
<template>
<Modal title="Add Product Template" scrollable @dismiss="emit('dismiss')">
<form @submit.prevent>
<Field id="template-name" label="Template name" class="tw-mb-6" is-required>
<Input placeholder="Input text" />
</Field>
<div class="tw-grid md:tw-grid-cols-2 md:tw-gap-6">
<Select label="Brand" class="tw-mb-6" placeholder="---" :options="[]" is-required single />
<Select label="Product line" class="tw-mb-6" :options="[]" single />
</div>
<div class="tw-grid md:tw-grid-cols-2 md:tw-gap-6">
<Select label="Licenses" class="tw-mb-6" placeholder="---" :options="[]" is-required single />
<Select
display-by="displayValue"
track-by="value"
label="Status"
class="tw-mb-6"
placeholder="---"
:options="[]"
is-required
hide-search
single
/>
</div>
<TextEditor label="Description" class="tw-mb-6" />
<div class="tw-grid md:tw-grid-cols-3 md:tw-gap-6">
<Field label="Wholesale Price" class="tw-mb-6" is-required>
<CurrencyInput v-model="wholesalePrice" placeholder="0.00" />
</Field>
<CurrencyInput v-model="retailPrice" label="Retail (MSRP) Price" placeholder="0.00" class="tw-mb-6" />
<CurrencyInput v-model="salePrice" label="Sale Price" placeholder="0.00" class="tw-mb-6" />
</div>
<div class="tw-grid md:tw-grid-cols-3 md:tw-gap-6">
<Switch v-model:checked="formState.sampleRequests" class="tw-mb-6" label="Sample requests" />
<Switch v-model:checked="formState.sellInMultiples" class="tw-mb-6" label="Sell in multiples" />
<Input label="Individual units per case" class="tw-mb-6" placeholder="1.0" type="number" />
</div>
<div class="tw-grid md:tw-grid-cols-2 md:tw-gap-6">
<Select label="Category" class="tw-mb-6" placeholder="---" :options="[]" is-required single />
<Select label="Subcategory" class="tw-mb-6" placeholder="---" :options="[]" is-required single />
</div>
<div class="tw-flex tw-flex-col md:tw-flex-row md:tw-gap-6">
<Field class="tw-flex-1" label="Unit of measure" fieldset is-required>
<div class="tw-grid md:tw-grid-cols-2 md:tw-gap-6">
<Select class="tw-mb-6" placeholder="0" display-by="label" :options="[]" hide-search single />
<Select class="tw-mb-6" placeholder="Measure" hide-search single />
</div>
</Field>
<Field class="tw-mb-6" hint-text="Per individual unit" label="Grams" is-required>
<template #default="{ fieldId }">
<Input :id="fieldId" placeholder="0" type="number" disabled />
</template>
</Field>
</div>
<Select
label="Classification"
class="tw-mb-6"
placeholder="---"
display-by="displayValue"
track-by="value"
:options="[]"
hide-search
is-required
single
/>
<RadioGroup
class="tw-mb-6 [&>div]:tw-gap-0"
label="How do you want to manage your inventory?"
name="inventory-management"
is-required
full-width
>
<Field class="tw-w-full tw-rounded-t-[4px] tw-border tw-border-b-0 tw-border-ice-500 tw-p-3">
<template #hint>
<span class="tw-ml-8">Each variety has its own inventory tracked separately</span>
</template>
<RadioNew id="managed" class="tw-text-ice-900" label="Managed" value="managed" />
</Field>
<Field class="tw-w-full tw-border tw-border-b-0 tw-border-ice-500 tw-p-3">
<template #hint>
<span class="tw-ml-8">All varieties pull from the same inventory quantity</span>
</template>
<RadioNew id="inherited" class="tw-text-ice-900" label="Inherited" value="inherited" />
</Field>
<Field class="tw-w-full tw-rounded-b-[4px] tw-border tw-border-ice-500 tw-p-3">
<template #hint>
<span class="tw-ml-8">Products have no limit and inventory is not tracked</span>
</template>
<RadioNew id="unlimited" class="tw-text-ice-900" label="Unlimited" value="unlimited" />
</Field>
</RadioGroup>
<div class="tw-grid md:tw-grid-cols-2 md:tw-gap-6">
<Switch v-model:checked="formState.showInventoryToBuyers" class="tw-mb-6" label="Show inventory to buyers" />
<Switch
v-model:checked="formState.allowOrderInFractionalQuantities"
class="tw-mb-6"
label="Allow order in fractional quantities"
/>
</div>
<Field class="tw-flex-1" label="Order threshold" fieldset>
<div class="tw-grid tw-items-end md:tw-grid-cols-2 md:tw-gap-6">
<Input class="tw-mb-6" placeholder="0.00" type="number" :hint-text="`Min order qty`" />
<Input class="tw-mb-6" placeholder="0.00" type="number" :hint-text="`Max order qty`" />
</div>
</Field>
<div class="tw-flex tw-flex-col md:tw-flex-row md:tw-gap-6">
<Input label="Increasing qty" hint-text="in units" class="tw-mb-6" placeholder="0" type="number" />
<Select
class="tw-mb-6 tw-flex-1"
label="Action taken"
display-by="displayValue"
track-by="value"
placeholder="---"
:options="[]"
hide-search
single
/>
</div>
</form>
<template #footer>
<div class="tw-flex tw-flex-col tw-gap-3 md:tw-flex-row md:tw-justify-end">
<Button class="md:tw-w-[105px] md:tw-min-w-auto" secondary @click="emit('close')">Cancel</Button>
<Button @click="emit('close')">Add Template</Button>
</div>
</template>
</Modal>
</template>
Drawer
Modal
can be used to create a "drawer", which is a sidebar that slides in & out from one side of the screen. Use the position
prop to enable the drawer pattern.
<script setup lang="ts">
import Modal from '../../../src/components/Modal/Modal.vue';
</script>
<template>
<Modal title="Your Drawer Title" position="right" size="narrow">
<h3 class="tw-mb-5">This is a modal drawer that opens from the right on large screens.</h3>
<p class="tw-mb-5">The HTML killer app is down, transpile the atomic browser so we can inject the ELF pattern!</p>
<p class="tw-mb-5">I'll clear the reactive RPC app, that should flexbox the AI platform!</p>
<p class="tw-mb-5">I'll integrate the documentation-driven S3 compiler, that should transaction the TOML kernel!</p>
<p class="tw-mb-5">Try to inject the IRC budget, maybe it will compile the domain-specific instance!</p>
<p class="tw-mb-5">
So duck-typing the webpack won't do anything, we need to transact the fullstack SOAP engineer!
</p>
</Modal>
</template>
Promo modals
Combining the featuredContent
slot, and the hideHeader
prop, you can create promotional modals. The close button will still appear in the top right.
<script setup lang="ts">
import Button from '../../../src/components/Button/Button.vue';
import Modal from '../../../src/components/Modal/Modal.vue';
const emit = defineEmits<{
(e: 'dismiss'): void;
}>();
function onSave() {
alert('Saved!');
emit('dismiss');
}
</script>
<template>
<Modal hide-header @dismiss="emit('dismiss')">
<template #featured-content>
<img src="./assets/promoExample.svg" class="tw-block tw-w-full" />
</template>
<h3 class="tw-mb-5">This is a promo modal.</h3>
<p>
Concurrency meta-programming composition configuration whiteboard cowboy coding looks good to me big O perf
matters. Lazy directed acyclic graph streams branch containerized browser AWS CSS-in-JS yarn. Key-value reflog
hardcoded mock idiosyncratic contexts model ship it bike-shedding npm browser. UI clean code views cache homebrew
scale CSS-in-JS functional programming JSX. Hacker News tabs vs spaces model view-model Babel parent var sudo
static.
</p>
<template #actions>
<Button secondary @click="emit('dismiss')">Cancel</Button>
<Button @click="onSave">Save</Button>
</template>
</Modal>
</template>
To support images of with any color background, you can use the closeButtonColorClass
prop to pass in a tailwind text color class.
TIP
You can set your text color's opacity by appending any of our color classes with /{percentage}
. Example: tw-text-white/50
will create color: rgba(255,255,255,0.5)
<script setup lang="ts">
import Button from '../../../src/components/Button/Button.vue';
import Modal from '../../../src/components/Modal/Modal.vue';
const emit = defineEmits<{
(e: 'dismiss'): void;
}>();
function onSave() {
alert('Saved!');
emit('dismiss');
}
</script>
<template>
<Modal hide-header @dismiss="emit('dismiss')">
<template #featured-content>
<img src="./assets/promoExample.svg" class="tw-block tw-w-full" />
</template>
<h3 class="tw-mb-5">This is a promo modal.</h3>
<p>
Concurrency meta-programming composition configuration whiteboard cowboy coding looks good to me big O perf
matters. Lazy directed acyclic graph streams branch containerized browser AWS CSS-in-JS yarn. Key-value reflog
hardcoded mock idiosyncratic contexts model ship it bike-shedding npm browser. UI clean code views cache homebrew
scale CSS-in-JS functional programming JSX. Hacker News tabs vs spaces model view-model Babel parent var sudo
static.
</p>
<template #actions>
<Button secondary @click="emit('dismiss')">Cancel</Button>
<Button @click="onSave">Save</Button>
</template>
</Modal>
</template>
Hides the Help widget
The Modal component will auto-hide the Zendesk "help widget launcher" (if it exists on the page) when a modal opens and un-hides the widget when the modal closes. Without this behavior, the help widget launcher can block the action buttons when the modal has a position of "right":
API
See the documentation below for a complete reference to all the props and classes available to the components mentioned here.