# Astro Utils — complete documentation Generated from the same source as the website. Begin with /docs/llms.txt for focused retrieval. # Build with Astro Utils Start here when implementing an Astro Utils feature. These instructions accompany the current documentation source; check your installed package versions before relying on an API. Browser previews simulate server behavior and do not demonstrate a deployed backend. ## Choose the utility - Forms: bound fields, validation, server callbacks, state, sessions, and chunked uploads. - Context: request-local values shared between Astro components. It is not persistent storage. - Express Endpoints: middleware, request parsing, and response helpers for Astro API routes. ## Install and configure Forms Use the complete [Forms setup](/docs/guides/forms/getting-started/index.md). Install `@astro-utils/forms` and `@astrojs/node` using npm, pnpm, Yarn, or Bun. Configure on-demand rendering, the Forms integration, middleware, and one shared `WebForms` layout. The exact setup files are included below. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` For deployment, follow [configuration](/docs/reference/forms/configuration/index.md) and supply a stable environment secret. Do not commit secrets. Sessions are signed, not encrypted; do not treat them as permanent application storage. ## Build the product filter This complete example uses a local catalog and requires no database. Copy the files to their labeled paths. Run your project's `dev` script, open `/products`, and submit budgets of 10, 50, and 100. Expect zero, two, and three results. Negative and empty budgets must fail validation. ### src/pages/products.astro ```astro title="src/pages/products.astro" --- import { BButton, BInput, Bind, BindForm } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; import { products } from '../data/products'; const bind = Bind({ maxPrice: 50 }); let matches = products.filter(product => product.price <= bind.maxPrice); function filterProducts() { matches = products.filter(product => product.price <= bind.maxPrice); } --- Filter products Apply filter

{matches.length} products found

    {matches.map(product =>
  • {product.name} — ${product.price}
  • )}
``` ### src/data/products.ts ```ts title="src/data/products.ts" // A local catalog keeps this example runnable without a database. // Replace the filter in your server callback with your application's query. export const products = [ { name: 'Notebook', price: 12 }, { name: 'Desk lamp', price: 35 }, { name: 'Headphones', price: 79 } ]; ``` ## Build the homepage collection search This example searches an Astro content collection on the server. It does not make a live database query. For database-backed products, replace `getCollection` with your application's database query inside the callback; do not assume an unspecified database client exists. Add the following collection to `src/content.config.ts`, merging it with existing collections. The inline loader makes this sample self-contained; replace it with your actual catalog source as needed. ```ts title="src/content.config.ts" import { defineCollection, z } from 'astro:content'; const products = defineCollection({ loader: async () => [ { id: 'notebook', name: 'Notebook', price: 12 }, { id: 'lamp', name: 'Desk lamp', price: 35 }, { id: 'headphones', name: 'Headphones', price: 79 } ], schema: z.object({ name: z.string(), price: z.number() }) }); export const collections = { products }; ``` ```astro title="src/pages/search.astro" --- import { getCollection } from "astro:content"; import { Bind, BindForm, BInput, BButton } from "@astro-utils/forms/forms.js"; import Layout from "../layouts/Layout.astro"; const bind = Bind({ query: "" }); let products = await getCollection("products"); async function search() { products = products.filter(product => product.name.toLowerCase().includes(bind.name.toLowerCase()) ); } --- Search

{products.length} products found

    {products.map(({ data }) =>
  • {data.name} — ${data.price}
  • )}
