shadcn.io is not affiliated with official shadcn/ui
Settings Drawer Form Block for Svelte
A longer settings form in a bottom drawer with a fixed header and footer, soft shadows at the scrolling edges, a count of unsaved changes worked out from the fields and a guard against closing a dirty form.
Installation
Overview
Edit a company in a drawer without losing your place with this drawer form for Svelte with Ark UI. A compact header holds the page title and the real trigger, an outline Button, over a drawing of the company page. The settings are shown open in place on load, docked to the foot of that page, and pressing Company settings opens the real Drawer with the same state. The body holds five fields: name, a website that is checked as you leave it, an industry, a description with a 500 character count that appears in the last 50, and a public listing Switch. The header and footer stay put while the body scrolls between them, with a soft shadow at whichever edge has more to scroll to. The footer says "2 unsaved changes", worked out from the fields, beside Discard changes and Save changes. Closing a dirty form by Escape, the swipe, the scrim or the close button swaps the footer for "Close without saving 2 changes?" with Keep editing and Close anyway. Save checks the fields, closes the drawer and puts the new name on the page behind. It's built from Drawer, Field, Input, Textarea, Switch, Shine Button and Guides, with a CSS entrance only. For settings, profile and record editing that is longer than a dialog.
FAQ
How do I change the fields, the limits and the saved company?
Edit SAVED for the company the page opens with, NAME_MIN, NAME_MAX and DESCRIPTION_MAX for the limits, and INDUSTRIES for the select. problemsOf is the one check for every field and changesOf counts how many differ from SAVED, so a new field is a line in each plus its row in CompanyFields. The drawn page behind reads the saved values, so it follows a save.
How does the unsaved count work, and what does closing do?
changesOf compares each field with what was saved, so editing a field and typing it back makes it clean again. With nothing changed every way out closes at once. With something changed Escape, the swipe, the scrim and the close button each ask first in the footer's own space, so the drawer never changes height, and focus lands on Keep editing; a second Escape means keep editing. Discard changes puts every field back and stays open.
How are the shadows at the edges of the body made?
With CSS only: four gradients on the scrolling body, two of them attached to the content (background-attachment: local) in the drawer's own colour, so they cover the shadows at the very top and the very bottom. A shadow therefore shows only at an edge with more to scroll to, without a scroll listener. The body is a normal scrolling region, so the keyboard and a screen reader scroll it as usual.
Why a bottom drawer, and how does it work on a phone?
A bottom drawer is the same shape on every screen, grows upward with the form and takes the swipe-down gesture phones expect; its form sits in a column no wider than 32rem. On a phone the in-place copy docks to the foot of the page and the footer's two buttons share a row. The real drawer keeps the catalog's behaviour: focus trapped inside, Escape or a swipe to close, focus back on the trigger.
What stays local, and how do I connect it for real?
Nothing is saved: Save changes says "Saved Ottersham Textiles in this demo. Nothing was sent." and the page behind is the block's own state. To make it real, send the values from the submit handler, check them on your server too, and keep the drawer open with the server's message in the status line when it refuses. Replace the typed SAVED with the company your server returns.
How do I connect it to react-hook-form or TanStack Form?
Use the form library's dirty state in place of changesOf. react-hook-form is a React library; use TanStack Form's adapter for your framework, whose field meta tells which fields are dirty. With TanStack Form, form.state.isDirty drives the guard and form.reset() is Discard changes.