Getting Started
Installation
To get started, you can install the package with the following command:
Create CSS File
In order to make the editor match the design of this website (and the whole shadcn/ui theme), I had to add this css file: You should copy this and add it to your project.
Quill CSS Overrides
@import "@vueup/vue-quill/dist/vue-quill.snow.css";
@import "@vueup/vue-quill/dist/vue-quill.bubble.css";
.ql-toolbar {
&.ql-snow {
border-color: var(--color-border);
border-top-left-radius: var(--radius-md);
border-top-right-radius: var(--radius-md);
font-family: var(--font-sans);
}
&.ql-snow {
.ql-stroke {
stroke: var(--color-muted-foreground);
}
.ql-fill {
fill: var(--color-muted-foreground);
}
button {
margin-inline: calc(var(--spacing) * 0.5);
border-radius: var(--radius-sm);
&:hover {
background-color: var(--color-muted);
color: var(--color-foreground);
}
&:hover {
.ql-fill {
fill: var(--color-foreground);
}
.ql-stroke {
stroke: var(--color-foreground);
}
}
.ql-stroke {
stroke: var(--color-muted-foreground);
}
&.ql-active {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-stroke {
stroke: var(--color-primary-foreground);
}
.ql-fill {
fill: var(--color-primary-foreground);
}
}
}
.ql-formats {
svg,
.ql-picker-label,
.ql-picker {
color: var(--color-muted-foreground);
}
button {
margin-inline: calc(var(--spacing) * 0.5);
border-radius: var(--radius-sm);
&:hover {
background-color: var(--color-muted);
color: var(--color-foreground);
}
&:hover {
.ql-fill {
fill: var(--color-foreground);
}
.ql-stroke {
stroke: var(--color-foreground);
}
}
.ql-fill {
fill: var(--color-muted-foreground);
}
.ql-stroke {
stroke: var(--color-muted-foreground);
}
&.ql-active {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-fill {
fill: var(--color-primary-foreground);
}
.ql-stroke {
stroke: var(--color-primary-foreground);
}
}
}
.ql-picker {
border-radius: var(--radius-sm);
.ql-picker-options {
margin-top: calc(var(--spacing) * 1);
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-card);
padding: calc(var(--spacing) * 1);
.ql-picker-item {
border-radius: var(--radius-sm);
&:hover {
background-color: var(--color-muted);
color: var(--color-foreground);
}
&.ql-selected {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
}
}
}
}
.ql-align,
.ql-color-picker {
&:hover {
.ql-fill {
fill: var(--color-foreground);
}
.ql-stroke {
stroke: var(--color-foreground);
}
}
.ql-picker-options {
margin-top: calc(var(--spacing) * 1);
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-card);
padding: calc(var(--spacing) * 1);
.ql-picker-item {
&.ql-selected {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-stroke {
stroke: var(--color-primary-foreground);
}
}
}
}
}
.ql-picker-label {
&:hover {
border-radius: var(--radius-sm);
background-color: var(--color-muted);
color: var(--color-foreground);
}
&.ql-active {
border-radius: var(--radius-sm);
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-stroke {
stroke: var(--color-primary-foreground);
}
}
}
.ql-stroke {
stroke: var(--color-muted-foreground);
}
.ql-expanded {
.ql-picker-label {
border-color: var(--color-border);
border-radius: var(--radius-sm);
}
}
}
}
}
.ql-container {
min-height: 150px;
background-color: transparent;
font-family: var(--font-sans);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
&:focus-within {
border-color: var(--color-ring) !important;
box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-ring) 50%, transparent);
}
&:is(.dark *) {
background-color: color-mix(in oklab, var(--color-input) 30%, transparent);
}
&.ql-snow {
a {
color: var(--color-sky-500);
&:hover {
color: var(--color-sky-500);
}
}
border-color: var(--color-border);
border-bottom-right-radius: var(--radius-md);
border-bottom-left-radius: var(--radius-md);
}
.ql-editor {
min-height: 150px;
.ql-font-monospace {
font-family: var(--font-mono);
}
&.ql-blank {
&:before {
color: var(--color-muted-foreground);
font-style: normal;
}
}
}
.ql-tooltip {
z-index: 9999;
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-card);
padding-inline: calc(var(--spacing) * 4);
padding-block: calc(var(--spacing) * 2);
color: var(--color-card-foreground);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
box-shadow: var(--shadow);
&::before {
cursor: pointer;
font-weight: var(--font-weight-medium);
}
input[type="text"] {
height: calc(var(--spacing) * 8);
width: 200px;
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: color-mix(in oklab, var(--color-muted) 30%, transparent);
padding: calc(var(--spacing) * 2);
color: var(--color-foreground);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
&:focus {
outline: none;
box-shadow: 0 0 0 1px var(--color-ring);
}
}
.ql-preview {
font-size: var(--text-sm);
line-height: 26px;
text-decoration-line: underline;
text-underline-offset: 2px;
}
.ql-remove {
color: var(--color-destructive);
&:hover {
color: var(--color-destructive);
}
}
}
}
.ql-container.ql-bubble {
border-style: solid;
border-width: 1px;
border-radius: var(--radius-md);
.ql-tooltip {
z-index: 9999;
border-style: solid;
border-width: 1px;
border-color: var(--color-border);
border-radius: var(--radius-lg);
background-color: var(--color-card);
padding-inline: calc(var(--spacing) * 4);
padding-block: calc(var(--spacing) * 2);
color: var(--color-card-foreground);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
box-shadow: var(--shadow);
&::before {
cursor: pointer;
font-weight: var(--font-weight-medium);
}
input[type="text"] {
height: calc(var(--spacing) * 8);
width: 200px;
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: color-mix(in oklab, var(--color-muted) 30%, transparent);
padding: calc(var(--spacing) * 2);
color: var(--color-foreground);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
&:focus {
outline: none;
box-shadow: 0 0 0 1px var(--color-ring);
}
}
.ql-preview {
font-size: var(--text-sm);
line-height: 26px;
text-decoration-line: underline;
text-underline-offset: 2px;
}
.ql-remove {
color: var(--color-destructive);
&:hover {
color: var(--color-destructive);
}
}
&:not(.ql-flip) .ql-tooltip-arrow {
border-bottom-color: var(--color-border);
}
.ql-toolbar {
.ql-stroke {
stroke: var(--color-muted-foreground);
}
.ql-fill {
fill: var(--color-muted-foreground);
}
button {
margin-inline: calc(var(--spacing) * 0.5);
border-radius: var(--radius-sm);
&:hover {
background-color: var(--color-muted);
color: var(--color-foreground);
}
&:hover {
.ql-fill {
fill: var(--color-foreground);
}
.ql-stroke {
stroke: var(--color-foreground);
}
}
.ql-stroke {
stroke: var(--color-muted-foreground);
}
&.ql-active {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-stroke {
stroke: var(--color-primary-foreground);
}
.ql-fill {
fill: var(--color-primary-foreground);
}
}
}
.ql-formats {
svg,
.ql-picker-label,
.ql-picker {
color: var(--color-muted-foreground);
}
button {
margin-inline: calc(var(--spacing) * 0.5);
border-radius: var(--radius-sm);
&:hover {
background-color: var(--color-muted);
color: var(--color-foreground);
}
&:hover {
.ql-fill {
fill: var(--color-foreground);
}
.ql-stroke {
stroke: var(--color-foreground);
}
}
.ql-fill {
fill: var(--color-muted-foreground);
}
.ql-stroke {
stroke: var(--color-muted-foreground);
}
&.ql-active {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-fill {
fill: var(--color-primary-foreground);
}
.ql-stroke {
stroke: var(--color-primary-foreground);
}
}
}
.ql-picker {
border-radius: var(--radius-sm);
.ql-picker-options {
margin-top: calc(var(--spacing) * 1);
border-style: solid;
border-width: 1px;
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-card);
padding: calc(var(--spacing) * 1);
.ql-picker-item {
border-radius: var(--radius-sm);
padding-inline: calc(var(--spacing) * 1);
&:hover {
background-color: var(--color-muted);
color: var(--color-foreground);
}
&.ql-selected {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
}
}
}
}
.ql-align,
.ql-color-picker {
&:hover {
.ql-fill {
fill: var(--color-foreground);
}
.ql-stroke {
stroke: var(--color-foreground);
}
}
.ql-picker-options {
margin-top: calc(var(--spacing) * 1);
border-style: solid;
border-width: 1px;
border-color: var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-card);
padding: calc(var(--spacing) * 1);
.ql-picker-item {
&.ql-selected {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-stroke {
stroke: var(--color-primary-foreground);
}
}
}
}
}
.ql-picker-label {
&:hover {
border-radius: var(--radius-sm);
background-color: var(--color-muted);
color: var(--color-foreground);
}
&.ql-active {
border-radius: var(--radius-sm);
background-color: var(--color-primary);
color: var(--color-primary-foreground);
.ql-stroke {
stroke: var(--color-primary-foreground);
}
}
}
.ql-stroke {
stroke: var(--color-muted-foreground);
}
.ql-expanded {
.ql-picker-label {
border-color: var(--color-border);
border-radius: var(--radius-sm);
}
}
}
}
}
}
Usage
Basic
Here is a basic example of how to use the Quill component. We are using a technique called Slot Forwarding so that if the developer wants to create a component and pass through the toolbar slot, they can do so.
Toolbar
We can add our custom toolbar configuration by using the toolbar prop.
Slot - Toolbar
Another way of customizing the toolbar is by using the toolbar slot. This way, we can create a custom toolbar with our own components.
Bubble Theme
We can pass the bubble value to the theme prop to use the snow theme.
You have to select something in the editor to see the toolbar.
Module
We can pass an object or an array of objects to the module prop to use any Quill module.
Something like this:
<script lang="ts" setup>
import { QuillEditor } from "@vueup/vue-quill";
type SingleModule = {
name: string;
module: any;
options?: any;
};
type ModuleObject = SingleModule | SingleModule[];
const modules = ref<ModuleObject | null>(null);
onMounted(async () => {
const BlotFormatter = (await import("quill-blot-formatter")).default;
modules.value = {
name: "blotFormatter",
module: BlotFormatter,
options: {
/* options */
},
};
});
</script>
Upload an image to see the module in action.