``` Run `astro sync` to generate collection types, then your project's `dev` script. Open `/search`. Search “lamp” to see Desk lamp, an unknown name for no results, and clear the field to show the whole collection. ## Execution and state `WebForms` handles the submission. `BindForm` restores bound values, bound controls validate input, and a button invokes its server callback. `whenFormOK` restricts callbacks to valid submissions. Astro then renders the updated result. A request-local variable is not durable state. Use bound state, sessions, or application storage according to the lifetime you need. ## Find the right recipe - [Binding and validation](/docs/guides/forms/data-binding/index.md) - [Tasks](/docs/examples/todo/index.md) - [Preferences](/docs/examples/settings/index.md) - [Upload progress](/docs/examples/upload/index.md) - [Draft service requests](/docs/examples/government-form/index.md) - [Login feedback](/docs/examples/login/index.md): illustrative demo credentials, not production authentication. - [Account fields](/docs/examples/signup/index.md): does not create real database accounts. - [Context](/docs/guides/context/index.md) - [HTTP endpoints](/docs/guides/express-endpoints/index.md) - [All documentation](/docs/llms.txt) ## Check your implementation 1. Verify imports against the relevant API reference; do not invent events or component props. 2. Run `astro check` and your project's build. Test the real server route, not only a documentation preview. 3. Exercise successful submission, invalid input, empty results, and repeated submissions. 4. Confirm which state survives a request or reload. Keep passwords out of persisted view state. 5. For uploads, test interruption and retry; the progress component's `for` value matches the field name. 6. Confirm any external database, authentication, or storage service separately. Static snippet compilation does not certify those integrations. --- # Submit a service request Grouped fields, draft saving, and final validation. Source: https://withastro-utils.github.io/docs/examples/government-form/ Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project. ## Try the interaction Save an incomplete draft, then finish the required fields and submit. The preview confirms each action; it does not persist a draft after reload. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/application.astro ```astro title="src/pages/application.astro" --- import { BButton, BInput, BOption, BSelect, BTextarea, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; type Form = { application: { legalName: string; birthDate: string | Date; service: string; details: string } }; const bind = Bind
({application: Astro.locals.session.applicationDraft}); let status = ''; function saveDraft() { Astro.locals.session.applicationDraft = bind.application; status = 'Draft saved.'; } async function submitApplication() { await sendData(bind.application); status = 'Application submitted.'; Astro.locals.session.applicationDraft = undefined; bind.application = {}; // clear the form } --- Submit a service request
Applicant details
Request Permit application Public record request
Save draft Submit application {status &&

{status}

}
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## How it works In your project, **Save draft** writes a small object to the current session. Reloading the page restores it; submission clears it. This identifies the draft by browser session, not by an authenticated account. For a real service, authenticate the applicant and store drafts durably under their user ID. The demo creates no official request. ## Continue Review [binding and validation](https://withastro-utils.github.io/docs/guides/forms/data-binding/index.md). --- # Examples Explore complete Astro Utils source with interactive previews for accounts, tasks, settings, uploads, and service requests. Source: https://withastro-utils.github.io/docs/examples/ Choose a pattern, inspect the source, then open **Preview** to try it. Previews simulate server behavior locally. - [Create an account](https://withastro-utils.github.io/docs/examples/signup/index.md): Bind account fields, validate them, and run a server action after a valid submission. - [Log in with feedback](https://withastro-utils.github.io/docs/examples/login/index.md): Try success and error feedback with a clearly labeled demo session. - [Manage a task list](https://withastro-utils.github.io/docs/examples/todo/index.md): Add and remove persisted items while the form automatically renders the latest server state. - [Edit profile settings](https://withastro-utils.github.io/docs/examples/settings/index.md): Combine text, select, and textarea controls in one saved preferences form. - [Upload a large file](https://withastro-utils.github.io/docs/examples/upload/index.md): Upload a file in bounded chunks and show progress before processing it on the server. - [Submit a service request](https://withastro-utils.github.io/docs/examples/government-form/index.md): Group an application into sections and save a small draft in the session. - [Filter a product catalog](https://withastro-utils.github.io/docs/examples/products/index.md): Bind a price filter, run server logic, and render matching products. ## Filter products with a server action [Explore the product-filter workspace](https://withastro-utils.github.io/docs/examples/products/index.md): bind a price field, filter a catalog, and render the matching rows in one Astro component. --- # Log in with feedback Success, error feedback, and session updates. Source: https://withastro-utils.github.io/docs/examples/login/ Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project. ## Try the interaction Use **demo@example.com** and **demo-only** for success. Any other credentials show an error. Try an error followed by success to see feedback reset. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/login.astro ```astro title="src/pages/login.astro" --- import { BButton, BInput, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; const bind = Bind({ email: '', password: '' }); let error = ''; let message = ''; function logIn() { if (bind.email !== 'demo@example.com' || bind.password !== 'demo-only') { error = 'Email or password is incorrect.'; } else { Astro.locals.session.demoUserId = 'demo-user'; message = 'Signed in to the demo.'; } } --- Log in with feedback {error &&

{error}

} {message &&

{message}

} Log in
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## How it works These public credentials demonstrate the form flow only. Replace the demo condition with your authentication provider before using this in an application. Passwords are excluded from view state; the callback clears the submitted value. Only the demo user ID is saved in the session. ## Continue Read the [session reference](https://withastro-utils.github.io/docs/reference/forms/session/index.md). --- # Filter a product catalog Bound input, server-side filtering, and updated results in one Astro component. Source: https://withastro-utils.github.io/docs/examples/products/ Query on the server. Render in the same component. This example uses a small local catalog so you can run it without a database; replace the filter callback with your own database query when connecting real products. ## Try the interaction Set a maximum price and apply the filter. Try **10** for no results, **50** for two products, or **100** for the full catalog. Negative and empty prices are rejected. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/products.astro ```astro title="src/pages/products.astro" --- import { BButton, BInput, Bind, BindForm } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; import { products } from '../data/products'; const bind = Bind({ maxPrice: 50 }); let matches = products.filter(product => product.price <= bind.maxPrice); function filterProducts() { matches = products.filter(product => product.price <= bind.maxPrice); } --- Filter products Apply filter

{matches.length} products found

    {matches.map(product =>
  • {product.name} — ${product.price}
  • )}
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ### src/data/products.ts ```ts title="src/data/products.ts" // A local catalog keeps this example runnable without a database. // Replace the filter in your server callback with your application's query. export const products = [ { name: 'Notebook', price: 12 }, { name: 'Desk lamp', price: 35 }, { name: 'Headphones', price: 79 } ]; ``` ## How it works `Bind` restores the submitted budget, `BInput` validates the field, and `BButton` runs `filterProducts` on the server when the form is valid. Astro renders the result count and matching rows together. The embedded preview simulates this interaction locally. ## Run it yourself Follow the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) or copy the configuration, middleware, and shared layout from the file sidebar. Add both the page and catalog files at the paths shown. --- # Edit profile settings Text, select, and textarea fields in one form. Source: https://withastro-utils.github.io/docs/examples/settings/ Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project. ## Try the interaction Change the name, language, and bio, then save. In your own project, reload the page to see the session-backed values restored. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/settings.astro ```astro title="src/pages/settings.astro" --- import { BButton, BInput, BOption, BSelect, BTextarea, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; type Preferences = { name: string; locale: string; bio: string }; const bind = Bind(Astro.locals.session.preferences ?? { name: 'Ari Chen', locale: 'en', bio: '' }); let saved = false; function savePreferences() { Astro.locals.session.preferences = { name: bind.name, locale: bind.locale, bio: bind.bio }; saved = true; } --- Edit profile settings {saved &&

Preferences saved.

} English Spanish French Save changes
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## How it works The source reads and writes a small preferences object through `Astro.locals.session`. The embedded preview only demonstrates the controls and save feedback. Use application storage for preferences that must survive session expiry or work across devices. ## Continue Explore the [input component reference](https://withastro-utils.github.io/docs/reference/forms/basic-form-components/index.md). --- # Create an account Account fields and valid-only actions. Source: https://withastro-utils.github.io/docs/examples/signup/ Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project. ## Try the interaction Enter a name and email, then submit. An invalid email is rejected before the action runs. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/signup.astro ```astro title="src/pages/signup.astro" --- import { BButton, BInput, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; const bind = Bind({ name: '', email: '' }); let message = ''; function signUp() { try { signupUser({ name: bind.name, email: bind.email }); message = `Account details received for ${bind.email}.`; bind.defaults(); Astro.locals.forms.redirectTimeoutSeconds('/', 2); // demo - not redirecting } catch (error){ message = `Error: ${error.message}`; } } --- Create an account Create account {message &&

