Getting Started
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.