Drawer
A drawer is a draggable dialog that is attached to any side of the viewport. It uses the Dialog primitive under the hood and adds dragging logic on top of it.
import Drawer from '@corvu/drawer'
import type { VoidComponent } from 'solid-js'
const DrawerExample: VoidComponent = () => {
return (
<Drawer breakPoints={[0.75]}>
{(props) => (
<>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 flex h-full max-h-125 flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
I'm a drawer!
</Drawer.Label>
<Drawer.Description class="mt-1 text-center">
Drag down to close me.
</Drawer.Description>
<p class="absolute inset-x-0 -bottom-5 z-10 text-center">
🐸 You found froggy!
</p>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
import './index.css'
import Drawer from '@corvu/drawer'
import type { VoidComponent } from 'solid-js'
const DrawerExample: VoidComponent = () => {
return (
<Drawer breakPoints={[0.75]}>
{(props) => (
<>
<Drawer.Trigger>Open Drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content>
<div class="notch" />
<Drawer.Label>I'm a drawer!</Drawer.Label>
<Drawer.Description>Drag down to close me.</Drawer.Description>
<p class="hidden_frog">🐸 You found froggy!</p>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerExample
[data-corvu-drawer-trigger] {
border-width: 0;
cursor: pointer;
margin-top: auto;
margin-bottom: auto;
border-radius: 0.5rem;
background-color: hsl(249, 87%, 94%);
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 100ms;
animation-duration: 100ms;
}
[data-corvu-drawer-trigger]:hover {
background-color: hsl(251, 86%, 89%);
}
[data-corvu-drawer-trigger]:active {
transform: translate(0px, 0.125rem);
}
[data-corvu-drawer-overlay] {
position: fixed;
inset: 0px;
z-index: 40;
}
[data-corvu-drawer-overlay][data-transitioning] {
transition-property: background-color;
transition-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
transition-duration: 500ms;
}
[data-corvu-drawer-content] {
box-sizing: border-box;
border-width: 0;
border-style: solid;
position: fixed;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 50;
display: flex;
height: 100%;
max-height: 500px;
flex-direction: column;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
border-top-width: 4px;
border-color: hsl(258, 79%, 74%);
background-color: hsl(249, 87%, 94%);
padding-top: 0.75rem;
}
[data-corvu-drawer-content]::after {
position: absolute;
left: 0px;
right: 0px;
top: 100%;
height: 50%;
background-color: inherit;
content: '';
}
[data-corvu-drawer-content][data-transitioning] {
transition-property: transform;
transition-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
transition-duration: 500ms;
}
@media (min-width: 768px) {
[data-corvu-drawer-content] {
-webkit-user-select: none;
user-select: none;
}
}
.notch {
height: 0.25rem;
width: 2.5rem;
align-self: center;
border-radius: 9999px;
background-color: hsl(258, 79%, 74%);
}
[data-corvu-drawer-label] {
margin-top: 0.5rem;
margin-bottom: 0px;
text-align: center;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
}
[data-corvu-drawer-description] {
margin-top: 0.25rem;
text-align: center;
}
.hidden_frog {
position: absolute;
left: 0px;
right: 0px;
bottom: -1.25rem;
z-index: 10;
text-align: center;
margin: 0px;
}
Features Section titled Features
- Attach to any side (top, right, bottom, left)
- Works with CSS transitions
- Custom snap- and breakpoints
- Handles scrollable content
- Customizable damping and velocity settings
Installation Section titled Installation
npm install @corvu/drawer
The drawer is also included in the main corvu
package under corvu/drawer
.
Usage Section titled Usage
import Drawer from '@corvu/drawer' // 'corvu/drawer'
// or
// import { Root, Trigger, ... } from '@corvu/drawer'
Anatomy Section titled Anatomy
<Drawer>
<Drawer.Trigger />
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.Close />
<Drawer.Label />
<Drawer.Description />
</Drawer.Content>
</Drawer.Portal>
</Drawer>
Every component besides the Root
and Content
components are just re-exports from the Dialog primitive. You can find more information about them in the Dialog documentation.
Animating the drawer Section titled Animating the drawer
The drawer content gets the data-transitioning
data attribute applied when it is transitioning. Transitioning means that the drawer is either opening, closing or moving to a snap point after the user stops dragging.
Animation is done by applying CSS transition properties when the drawer is in this transitioning
state. You can use any transition timing function, duration and delay you want. The drawer will automatically apply the data-transitioning
attribute to the Content
component for the duration of the transition and remove it when it is done.
A plain CSS example would look like this:
[data-corvu-dialog-content][data-transitioning] {
transition-property: transform;
transition-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
transition-duration: 500ms;
}
With tailwind you can target data attributes like this:
<Drawer.Content
class="
data-transitioning:transition-transform
data-transitioning:duration-500
"
>
</Drawer.Content>
Additionally, there are different data attributes applied depending on the current state of the drawer:
data-opening
: Present when the drawer is in the open transition.data-closing
: Present when the drawer is in the close transition.data-snapping
: Present when the drawer is transitioning after the user stops dragging.
This allows you to apply different transitions based on the transition state of the drawer.
You can also use the openPercentage
property returned by the Drawer.useContext()
or the root children callback and use it to animate the background for example:
<Drawer>
{(props) => (
<Drawer.Overlay
class="fixed inset-0 z-40 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
)}
</Drawer>
Examples Section titled Examples
Snap- and breakpoints Section titled Snap- and breakpoints
The drawer root accepts a snapPoints
array to customize the points to snap to. Valid values are either percentages or pixel values. The drawer will snap to the closest snap point when the user stops dragging (With the velocityFunction
in mind. See the API Reference for more information)
<Drawer snapPoints={[0, 0.5, 1]}>
...
</Drawer>
Here, the drawer additionally snaps to 50% of its height.
Snappoints example
import Drawer from '@corvu/drawer'
import type { VoidComponent } from 'solid-js'
const DrawerSnapPointExample: VoidComponent = () => {
return (
<Drawer snapPoints={[0, 0.5, 1]} allowSkippingSnapPoints={false}>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
Snappoints example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 flex h-full max-h-125 flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
I'm a drawer!
</Drawer.Label>
<Drawer.Description class="mt-1 text-center">
I will snap at <span class="font-bold">50%</span> of my height.{' '}
<br /> My current height is:{' '}
<span class="font-bold">
{(props.openPercentage * 100).toFixed(2)}%
</span>
</Drawer.Description>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerSnapPointExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Per default, the drawer will snap to the closest snap point. You can change this behavior by providing custom breakPoints
to the root component:
<Drawer breakPoints={[0.75]}>
...
</Drawer>
Here, the drawer will close when the user drags below 75% of the drawer’s height.
Breakpoints example
import Drawer from '@corvu/drawer'
import type { VoidComponent } from 'solid-js'
const DrawerBreakPointExample: VoidComponent = () => {
return (
<Drawer breakPoints={[0.75]} velocityFunction={() => 1}>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
Breakpoints example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 flex h-full max-h-125 flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
I'm a drawer!
</Drawer.Label>
<Drawer.Description class="mt-1 text-center">
I will close when I'm under <span class="font-bold">75%</span>{' '}
of my height. <br /> My current height is:{' '}
<span class="font-bold">
{(props.openPercentage * 100).toFixed(2)}%
</span>
</Drawer.Description>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerBreakPointExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Scrollable elements Section titled Scrollable elements
Scrollable elements work out of the box. The drawer will check if the user is dragging on a scrollable element and handle dragging properly.
Scrollable example
import { For, type VoidComponent } from 'solid-js'
import clsx from 'clsx'
import Drawer from '@corvu/drawer'
const DrawerScrollableExample: VoidComponent = () => {
return (
<Drawer>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
Scrollable example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 flex h-full max-h-125 flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div class="h-1 w-10 shrink-0 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
Drawer with a scrollable element
</Drawer.Label>
<div class="mt-3 grow divide-y divide-corvu-400 overflow-y-auto">
<For each={new Array(20)}>
{(_, idx) => (
<p
class={clsx('py-2 text-center font-bold', {
'bg-corvu-200': idx() % 2 === 0,
})}
>
List item {idx() + 1}
</p>
)}
</For>
</div>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerScrollableExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Transitioning the content height Section titled Transitioning the content height
corvu’s drawer can handle height: auto
and transition between heights when the content changes. This is possible by using a ResizeObserver
to detect height changes and apply a fixed height for the time of the transition.
This is disabled by default and you need to set the transitionResize
property on the root component to enable it. Also, remember to set transition-property: height
on the drawer content ;).
Transition resize example
import { createSignal, type VoidComponent } from 'solid-js'
import Drawer from '@corvu/drawer'
const heightSequence = [500, 400, 300, 400]
const DrawerTransitionResizeExample: VoidComponent = () => {
const [currentHeight, setCurrentHeight] = createSignal(400)
return (
<Drawer transitionResize>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
Transition resize example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="group fixed inset-x-0 bottom-0 z-50 flex flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-[transform,height] data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
Dynamic content height example
</Drawer.Label>
<Drawer.Description class="mt-1 text-center">
I will transition between height changes
</Drawer.Description>
<button
onClick={() => {
const nextHeight = heightSequence.shift()
if (nextHeight !== undefined) {
setCurrentHeight(nextHeight)
heightSequence.push(nextHeight)
}
}}
class="mx-auto mt-2 rounded-md bg-corvu-100 px-3 py-2 font-medium"
>
Resize content
</button>
<div
class="mx-5 mb-5 mt-3 flex items-center justify-center rounded-lg border-2 border-corvu-400 group-data-resizing:grow"
style={{
height: `${currentHeight()}px`,
}}
>
<p class="text-2xl">🌟</p>
</div>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerTransitionResizeExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Dynamic content height Section titled Dynamic content height
You might want the height of the drawer to adjust so its content is always fully visible no matter what snap point the user is on.
A common example would be a comment section which can be expanded:
Comments example
import { For, type VoidComponent } from 'solid-js'
import Drawer from '@corvu/drawer'
const DrawerCommentsExample: VoidComponent = () => {
return (
<Drawer snapPoints={[0, 0.7, 1]} allowSkippingSnapPoints={false}>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
Comments example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="group fixed inset-x-0 bottom-0 z-50 h-full max-h-[95%] rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div
class="flex flex-col group-data-transitioning:transition-[height] group-data-transitioning:duration-500 group-data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
height: `${
props.openPercentage < 0.7 ? 70 : props.openPercentage * 100
}%`,
}}
>
<div class="h-1 w-10 shrink-0 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="border-b-2 border-corvu-400 py-2 text-center text-xl font-bold">
Comments
</Drawer.Label>
<div class="grow divide-y divide-white/10 overflow-y-auto">
<For each={new Array(20)}>
{() => (
<div class="flex items-center space-x-2 p-2">
<div class="size-8 rounded-full bg-corvu-200" />
<div>
<p class="font-bold">Username</p>
<p class="text-sm">This is a comment</p>
</div>
</div>
)}
</For>
</div>
<div class="z-10 border-t-2 border-corvu-400 p-2 text-center text-lg font-bold">
Comments Footer
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerCommentsExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
To achieve this, you can use the props.openPercentage
property and change the height of the drawer accordingly. Make sure to create a wrapper element inside Drawer.Content
and not apply the height directly to the Content
component, otherwise the drawer will not be able to calculate the correct height.
style={{
height: `${
props.openPercentage < 0.7
? 70
: props.openPercentage * 100
}%`,
}}
The height will adjust to match the drawer’s height until it’s under 70%, which is the last snap point. Don’t forget to apply the same transition function as you defined for your drawer transform and you’re good to go!
Disable dragging on certain elements Section titled Disable dragging on certain elements
You can disable drag on an element by giving it the data-corvu-no-drag
attribute. corvu will ignore any drag events on elements with this attribute.
No drag example
import Drawer from '@corvu/drawer'
import type { VoidComponent } from 'solid-js'
const DrawerNoDragExample: VoidComponent = () => {
return (
<Drawer>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
No drag example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 flex h-full max-h-125 flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:select-none">
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
I'm a drawer!
</Drawer.Label>
<div
class="m-6 flex h-50 items-center justify-center rounded-lg border-2 border-corvu-400 text-center text-lg"
data-corvu-no-drag
>
Dragging in here does nothing.
<br />
Have a cookie 🍪
</div>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
export default DrawerNoDragExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Responsive drawer/dialog Section titled Responsive drawer/dialog
You can combine the drawer and dialog primitives to create a responsive component that renders a drawer on mobile and a dialog on desktop. The createMediaQuery
primitive accepts a media query and returns a boolean signal indicating whether the query currently matches or not.
import createMediaQuery from '@examples/primitives/drawer/responsive/createMediaQuery'
import Dialog from '@corvu/dialog'
import Drawer from '@corvu/drawer'
import { Show } from 'solid-js'
import type { VoidComponent } from 'solid-js'
const DrawerResponsiveExample: VoidComponent = () => {
const isDesktop = createMediaQuery('(min-width: 768px)')
const MobileDrawer = () => (
<Drawer breakPoints={[0.75]}>
{(props) => (
<>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Edit Profile
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 h-full max-h-125 flex-col rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] flex items-center md:select-none">
<div class="max-w-80 w-full flex-col flex">
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="mt-2 text-center text-xl font-bold">
Edit Profile
</Drawer.Label>
<Drawer.Description class="mt-1 text-center">
Make changes to your profile here
</Drawer.Description>
<p class="mt-3 text-sm">Username</p>
<input
placeholder="corvu"
class="mt-1 w-full rounded-sm border-2 border-corvu-400 bg-corvu-100 focus:outline-hidden"
/>
<div class="mt-3 flex justify-between">
<Drawer.Close class="rounded-md bg-corvu-200 px-3 py-2">
Cancel
</Drawer.Close>
<Drawer.Close class="rounded-md bg-corvu-300 px-3 py-2 font-bold">
Submit
</Drawer.Close>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
return (
<Show when={isDesktop()} fallback={<MobileDrawer />}>
<Dialog>
<Dialog.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5 slide-in-from-top-2">
Edit Profile
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 z-50 bg-black/50 data-open:animate-in data-open:fade-in-0% data-closed:animate-out data-closed:fade-out-0%" />
<Dialog.Content class="fixed left-1/2 top-1/2 z-50 min-w-80 -translate-x-1/2 -translate-y-1/2 rounded-lg border-2 border-corvu-400 bg-corvu-100 px-6 py-5 data-open:animate-in data-open:fade-in-0% data-open:zoom-in-95% data-open:slide-in-from-top-10% data-closed:animate-out data-closed:fade-out-0% data-closed:zoom-out-95% data-closed:slide-out-to-top-10%">
<Dialog.Label class="text-xl font-bold">Edit Profile</Dialog.Label>
<Dialog.Description class="mt-1">
Make changes to your profile here
</Dialog.Description>
<p class="mt-3 text-sm">Username</p>
<input
placeholder="corvu"
class="mt-1 w-full rounded-sm border-2 border-corvu-400 bg-corvu-100 focus:outline-hidden"
/>
<div class="mt-3 flex justify-between">
<Dialog.Close class="rounded-md bg-corvu-200 px-3 py-2">
Cancel
</Dialog.Close>
<Dialog.Close class="rounded-md bg-corvu-300 px-3 py-2 font-bold">
Submit
</Dialog.Close>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
</Show>
)
}
export default DrawerResponsiveExample
import { createSignal, onCleanup, onMount } from 'solid-js'
const createMediaQuery = (query: string) => {
const [matches, setMatches] = createSignal(false)
onMount(() => {
const onChange = (event: MediaQueryListEvent) => {
setMatches(event.matches)
}
const result = matchMedia(query)
result.addEventListener('change', onChange)
setMatches(result.matches)
onCleanup(() => {
result.removeEventListener('change', onChange)
})
})
return matches
}
export default createMediaQuery
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Persistent content Section titled Persistent content
To persist the content of the drawer even when it’s unmounted, you can use corvu’s createPersistent
utility. It will cache the content when the drawer is opened for the first time and preserve its state and HTML elements.
const PersistedDrawerContent = () => {
const persistedContent = createPersistent(() => {
return <input />
})
return (
<Drawer>
<Drawer.Trigger>
Open
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Content>
{persistedContent()}
</Drawer.Content>
</Drawer.Portal>
</Drawer>
)
}
Give it a try! 🐦⬛
Persistent content example
import createPersistent from 'solid-persistent'
import Drawer from '@corvu/drawer'
import type { VoidComponent } from 'solid-js'
const DrawerPersistentExample: VoidComponent = () => {
const persistedContent = createPersistent(DrawerContent)
return (
<Drawer>
{(props) => (
<>
<div class="my-auto flex flex-col items-center">
<p class="mb-2 rounded-lg bg-corvu-300 px-2 py-1 font-bold">
Persistent content example
</p>
<Drawer.Trigger class="my-auto rounded-lg bg-corvu-100 px-4 py-3 text-lg font-medium transition-all duration-100 hover:bg-corvu-200 active:translate-y-0.5">
Open Drawer
</Drawer.Trigger>
</div>
<Drawer.Portal>
<Drawer.Overlay
class="fixed inset-0 z-50 data-transitioning:transition-colors data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{
'background-color': `rgb(0 0 0 / ${
0.5 * props.openPercentage
})`,
}}
/>
<Drawer.Content class="fixed inset-x-0 bottom-0 z-50 flex h-full max-h-4/5 flex-col items-center rounded-t-lg border-t-4 border-corvu-400 bg-corvu-100 px-5 pt-3 after:absolute after:inset-x-0 after:top-[calc(100%-1px)] after:h-1/2 after:bg-inherit data-transitioning:transition-transform data-transitioning:duration-500 data-transitioning:ease-[cubic-bezier(0.32,0.72,0,1)] md:max-h-[500px] md:select-none">
{persistedContent()}
</Drawer.Content>
</Drawer.Portal>
</>
)}
</Drawer>
)
}
const DrawerContent = () => (
<>
<div class="h-1 w-10 self-center rounded-full bg-corvu-400" />
<Drawer.Label class="text-lg font-bold">
Persistent drawer content
</Drawer.Label>
<Drawer.Description class="mt-2 text-center">
This drawer will preserve the state inside the content,
<br /> even after it gets unmounted from the DOM.
<br /> Enter something in the input and reopen the drawer.
</Drawer.Description>
<input class="mt-4 rounded-sm border border-corvu-200 bg-corvu-bg px-3 py-2 ring-2 ring-corvu-400 focus-visible:border focus-visible:border-corvu-200 focus-visible:ring-2 focus-visible:ring-corvu-400" />
<p class="mt-3 text-center text-sm">
💡 I'm an uncontrolled input, preserving my state because I never
rerender!
</p>
</>
)
export default DrawerPersistentExample
@import 'tailwindcss';
@theme {
--color-corvu-text: #180f24;
--color-corvu-bg: #f3f1fe;
--color-corvu-100: #e6e2fd;
--color-corvu-200: #d4cbfb;
--color-corvu-300: #bcacf6;
--color-corvu-400: #a888f1;
--animate-expand: expand 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-collapse: collapse 250ms cubic-bezier(0.32,0.72,0,0.75);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-in: enter 150ms ease;
--animate-out: exit 150ms ease;
@keyframes expand {
0% {
height: 0px;
}
100% {
height: var(--corvu-disclosure-content-height);
}
}
@keyframes collapse {
0% {
height: var(--corvu-disclosure-content-height);
}
100% {
height: 0px;
}
}
@keyframes caret-blink {
0%, 70%, 100% {
opacity: 0;
}
20%, 50% {
opacity: 1;
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
}
}
}
@utility fade-in-* {
--tw-enter-opacity: --value(percentage, ratio);
}
@utility fade-out-* {
--tw-exit-opacity: --value(percentage, ratio);
}
@utility zoom-in-* {
--tw-enter-scale: --value(percentage, ratio);
}
@utility zoom-out-* {
--tw-exit-scale: --value(percentage, ratio);
}
@utility slide-in-from-top-* {
--tw-enter-translate-y: calc(--value(percentage) * -1);
--tw-enter-translate-y: calc(--value(ratio) * -100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-in-from-bottom-* {
--tw-enter-translate-y: --value(percentage);
--tw-enter-translate-y: calc(--value(ratio) * 100%);
--tw-enter-translate-y: calc(--value(integer) * var(--spacing));
}
@utility slide-out-to-top-* {
--tw-exit-translate-y: calc(--value(percentage) * -1);
--tw-exit-translate-y: calc(--value(ratio) * -100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing) * -1);
}
@utility slide-out-to-bottom-* {
--tw-exit-translate-y: --value(percentage);
--tw-exit-translate-y: calc(--value(ratio) * 100%);
--tw-exit-translate-y: calc(--value(integer) * var(--spacing));
}
@plugin '@tailwindcss/forms';
Accessibility Section titled Accessibility
Adheres to the Dialog WAI-ARIA design pattern and Alert Dialog WAI-ARIA design pattern.
API reference Section titled API reference
Only components which are specific to the drawer are documented here. For all other components, please refer to the Dialog API reference.
The dialog context is re-exported as Drawer.useDialogContext
and the root children callback also accepts all props of the dialog root callback function, which are documented in the Dialog API reference.
Context wrapper for the drawer. Is required for every drawer you create.
Props
Inherits <Dialog.Root />
Props.
Property | Default | Type/Description |
---|---|---|
snapPoints | [0, 1] | Size[] An array of points to snap to. Can be either percentages of the total drawer height or CSS pixel values. |
breakPoints | - | [Size | null] Optionally override the default breakpoints between snap points. This list has to be the length of snapPoints.length - 1 . Provide null for breakpoints you don't want to override. |
defaultSnapPoint | 1 | Size The point to snap to when the drawer opens. |
activeSnapPoint | - | Size The active snap point. |
onActiveSnapPointChange | - | (activeSnapPoint: Size) => void Callback fired when the active snap point changes. |
side | 'bottom' | Side The side of the viewport the drawer appears. Is used to properly calculate dragging. |
dampFunction | - | (distance: number) => number Function to create a dampened distance if the user tries to drag the drawer away from the last snap point. |
velocityFunction | - | (distance: number, time: number) => number Function to calculate the velocity when the user stop dragging. This velocity modifier is used to calculate the point the drawer will snap to after release. You can disable velocity by always returning 1 |
velocityCacheReset | 200 | number After how many milliseconds the cached distance used for the velocity function should reset. |
allowSkippingSnapPoints | true | boolean Whether the user can skip snap points if the velocity is high enough. |
handleScrollableElements | true | boolean corvu drawers have logic to make dragging and scrolling work together. If you don't want this behavior or if you want to implement something yourself, you can disable it with this property. |
transitionResize | false | boolean Whether the drawer should watch for size changes and apply a fixed width/height for transitions. |
Button that changes the open state of the drawer when clicked.
Props
Inherits <Dialog.Trigger />
Props.
Data attributes
Data attributes present on <Trigger />
components.
Property | Description |
---|---|
data-corvu-drawer-trigger | Present on every drawer trigger element. |
data-open | Present when the drawer is open. |
data-closed | Present when the drawer is closed. |
Portals its children at the end of the body element to ensure that the dialog always rendered on top.
Props
Inherits <Dialog.Portal />
Props.
Component which can be used to create a faded background. Can be animated.
Props
Inherits <Dialog.Overlay />
Props.
Data attributes
Data attributes present on <Overlay />
components.
Property | Description |
---|---|
data-corvu-drawer-overlay | Present on every drawer overlay element. |
data-open | Present when the drawer is open. |
data-closed | Present when the drawer is closed. |
data-transitioning | Present when the drawer is transitioning (opening, closing or snapping). |
data-opening | Present when the drawer is in the open transition. |
data-closing | Present when the drawer is in the close transition. |
data-snapping | Present when the drawer is transitioning after the user stops dragging. |
data-resizing | Present when the drawer is transitioning after the size (width/height) changes. Only present if transitionResize is set to true . |
Content of the drawer. Can be animated.
Props
Inherits <Dialog.Content />
Props.
Data attributes
Data attributes present on <Content />
components.
Property | Description |
---|---|
data-corvu-drawer-content | Present on every drawer content element. |
data-open | Present when the drawer is open. |
data-closed | Present when the drawer is closed. |
data-transitioning | Present when the drawer is transitioning (opening, closing or snapping). |
data-opening | Present when the drawer is in the open transition. |
data-closing | Present when the drawer is in the close transition. |
data-snapping | Present when the drawer is transitioning after the user stops dragging. |
data-resizing | Present when the drawer is transitioning after the size (width/height) changes. Only present if transitionResize is set to true . |
Close button that changes the open state to false when clicked.
Props
Inherits <Dialog.Close />
Props.
Data attributes
Data attributes present on <Close />
components.
Property | Description |
---|---|
data-corvu-drawer-close | Present on every drawer close element. |
Label element to announce the drawer to accessibility tools.
Props
Inherits <Dialog.Label />
Props.
Data attributes
Data attributes present on <Label />
components.
Property | Description |
---|---|
data-corvu-drawer-label | Present on every drawer label element. |
Description element to announce the drawer to accessibility tools.
Props
Inherits <Dialog.Description />
Props.
Data attributes
Data attributes present on <Description />
components.
Property | Description |
---|---|
data-corvu-drawer-description | Present on every drawer description element. |
Context which exposes various properties to interact with the drawer. Optionally provide a contextId to access a keyed context.
Returns
Property | Type/Description |
---|---|
snapPoints | Accessor<Size[]> An array of points to snap to. Can be either percentages of the total drawer height or CSS pixel values. |
breakPoints | Accessor<[Size | null]> Breakpoints between snap points. |
defaultSnapPoint | Accessor<Size> The point to snap to when the drawer opens. |
activeSnapPoint | Accessor<Size> The active snap point. |
setActiveSnapPoint | (snapPoint: Size) => void Change the active snap point. |
side | Accessor<Side> The side of the viewport the drawer appears. Is used to properly calculate dragging. |
isDragging | Accessor<boolean> Whether the drawer is currently being dragged by the user. |
isTransitioning | Accessor<boolean> Whether the drawer is currently transitioning to a snap point after the user stopped dragging or the drawer opens/closes. |
transitionState | Accessor<'opening' | 'closing' | 'snapping' | 'resizing' | null> The transition state that the drawer is currently in. |
openPercentage | Accessor<number> How much the drawer is currently open. Can be > 1 depending on your dampFunction . |
translate | Accessor<number> The current translate value applied to the drawer. Is the same for every side. |
velocityCacheReset | Accessor<number> After how many milliseconds the cached distance used for the velocity function should reset. |
allowSkippingSnapPoints | Accessor<boolean> Whether the user can skip snap points if the velocity is high enough. |
handleScrollableElements | Accessor<boolean> Whether the logic to handle dragging on scrollable elements is enabled. |
transitionResize | Accessor<boolean> Whether the drawer watches for size changes and applies a fixed width/height for transitions. |
Inherited from <Dialog.useContext />
.
Props that are passed to the Root component children callback.
Props
Inherits <Dialog.RootChildrenProps />
Props.
Property | Type/Description |
---|---|
snapPoints | Size[] An array of points to snap to. Can be either percentages of the total drawer height or CSS pixel values. |
breakPoints | [Size | null] Breakpoints between snap points. |
defaultSnapPoint | Size The point to snap to when the drawer opens. |
activeSnapPoint | Size The active snap point. |
setActiveSnapPoint | (snapPoint: Size) => void Set the current active snap point. |
side | Side The side of the viewport the drawer appears. Is used to properly calculate dragging. |
isDragging | boolean Whether the drawer is currently being dragged by the user. |
isTransitioning | boolean Whether the drawer is currently transitioning to a snap point after the user stopped dragging or the drawer opens/closes. |
transitionState | 'opening' | 'closing' | 'snapping' | 'resizing' | null The transition state that the drawer is currently in. |
openPercentage | number How much the drawer is currently open. Can be > 1 depending on your dampFunction . |
translate | number The current translate value applied to the drawer. Is the same for every side. |
velocityCacheReset | number After how many milliseconds the cached distance used for the velocity function should reset. |
allowSkippingSnapPoints | boolean Whether the user can skip snap points if the velocity is high enough. |
handleScrollableElements | boolean Whether the logic to handle dragging on scrollable elements is enabled. |
transitionResize | boolean Whether the drawer watches for size changes and applies a fixed width/height for transitions. |
type Side = 'top' | 'right' | 'bottom' | 'left'
type Size = `${number}px` | number
Developed and designed by Jasmin