{message}

}
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## How it works The runnable source stores the submitted details in the signed session. It does not create a database account or send an email. Replace that small callback with your account service once the form works. ## Continue Learn how to [show custom validation errors](https://withastro-utils.github.io/docs/guides/forms/data-binding/index.md#validate-a-reserved-name). --- # Manage a task list Adding and removing repeated items. Source: https://withastro-utils.github.io/docs/examples/todo/ Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project. ## Try the interaction Add a task, then remove it. Removal does not require a valid value in the new-task field. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/todo.astro ```astro title="src/pages/todo.astro" --- import { BButton, BInput, Bind, BindForm } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; type TaskForm = { newTask: string; message: string; }; type TodoTask = { title: string; id: string; } const bind = Bind({ newTask: '', message: '' }); const tasks: TodoTask[] = Astro.locals.session.tasks ??= [{ id: 'limits', title: 'Review upload limits' }, { id: 'notes', title: 'Publish migration notes' }]; async function addTask() { tasks.push({title: bind.newTask, id: crypto.randomUUID()}); bind.message = `${bind.newTask} added.`; bind.newTask = ''; } async function removeTask(id: string, title: string) { const index = tasks.findIndex(task => task.id === id); if (index === -1) { bind.message = 'This task has already been removed.'; return; } tasks.splice(index, 1); bind.message = `${title} removed.`; } --- Manage a task list {bind.message &&

{bind.message}

} Add task
    {tasks.map(task => (
  • {task.title} removeTask(task.id, task.title)}>Remove
  • ))}
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## How it works The runnable source stores a small list in the signed session cookie. It survives page reloads while that session remains valid; it is not shared between devices. Keep the list small or move it to application storage. The removal handler checks that the item still exists before changing the list. ## Continue Learn about [stable keys and reusable components](https://withastro-utils.github.io/docs/guides/forms/loops-and-components/index.md). --- # Upload a large file Chunked transfer, progress, and completion feedback. Source: https://withastro-utils.github.io/docs/examples/upload/ Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project. ## Try the interaction Select a file and upload it. Use the preview’s **Simulate upload failure** option to compare success with a retryable error. ### src/layouts/Layout.astro ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ### src/pages/upload.astro ```astro title="src/pages/upload.astro" --- import { type BigFile, BButton, Bind, BindForm, FormErrors, UploadBigFile, UploadBigFileProgress } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; type UploadForm = { file: BigFile; }; const bind = Bind(); let message = ''; async function saveFile() { message = `${bind.file.name} is ready.`; } --- Upload a large file {message &&

{message}

} Upload file
``` ### src/middleware.ts ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` ### astro.config.mjs ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## How it works The source uploads the selected file and reports its metadata. Add your application processing in `saveFile`. The embedded preview simulates transfer progress and never sends your file to a server. `UploadBigFileProgress for="file"` matches the upload control’s `name="file"`, even when its HTML ID is `document`. Every `BButton` submits its containing form, so put actions that must not upload the selected file outside that form. See [upload limits and failure handling](https://withastro-utils.github.io/docs/reference/forms/upload-big-file/index.md#configure-upload-limits). ## Continue Review [Forms configuration](https://withastro-utils.github.io/docs/reference/forms/configuration/index.md). --- # Context quickstart Share a value between a layout and a nested component. Source: https://withastro-utils.github.io/docs/guides/context/ Context passes values to nested Astro components without forwarding a prop through every intermediate component. It exists during a render; it is not a browser store or persistent session. ## Install Context ```sh title="Terminal" npm install @astro-utils/context ``` ```sh title="Terminal" pnpm add @astro-utils/context ``` ```sh title="Terminal" yarn add @astro-utils/context ``` ```sh title="Terminal" bun add @astro-utils/context ``` ## Provide a named value ```astro title="src/pages/index.astro" --- import Context from '@astro-utils/context/Context.astro'; const user = await fetchUser(); ---

