Drawer
Side-anchored panel that slides in from screen edges.
TailwindCSS
Alpine.js
Account
Make changes to your account here. Click save when you are done.
package showcase
import "github.com/axzilla/templui/components"
templ DrawerDefault() {
@components.Drawer() {
@components.DrawerTrigger(components.DrawerTriggerProps{Class: "mb-4"}) {
@components.Button() {
Open
}
}
@components.DrawerContent(components.DrawerContentProps{
Side: components.DrawerPositionRight,
}) {
@components.DrawerHeader() {
@components.DrawerTitle() {
Account
}
@components.DrawerDescription() {
Make changes to your account here. Click save when you are done.
}
}
@components.Card() {
@components.CardContent() {
<div class="flex flex-col gap-4">
@components.Input(components.InputProps{
Type: components.InputTypeText,
Placeholder: "Name",
ID: "name",
Value: "John Doe",
})
@components.Input(components.InputProps{
Type: components.InputTypeText,
Placeholder: "Username",
ID: "username",
Value: "@johndoe",
})
</div>
}
}
@components.DrawerFooter() {
@components.DrawerClose() {
Cancel
}
@components.Button() {
Save
}
}
}
}
}
package components
import "github.com/axzilla/templui/utils"
type DrawerPosition string
const (
DrawerPositionTop DrawerPosition = "top"
DrawerPositionRight DrawerPosition = "right"
DrawerPositionBottom DrawerPosition = "bottom"
DrawerPositionLeft DrawerPosition = "left"
)
type DrawerProps struct {
ID string
Class string
Attributes templ.Attributes
Side DrawerPosition
}
type DrawerTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerContentProps struct {
ID string
Class string
Attributes templ.Attributes
Side DrawerPosition
}
type DrawerHeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerFooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerTitleProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerDescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerCloseProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Drawer(props ...DrawerProps) {
{{ var p DrawerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<div
id={ p.ID }
x-data="drawer"
@keydown.escape.window="close"
class={ utils.TwMerge("relative", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerTrigger(props ...DrawerTriggerProps) {
{{ var p DrawerTriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
@click="open"
class={ utils.TwMerge("cursor-pointer", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerContent(props ...DrawerContentProps) {
{{ var p DrawerContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
x-show="isOpen"
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-xs"
@click="close"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
></div>
<div
id={ p.ID + "-content" }
x-show="isOpen"
@click.away="close"
class={
utils.TwMerge(
"fixed z-50",
p.Class,
utils.If(p.Side == DrawerPositionRight, "inset-y-0 right-0 w-3/4 md:w-1/2 lg:w-1/3"),
utils.If(p.Side == DrawerPositionLeft, "inset-y-0 left-0 w-3/4 md:w-1/2 lg:w-1/3"),
utils.If(p.Side == DrawerPositionTop, "inset-x-0 top-0 h-auto sm:h-1/2"),
utils.If(p.Side == DrawerPositionBottom, "inset-x-0 bottom-0 h-auto sm:h-1/2"),
),
}
x-transition:enter="transition ease-out duration-300"
x-transition:leave="transition ease-in duration-300"
if p.Side == DrawerPositionLeft {
x-transition:enter-start="opacity-0 transform -translate-x-full"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform -translate-x-full"
}
if p.Side == DrawerPositionRight {
x-transition:enter-start="opacity-0 transform translate-x-full"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform translate-x-full"
}
if p.Side == DrawerPositionTop {
x-transition:enter-start="opacity-0 transform -translate-y-full"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-full"
}
if p.Side == DrawerPositionBottom {
x-transition:enter-start="opacity-0 transform translate-y-full"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform translate-y-full"
}
{ p.Attributes... }
>
<div
class={
utils.TwMerge(
"h-full overflow-y-auto bg-background p-6 shadow-lg",
utils.If(p.Side == DrawerPositionRight, "border-l"),
utils.If(p.Side == DrawerPositionLeft, "border-r"),
utils.If(p.Side == DrawerPositionBottom, "border-t"),
utils.If(p.Side == DrawerPositionTop, "border-b"),
),
}
@click.stop
>
{ children... }
</div>
</div>
}
templ DrawerHeader(props ...DrawerHeaderProps) {
{{ var p DrawerHeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col space-y-1.5 pb-4", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerTitle(props ...DrawerTitleProps) {
{{ var p DrawerTitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<h2
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-lg font-semibold leading-none tracking-tight", p.Class) }
{ p.Attributes... }
>
{ children... }
</h2>
}
templ DrawerDescription(props ...DrawerDescriptionProps) {
{{ var p DrawerDescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<p
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-sm text-muted-foreground", p.Class) }
{ p.Attributes... }
>
{ children... }
</p>
}
templ DrawerFooter(props ...DrawerFooterProps) {
{{ var p DrawerFooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerClose(props ...DrawerCloseProps) {
{{ var p DrawerCloseProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<button
if p.ID != "" {
id={ p.ID }
}
@click="close"
class={
utils.TwMerge(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background",
"transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent",
"hover:text-accent-foreground h-10 px-4 py-2",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</button>
}
templ DrawerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('drawer', () => ({
isOpen: false,
open() {
this.isOpen = true
},
close() {
this.isOpen = false
},
}))
})
</script>
}
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just Drawer
@components.DrawerScript()
2. Use the component:
@components.Drawer(components.DrawerProps{...})
Examples
Positions
Top Drawer
This drawer slides in from the top of the screen.
Right Drawer
This drawer slides in from the right side of the screen.
Bottom Drawer
This drawer slides in from the bottom of the screen.
Left Drawer
This drawer slides in from the left side of the screen.
package showcase
import "github.com/axzilla/templui/components"
templ DrawerPositions() {
<div class="flex flex-wrap gap-2">
@components.Drawer() {
@components.DrawerTrigger() {
@components.Button() {
Top
}
}
@components.DrawerContent(components.DrawerContentProps{
Side: components.DrawerPositionTop,
}) {
@components.DrawerHeader() {
@components.DrawerTitle() {
Top Drawer
}
}
<p>This drawer slides in from the top of the screen.</p>
@components.DrawerFooter() {
@components.DrawerClose() {
Close
}
}
}
}
@components.Drawer() {
@components.DrawerTrigger() {
@components.Button() {
Right
}
}
@components.DrawerContent(components.DrawerContentProps{
Side: components.DrawerPositionRight,
}) {
@components.DrawerHeader() {
@components.DrawerTitle() {
Right Drawer
}
}
<p>This drawer slides in from the right side of the screen.</p>
@components.DrawerFooter() {
@components.DrawerClose() {
Close
}
}
}
}
@components.Drawer() {
@components.DrawerTrigger() {
@components.Button() {
Bottom
}
}
@components.DrawerContent(components.DrawerContentProps{
Side: components.DrawerPositionBottom,
}) {
@components.DrawerHeader() {
@components.DrawerTitle() {
Bottom Drawer
}
}
<p>This drawer slides in from the bottom of the screen.</p>
@components.DrawerFooter() {
@components.DrawerClose() {
Close
}
}
}
}
@components.Drawer() {
@components.DrawerTrigger() {
@components.Button() {
Left
}
}
@components.DrawerContent(components.DrawerContentProps{
Side: components.DrawerPositionLeft,
}) {
@components.DrawerHeader() {
@components.DrawerTitle() {
Left Drawer
}
}
<p>This drawer slides in from the left side of the screen.</p>
@components.DrawerFooter() {
@components.DrawerClose() {
Close
}
}
}
}
</div>
}
package components
import "github.com/axzilla/templui/utils"
type DrawerPosition string
const (
DrawerPositionTop DrawerPosition = "top"
DrawerPositionRight DrawerPosition = "right"
DrawerPositionBottom DrawerPosition = "bottom"
DrawerPositionLeft DrawerPosition = "left"
)
type DrawerProps struct {
ID string
Class string
Attributes templ.Attributes
Side DrawerPosition
}
type DrawerTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerContentProps struct {
ID string
Class string
Attributes templ.Attributes
Side DrawerPosition
}
type DrawerHeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerFooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerTitleProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerDescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DrawerCloseProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Drawer(props ...DrawerProps) {
{{ var p DrawerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<div
id={ p.ID }
x-data="drawer"
@keydown.escape.window="close"
class={ utils.TwMerge("relative", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerTrigger(props ...DrawerTriggerProps) {
{{ var p DrawerTriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
@click="open"
class={ utils.TwMerge("cursor-pointer", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerContent(props ...DrawerContentProps) {
{{ var p DrawerContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
x-show="isOpen"
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-xs"
@click="close"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
></div>
<div
id={ p.ID + "-content" }
x-show="isOpen"
@click.away="close"
class={
utils.TwMerge(
"fixed z-50",
p.Class,
utils.If(p.Side == DrawerPositionRight, "inset-y-0 right-0 w-3/4 md:w-1/2 lg:w-1/3"),
utils.If(p.Side == DrawerPositionLeft, "inset-y-0 left-0 w-3/4 md:w-1/2 lg:w-1/3"),
utils.If(p.Side == DrawerPositionTop, "inset-x-0 top-0 h-auto sm:h-1/2"),
utils.If(p.Side == DrawerPositionBottom, "inset-x-0 bottom-0 h-auto sm:h-1/2"),
),
}
x-transition:enter="transition ease-out duration-300"
x-transition:leave="transition ease-in duration-300"
if p.Side == DrawerPositionLeft {
x-transition:enter-start="opacity-0 transform -translate-x-full"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform -translate-x-full"
}
if p.Side == DrawerPositionRight {
x-transition:enter-start="opacity-0 transform translate-x-full"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform translate-x-full"
}
if p.Side == DrawerPositionTop {
x-transition:enter-start="opacity-0 transform -translate-y-full"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-full"
}
if p.Side == DrawerPositionBottom {
x-transition:enter-start="opacity-0 transform translate-y-full"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform translate-y-full"
}
{ p.Attributes... }
>
<div
class={
utils.TwMerge(
"h-full overflow-y-auto bg-background p-6 shadow-lg",
utils.If(p.Side == DrawerPositionRight, "border-l"),
utils.If(p.Side == DrawerPositionLeft, "border-r"),
utils.If(p.Side == DrawerPositionBottom, "border-t"),
utils.If(p.Side == DrawerPositionTop, "border-b"),
),
}
@click.stop
>
{ children... }
</div>
</div>
}
templ DrawerHeader(props ...DrawerHeaderProps) {
{{ var p DrawerHeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col space-y-1.5 pb-4", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerTitle(props ...DrawerTitleProps) {
{{ var p DrawerTitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<h2
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-lg font-semibold leading-none tracking-tight", p.Class) }
{ p.Attributes... }
>
{ children... }
</h2>
}
templ DrawerDescription(props ...DrawerDescriptionProps) {
{{ var p DrawerDescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<p
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-sm text-muted-foreground", p.Class) }
{ p.Attributes... }
>
{ children... }
</p>
}
templ DrawerFooter(props ...DrawerFooterProps) {
{{ var p DrawerFooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ DrawerClose(props ...DrawerCloseProps) {
{{ var p DrawerCloseProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<button
if p.ID != "" {
id={ p.ID }
}
@click="close"
class={
utils.TwMerge(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background",
"transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent",
"hover:text-accent-foreground h-10 px-4 py-2",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</button>
}
templ DrawerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('drawer', () => ({
isOpen: false,
open() {
this.isOpen = true
},
close() {
this.isOpen = false
},
}))
})
</script>
}
}