This is home page

``` ## Every layout/component can use it ```astro title="src/components/Button.astro" --- import getContext from '@astro-utils/context'; const { user } = getContext(Astro, 'user'); --- ``` ```astro title="src/layout/Website.astro" --- import getContext from '@astro-utils/context'; const { title } = Astro.props; const { user } = getContext(Astro, 'user'); --- {title}

{user ? "User logged in" : "User not logged in"}

``` ## Nested values A nested provider with the same `contextName` inherits the outer values and overrides the props it supplies (extends and override). An absent context returns `{}`. See [Context API](https://withastro-utils.github.io/docs/reference/context/index.md) for manual async rendering and the prop reference. --- # Express Endpoints quickstart Create and call an Astro API route with Express-style request and response helpers. Source: https://withastro-utils.github.io/docs/guides/express-endpoints/ Use Express-style routing and middleware in an Astro API endpoint. This example accepts JSON and returns a greeting. ## Install the package and adapter ```sh title="Terminal" npm install @astro-utils/express-endpoints @astrojs/node ``` ```sh title="Terminal" pnpm add @astro-utils/express-endpoints @astrojs/node ``` ```sh title="Terminal" yarn add @astro-utils/express-endpoints @astrojs/node ``` ```sh title="Terminal" bun add @astro-utils/express-endpoints @astrojs/node ``` ## Enable server rendering ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }) }); ``` ## Create a route ```ts title="src/pages/api/hello.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); router.body('json'); export const POST = router.route((req, res) => { const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''; if (!name) { res.status(400).json({ error: 'Name is required.' }); return; } res.json({ message: `Hello, ${name}!` }); }); ``` ## Call it Start your project with its `dev` script. Use the port printed by Astro in this request: ```sh title="Terminal" curl -X POST http://localhost:4321/api/hello \ -H 'Content-Type: application/json' \ -d '{"name":"Alex"}' ``` The response is `{"message":"Hello, Alex!"}`. Sending `{}` returns status 400 and `{"error":"Name is required."}`. The example has no artificial delay or external service dependency. ## Parse a different body `router.body('multipart')` makes uploaded files available on `req.filesOne` and `req.filesMany`. Use `urlencoded` for an ordinary HTML form or `text` for a text body. The default `auto` mode selects the parser from `Content-Type`. See [request examples](https://withastro-utils.github.io/docs/reference/express/request/index.md) and [response examples](https://withastro-utils.github.io/docs/reference/express/response/index.md). ## Add schema validation For schema-based validation, install the Zod major version supported by the package's [express-zod-safe](https://www.npmjs.com/package/express-zod-safe) integration: ```sh title="Terminal" npm install zod@^4 ``` ```sh title="Terminal" pnpm add zod@^4 ``` ```sh title="Terminal" yarn add zod@^4 ``` ```sh title="Terminal" bun add zod@^4 ``` ```ts title="src/pages/api/validated-hello.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; import { z } from 'zod'; const router = new ExpressRoute(); router.validate({ body: z.object({ name: z.string().trim().min(1) }) }); export const POST = router.route((req, res) => { res.json({ message: `Hello, ${req.body.name}!` }); }); ``` By default, it's relax validation that ignore extra properties, defaults to: - defaultSchemaObject: lax - missingSchemaBehavior: any Validation applies to the next route created by this router. Review [middleware and error handling](https://withastro-utils.github.io/docs/reference/express/router-and-errors/index.md) before composing several handlers. --- # Binding & Validation Using data binding with Astro Forms Source: https://withastro-utils.github.io/docs/guides/forms/data-binding/ With Astro Forms you can easily create forms with data binding and validation. It also provides a simple way to handle form submissions and preserve state between postbacks. ## Data Binding Astro Forms supports two-way data binding. This means that you can bind a form field to a property on `Bind` instance and the value of the property will be updated when the field changes and vice versa. ### Submit a profile ### src/pages/profile.astro ```astro title="src/pages/profile.astro" --- import { Bind, BindForm, BButton, BInput } from "@astro-utils/forms/forms.js"; import Layout from "../layouts/Layout.astro"; type Form = { name: string; age: number; } const bind = Bind(); let showSubmitText = ''; function formSubmit(){ showSubmitText = `Your name is ${bind.name}; you are ${bind.age} years old.`; bind.age++; } --- {showSubmitText} Submit ``` In this example, the `Bind` instance is bound to the form fields. - When the user changes the value of the `name` or `age` fields and submits, the `Bind` instance is updated. - The `formSubmit` function will be called when the user clicks the `Submit` button and the form is valid. - After `formSubmit` the `age` property will be incremented and the `showSubmitText` will be updated. ## Validate a reserved name HTML constraints such as `required` and `minlength` are checked by the browser and server. A custom validator handles application rules. Try `admin` to see a server validation error; `Alex` succeeds. ```astro title="src/pages/choose-name.astro" --- import { BButton, BInput, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; const bind = Bind({ name: '' }); let saved = false; function validateName(value: string) { if (['admin', 'support'].includes(value.trim().toLowerCase())) { return { error: 'Choose a name other than admin or support.' }; } } function save() { saved = true; } --- Choose a name Save name {saved &&

Name accepted: {bind.name}

}
``` Return `{ error }` to reject a value, or return nothing to accept it. `FormErrors` renders the feedback; `whenFormOK` prevents `save` from running on invalid input. See the [validation contract](https://withastro-utils.github.io/docs/reference/forms/data-binding/index.md#custom-validation). ## View State Astro Forms also supports view state. This means that the values of the form fields will be preserved between postbacks. ### Keep a counter between submissions ### src/pages/counter.astro ```astro title="src/pages/counter.astro" --- import { Bind, BindForm, BButton } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; type Form = { counter: number; } const bind = Bind({counter: 0}); function incCounter(){ bind.counter++; } --- Counter: {bind.counter} Increase counter ``` What happens here is that the `counter` property of the `Bind` instance will be incremented every time the user clicks the `Increase counter` button. The `Bind` state persists between postbacks by storing compressed and encrypted view state on the client. ### Valid values The state of the `Bind` instance must be serializable, but it can contain more than plain JSON values. You can use any valid [SuperJSON](https://github.com/blitz-js/superjson) value in the `Bind` instance. This includes `Date`, `Map`, `URL`, `Set`, `RegExp`, `BigInt`, `undefined` are supported. ### Button State You can use state per `BButton` component. You can also change the `BButton` props easily each time the button is clicked. Special props: - **state**: any - store state per button - **extra**: any - store extra data per button (cannot be changed in the `onClick` function) - **innerText**: string - the text of the button (overrides the children) - **innerHTML**: string - the HTML of the button (overrides the children) - **remove**: boolean - remove the button from the HTML - **reset()**: function - reset the button to the initial state - **defaults**: any - the default state of the button ### src/pages/button-colors.astro ```astro title="src/pages/button-colors.astro" --- import { BButton, Bind, BindForm } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'black', 'white', 'gray']; function changeColor() { this.state.color++; if (this.state.color >= colors.length) { this.state.color = this.extra; } this.style = `color: ${colors[this.state.color]}`; } --- { colors.map((_, i) => ( Button {i} )) } ``` In this example, the `changeColor` will change the button color each time it is clicked to another color in the `colors` array. If the button is clicked more times than the length of the `colors` array, it starts from the beginning. --- # Getting Started Install Astro Utils Forms and run a complete server action without an external service. Source: https://withastro-utils.github.io/docs/guides/forms/getting-started/ Build a form that sends a name to your server and renders a greeting. Start inside an existing Astro project; no database or email provider is needed. **Use server rendering** Forms need on-demand rendering. This guide configures the Node adapter and a single `WebForms` root. ## 1. Install Forms and the adapter ```sh title="Terminal" npm install @astro-utils/forms @astrojs/node ``` ```sh title="Terminal" pnpm add @astro-utils/forms @astrojs/node ``` ```sh title="Terminal" yarn add @astro-utils/forms @astrojs/node ``` ```sh title="Terminal" bun add @astro-utils/forms @astrojs/node ``` ## 2. Configure Astro ```js title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import forms from '@astro-utils/forms/dist/integration.js'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), integrations: [forms] }); ``` ## 3. Add middleware ```ts title="src/middleware.ts" import forms from '@astro-utils/forms'; export const onRequest = forms(); ``` The default is enough for local development. For deployed projects, pass a stable environment secret through the [middleware configuration](https://withastro-utils.github.io/docs/reference/forms/configuration/index.md). Keep it stable across restarts and out of source control. For an existing middleware chain, compose this middleware with Astro's `sequence()` helper. ## 4. Add the shared layout Use one `WebForms` root around your form content. The examples throughout these docs import this layout. ```astro title="src/layouts/Layout.astro" --- import { WebForms } from '@astro-utils/forms/forms.js'; --- <slot name="title" /> ``` ## 5. Create your first form Save the following page as `src/pages/hello.astro`. **Preview** simulates the same interaction inside the docs; running the page in your own project executes the callback on your server. ### src/pages/hello.astro ```astro title="src/pages/hello.astro" --- import { BButton, BInput, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; const bind = Bind({ name: '' }); let message = ''; function greet() { message = `Hello, ${bind.name}! Your server action ran.`; } --- Your first form Say hello {message &&

{message}

}
``` ## 6. Run it Start your Astro project with your package manager's `dev` script. Open `/hello` on the URL printed by Astro, enter a name, and select **Say hello**. You should see **Hello, Alex! Your server action ran.** when you enter Alex. An empty name prevents the valid-only callback. ## What happens on submit 1. `WebForms` receives the POST request. 2. `BindForm` restores state and validates the submitted controls. 3. `BButton` calls `greet` when validation succeeds. 4. The form renders again with the updated message. `message` is request-local feedback. Use `omitState` when feedback is stored on `bind` but should not persist into the next submission. ## Next steps Learn [binding and validation](https://withastro-utils.github.io/docs/guides/forms/data-binding/index.md), or adapt the [account example](https://withastro-utils.github.io/docs/examples/signup/index.md) to your application. If the action does not run, check server rendering, middleware, the integration, and the `WebForms` layout first. --- # Server Helpers Redirect after a form action, refresh a parent form, and update the browser. Source: https://withastro-utils.github.io/docs/guides/forms/js-helpers/ Use `Astro.locals.forms` inside a Forms page with the [shared setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md). ## Show feedback before redirecting ```astro title="src/pages/finish.astro" --- import { BButton, BindForm } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; let message = ''; function finish() { message = 'Done. Returning to the home page in two seconds.'; Astro.locals.forms.redirectTimeoutSeconds('/', 2); } --- Finish a task Finish

{message}

``` `redirectTimeoutSeconds` sends a browser action, so the user sees the rendered feedback before navigating. Use `redirect('/account')` in the callback when you want an immediate HTTP redirect instead. ## Refresh a parent after a child action A form rerenders after its own action. Call `reloadState()` only when a nested form changes data shown by its parent. The [editable users example](https://withastro-utils.github.io/docs/guides/forms/loops-and-components/index.md) demonstrates deletion and the parent's count updating together. For database-backed parent data, attach a loader to `bind.on.reloadState`. A reload does not run the original button callback again. ## Choose a helper | Helper | Use it for | | --- | --- | | `redirect(url, status?)` | An immediate server redirect. | | `redirectTimeoutSeconds(url, seconds?)` | Render feedback, then navigate in the browser. | | `reloadState()` | Rerender a parent affected by a child action. | | `updateSearchParams()` | Change several URL query values, then call the returned `redirect()`. | | `updateOneSearchParam(key, value?, status?)` | Change or remove one query value and redirect. | | `alert(message)` | Show a browser alert. Prefer inline feedback for ordinary validation. | | `consoleLog(...values)` / `console(type, ...values)` | Log in the browser. | | `callFunction(name, ...args)` | Call an existing browser function. | ## Stop a nested operation with a response Use [ThrowOverrideResponse](https://withastro-utils.github.io/docs/reference/forms/advanced-responses/index.md) when a nested function must stop processing and return a response. Setting a redirect and throwing a response serve different control-flow needs; ordinary form callbacks usually only need the redirect helper. --- # Loops & Reusable Components Keep each repeated form attached to the right item when a list changes. Source: https://withastro-utils.github.io/docs/guides/forms/loops-and-components/ Start with the [shared Forms layout and middleware](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md). These two files make a small editable list stored in the browser's Forms session. ## Render a list of forms ```astro title="src/pages/users.astro" --- import { BButton, Bind, BindForm } from '@astro-utils/forms/forms.js'; import Layout from '../layouts/Layout.astro'; import UserItem from '../components/UserItem.astro'; type User = { id: string; name: string; }; const users: User[] = Astro.locals.session.users ??= [{ id: 'first', name: 'Alex' }]; const bind = Bind({}); function addUser() { users.push({ id: crypto.randomUUID(), name: 'New user' }); } --- Edit users

Users: {users.length}

Add user {users.map(user => )}
``` ## Give each item a stable key ```astro title="src/components/UserItem.astro" --- import { BButton, BInput, Bind, BindForm, FormErrors } from '@astro-utils/forms/forms.js'; interface Props { user: { id: string; name: string } } const { user } = Astro.props; const bind = Bind({ name: user.name }); let saved = false; function saveUser() { user.name = bind.name; saved = true; } function deleteUser() { const users: Props['user'][] = Astro.locals.session.users; const index = users.findIndex(item => item.id === user.id); if (index !== -1) users.splice(index, 1); Astro.locals.forms.reloadState(); // reloads the parent BindForm (until the top) } --- Save Delete {saved &&

Saved!

}
``` ## Try adding and removing items Open `/users`, add two users, edit their names, and delete the first user. The remaining form must keep its own name and actions. `key={user.id}` ties each child form to the item instead of its current position. Do not use the array index as the key when items can be inserted, removed, or reordered. Ordinary static components and loops do not need an explicit key. Saving a child renders that child again. Deleting changes the parent's list, so `reloadState()` requests a parent rerender and updates the count. If parent data comes from a database, reload it in the parent bind's `on.reloadState` callback before rendering. Keep the JWT (cookie session) small. For a larger list, use durable storage and fetch only the items shown on the page. --- # Server-first forms for Astro Build Astro forms with server-side validation, data binding, state, and server actions. Explore examples, request context, and Express-style endpoints. Source: https://withastro-utils.github.io/docs/ Astro Utils provides server-first forms, request context, and Express-style endpoints. [Start building](https://withastro-utils.github.io/docs/agents/getting-started.md) - [Create an account](https://withastro-utils.github.io/docs/examples/signup/index.md): Bind account fields, validate them, and run a server action after a valid submission. - [Log in with feedback](https://withastro-utils.github.io/docs/examples/login/index.md): Try success and error feedback with a clearly labeled demo session. - [Manage a task list](https://withastro-utils.github.io/docs/examples/todo/index.md): Add and remove persisted items while the form automatically renders the latest server state. - [Edit profile settings](https://withastro-utils.github.io/docs/examples/settings/index.md): Combine text, select, and textarea controls in one saved preferences form. - [Upload a large file](https://withastro-utils.github.io/docs/examples/upload/index.md): Upload a file in bounded chunks and show progress before processing it on the server. - [Submit a service request](https://withastro-utils.github.io/docs/examples/government-form/index.md): Group an application into sections and save a small draft in the session. - [Filter a product catalog](https://withastro-utils.github.io/docs/examples/products/index.md): Bind a price filter, run server logic, and render matching products. --- # Context API Context component, getContext, and asyncContext contracts for Astro Utils Context. Source: https://withastro-utils.github.io/docs/reference/context/ ## `Context` ```astro title="src/layouts/Layout.astro" --- import Context from '@astro-utils/context/Context.astro'; --- ``` ## `getContext(Astro, name?)` Returns the nearest value for `name`, or `{}` when no provider is active. The default name is `"default"`. ```astro title="src/components/ContextContent.astro" --- import getContext from '@astro-utils/context'; const theme = getContext(Astro, 'theme'); --- ``` ## `asyncContext(promise, Astro, options?)` Runs an async render while the supplied context is active and always cleans it up afterward. ```astro title="src/components/ContextContent.astro" --- import { asyncContext } from '@astro-utils/context'; const html = await asyncContext(() => Astro.slots.render('default'), Astro, { name: 'theme', context: { color: 'blue' } }); --- ``` | Option | Type | Default | | --- | --- | --- | | `name` | `string` | `"default"` | | `context` | `unknown` | `Astro.props` | Start with the [connected provider/consumer example](https://withastro-utils.github.io/docs/guides/context/index.md) before managing slot rendering manually. --- # Request API Parsed values and content-negotiation helpers available on ExpressRequest. Source: https://withastro-utils.github.io/docs/reference/express/request/ ## Read a JSON request ```ts title="src/pages/api/profile.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; import z from "zod"; const router = new ExpressRoute(); router.body('json'); router.validate({ body: z.object({ name: z.string() }), query: z.object({ page: z.coerce.number() }) }); export const POST = router.route((req, res) => { res.json({ name: req.body.name, page: req.query.page ?? 1, userAgent: req.get('user-agent') }); }); ``` ## Read an uploaded file ```ts title="src/pages/api/upload-info.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); router.body('multipart'); export const POST = router.route((req, res) => { if (req.error) { res.status(400).json({ error: 'The upload could not be parsed.' }); return; } const file = req.filesOne.document; if (!file) { res.status(400).json({ error: 'Attach a file in the document field.' }); return; } res.json({ name: file.name, bytes: file.size }); }); ``` Send multipart form data with a field named `document`. Use `filesMany.document` when the field accepts several files. ## Values | Property | Value | | --- | --- | | `astroContext` | Original Astro `APIContext` | | `body` | Parsed body | | `query`, `params`, `headers` | String maps | | `cookies` | Parsed cookie values | | `session`, `locals` | Values supplied by Astro middleware | | `filesOne` | First native `File` for each multipart field | | `filesMany` | Every native `File` for each multipart field | | `method`, `url`, `originalUrl`, `path` | Request routing values | | `hostname`, `subdomains`, `ip` | Connection values | | `error` | Multipart parsing error, when present | ## Helpers | Method | Behavior | | --- | --- | | `get(name)` | Gets a request header case-insensitively. | | `header(name, fallback?)` | Gets a header with an optional fallback. | | `is(type)` | Compares the request content type. | | `param(name, fallback?)` | Checks params, then body, then query. | | `accepts(types, ...types)` | Negotiates media types. | | `acceptsCharsets(types, ...types)` | Negotiates character sets. | | `acceptsEncodings(types, ...types)` | Negotiates encodings. | | `acceptsLanguages(types, ...types)` | Negotiates languages. | --- # Response API Status, headers, bodies, redirects, files, cookies, and streaming with ExpressResponse. Source: https://withastro-utils.github.io/docs/reference/express/response/ ## Return JSON with status and headers ```ts title="src/pages/api/status.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); export const GET = router.route((_req, res) => { res.status(200); res.set('Cache-Control', 'no-store'); res.json({ status: 'ready', checkedAt: new Date().toISOString() }); }); ``` A GET to `/api/status` returns JSON with an explicit status and cache policy. Set headers before sending the body; `json()` finishes the response. `ExpressResponse` is a writable stream. Set status and headers before the first `write()` or `end()` call. ## Save a preference and redirect ```ts title="src/pages/api/preferences.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); export const POST = router.route((_req, res) => { res.cookie('theme', 'dark', { httpOnly: true, sameSite: 'lax', path: '/', maxAge: 86400000 }); // maxAge in milliseconds res.redirect('/', 303); }); ``` The cookie stores a display preference, not an authentication credential. Use secure cookies when serving over HTTPS. A 303 redirects the browser to a GET after the POST. ## Stream a text response ```ts title="src/pages/api/report.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); export const GET = router.route((_req, res) => { res.type('text/plain'); res.write('Report started\n'); res.end('Report complete\n'); }); ``` Set headers before the first write, and always end the response. For a file already on disk, use `sendFile` or `attachment` instead. ## Finish a response | Method | Behavior | | --- | --- | | `status(code)` | Sets `statusCode`. | | `sendStatus(code)` | Sends the standard status text. | | `send(body)` | Sends bytes, text, or JSON-compatible data. | | `json(value)` | Sends JSON. | | `html(value)` | Sends HTML. | | `end(body?)` | Ends the response. | | `write(chunk, encoding?)` | Starts or continues a streamed response. | | `redirect(url, status?)` | Redirects with status 302 by default. | | `sendFile(path)` | Streams a file and infers its content type. | | `attachment(path, name?)` | Streams a file as a download. | ## Headers and cookies | Method | Behavior | | --- | --- | | `set()` / `header()` | Sets one header or a header map. | | `get()` / `getHeader()` | Reads a header. | | `getHeaders()` / `getHeaderNames()` | Lists response headers. | | `hasHeader()` / `removeHeader()` | Checks or removes a header. | | `append()` / `appendHeader()` | Appends a header value. | | `type(value)` | Sets a MIME content type. | | `writeHead(code, headers?)` | Sets status and headers, then starts the response. | | `cookie(name, value, options?)` | Appends a `Set-Cookie` header. Object values use JSON cookie encoding. | | `clearCookie(name, options?)` | Expires a cookie. | | `cacheControl({ minuets, days, months })` | Sets `max-age`. The public minute option is spelled `minuets`. | `headersSent` becomes true after the response starts. Header changes and duplicate response endings then throw `ExpressError`. --- # Router and errors ExpressRoute middleware, body parsing, validation, route creation, and error classes. Source: https://withastro-utils.github.io/docs/reference/express/router-and-errors/ ## Compose middleware and a handler ```ts title="src/pages/api/status.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); router.use((_req, res, next) => { res.set('Cache-Control', 'no-store'); next(); }); export const GET = router.route((_req, res) => { res.json({ status: 'ready' }); }); ``` `next()` allows the final handler to run. A middleware that sends a response should return without calling it. ## `ExpressRoute` | Method | Behavior | | --- | --- | | `use(middleware)` | Appends middleware or another router and returns this router. | | `body(type)` | Sets parsing to `auto`, `json`, `multipart`, `urlencoded`, or `text`. | | `validate(schemas)` | Adds Zod Express validation to the next route. | | `route(...handlers)` | Returns an Astro `APIRoute`. | Body parsing runs only for `POST`, `PUT`, and `PATCH`. In `auto` mode, the request `Content-Type` selects the parser. Middleware stops unless it calls `next()`. A handler must write or end the response before the Astro route can complete. ## Errors `ExpressError` represents response lifecycle and header errors. `ExpressBodyError` extends it for body parsing failures. Multipart parser failures are recorded on `req.error`. Other uncaught errors return their message using `error.status` when present, otherwise status 500. ## Handle an application error ```ts title="src/pages/api/double.ts" import { ExpressRoute } from '@astro-utils/express-endpoints'; const router = new ExpressRoute(); export const GET = router.route((req, res) => { const value = Number(req.query.value); if (req.query.value == null || !Number.isFinite(value)) { res.status(400).json({ error: 'Supply a numeric value query parameter.' }); return; } res.json({ result: value * 2 }); }); ``` Call `/api/double?value=4` for `{"result":8}`, or omit `value` to see the explicit JSON error. Catch application failures where you can turn them into a useful response. Do not expose internal exception details to callers. `ExpressError.code` is not the same property as the router’s `error.status` fallback; explicitly set the response status when handling an error. --- # Advanced responses Return a custom response from a nested Forms operation. Source: https://withastro-utils.github.io/docs/reference/forms/advanced-responses/ ## Stop processing and return a response This is an Astro frontmatter excerpt for a page using the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md). A nested operation throws to stop page processing immediately: ```astro title="src/pages/account.astro" --- import { ThrowOverrideResponse } from '@astro-utils/forms/forms.js'; function requireUser() { if (!Astro.locals.session.userId) { throw new ThrowOverrideResponse(Astro.redirect('/login')); } } requireUser(); --- ``` Your authentication system must set `session.userId` after verifying the user. This guard is not an authentication implementation. ## Return a custom body ```astro title="src/pages/export.astro" --- import { ThrowOverrideResponse } from '@astro-utils/forms/forms.js'; throw new ThrowOverrideResponse(new Response('name\nAlex\n', { headers: { 'Content-Type': 'text/csv; charset=utf-8' } })); --- ``` ## Constructor behavior | Argument | Behavior | | --- | --- | | `response` | Optional native `Response` (or `null`). | | `message` | Optional fallback message. | If no response is supplied, Forms uses `Astro.locals.forms.overrideResponse`. If that is also missing, the fallback message is returned with status 500. The default message is `An error occurred, please try again later.` For an ordinary redirect from a button callback, use the [redirect helper](https://withastro-utils.github.io/docs/guides/forms/js-helpers/index.md). --- # Basic Form Components Bound inputs, select controls, buttons, and validation feedback. Source: https://withastro-utils.github.io/docs/reference/forms/basic-form-components/ The template excerpts below belong inside a `BindForm` using the [shared Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md). Import the components you use from `@astro-utils/forms/forms.js`. ## Inputs ### BInput The basic component for most form fields. ```astro ``` It includes the following attributes: - **type** - HTML input types: `text`, `checkbox`, `date`, `email`, `number`, `radio`, `file`... / `int` - **min** - number / date - **max** - number / date - **minlength** - minimum text length - **maxlength** - maximum text length - **pattern** - regex pattern for text - **multiple** - for files - **required** - a value must be provided - **readonly** - make the value unchangeable - **value** - default value, mainly use for the `readonly` attribute - **as** - change the base element (default `input`, can be a React component) - **parseTimeFormat** - for type `time` only, allowing you to parse and stringify time as milliseconds instead of a `Date` - **props** - props for the `as` element ### BTextarea Text-only input rendered as a `