import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
A form lets your app ask users to input and submit data. Forms can be defined with a simple form object that takes a list of fields, and return user responses directly as promises.
Promise-based forms:
import { showForm } from '@devvit/web/client';
// Show form and get user response directly
const result = await showForm({
form: {
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
},
],
},
data: { name: 'Default value' } // Optional initial data
});
// Handle form submission result immediately
if (result) {
const { name } = result;
// Process the data directly
console.log(`User entered: ${name}`);
// Chain additional actions
await fetch('/api/save-name', {
method: 'POST',
body: JSON.stringify({ name })
});
// Or show another form in sequence
const step2 = await showForm({
form: {
fields: [
{
type: 'string',
name: 'food',
label: 'Favorite food?',
},
],
}
});
if (step2) {
console.log(`Multi-step complete: ${name}, ${step2.food}`);
}
} else {
console.log('User cancelled the form');
}showForm(options) → Returns Promise
form(Form): The form specification objectdata(FormValues, optional): Initial form field values- Returns:
Promise<FormValues | null>- Resolves with form data or null if cancelled
// Interactive post with form // addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. View the announcement below this example. Devvit.addCustomPostType({ name: 'FormExample', render: (context) => { const [name, setName] = useState('unknown');
const myForm = useForm(
{
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
},
],
},
(values) => {
// onSubmit handler
setName(values.name);
}
);
return (
<vstack gap="medium" height="100%" alignment="middle center">
<text>Hello {name}!</text>
<button
onPress={() => {
context.ui.showForm(myForm);
}}
>
Set name
</button>
</vstack>
);
},
});
// Menu action with form const myForm = Devvit.createForm( { fields: [ { type: 'string', name: 'food', label: 'What is your favorite food?', }, ], }, (event, context) => { // onSubmit handler context.ui.showToast({ text: event.values.food }); } );
Devvit.addMenuItem({ label: 'Show a form', location: 'subreddit', onPress: async (_event, context) => { context.ui.showForm(myForm); }, });
[View `addCustomPostType` deprecation announcement.](https://www.reddit.com/r/Devvit/comments/1r3xcm2/devvit_web_and_the_future_of_devvit/)
### Methods
**`context.ui.showForm(formConfig, onSubmit)`** - For interactive posts
- `formConfig` (Form): The form specification object
- `onSubmit` (function): Callback function when form is submitted
**`Devvit.createForm(formConfig, onSubmit)`** - For menu actions
- `formConfig` (Form): The form specification object
- `onSubmit` (function): Callback function when form is submitted
</TabItem>
</Tabs>
## Menu response forms
For forms that open from a menu item, you can use menu responses. This is useful since you do not have access to the `@devvit/web/client` library from a menu item endpoint.
<Tabs>
<TabItem value="web" label="Devvit Web">
**Configure forms in devvit.json:**
```json title="devvit.json"
{
"forms": {
"nameForm": "/internal/form/name-submit",
"reviewForm": "/internal/form/review-submit"
}
}
Server endpoint that shows form via menu response:
<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
type NameFormRequest = { name: string };
type ReviewFormRequest = { review: string };
// Menu action that triggers menu response form
app.post('/internal/menu/start-workflow', async (c) => {
const _input = await c.req.json<MenuItemRequest>();
// Server processing before showing form
const userData = await fetchUserData();
return c.json<UiResponse>({
showForm: {
name: 'nameForm',
form: {
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
},
],
},
data: { name: userData.name } // Pre-populate from server
}
});
});
// Form submission handler that can chain to another form
app.post('/internal/form/name-submit', async (c) => {
const { name } = await c.req.json<NameFormRequest>();
// Server processing
await saveUserName(name);
// Show next form in workflow
return c.json<UiResponse>({
showForm: {
name: 'reviewForm',
form: {
fields: [
{
type: 'paragraph',
name: 'review',
label: 'How was your experience?',
},
],
}
}
});
});
app.post('/internal/form/review-submit', async (c) => {
const { review } = await c.req.json<ReviewFormRequest>();
await saveReview(review);
return c.json<UiResponse>({
showToast: 'Thank you for your feedback!'
});
});import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
type NameFormRequest = { name: string };
type ReviewFormRequest = { review: string };
// Menu action that triggers menu response form
router.post<string, never, UiResponse, MenuItemRequest>("/internal/menu/start-workflow", async (_req, res) => {
// Server processing before showing form
const userData = await fetchUserData();
res.json({
showForm: {
name: 'nameForm',
form: {
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
},
],
},
data: { name: userData.name } // Pre-populate from server
}
});
});
// Form submission handler that can chain to another form
router.post<string, never, UiResponse, NameFormRequest>("/internal/form/name-submit", async (req, res) => {
const { name } = req.body;
// Server processing
await saveUserName(name);
// Show next form in workflow
res.json({
showForm: {
name: 'reviewForm',
form: {
fields: [
{
type: 'paragraph',
name: 'review',
label: 'How was your experience?',
},
],
}
}
});
});
router.post<string, never, UiResponse, ReviewFormRequest>("/internal/form/review-submit", async (req, res) => {
const { review } = req.body;
await saveReview(review);
res.json({
showToast: 'Thank you for your feedback!'
});
});For Devvit Blocks, use the standard promise-based approach even in menu actions:
Devvit.addMenuItem({
label: 'Multi-step workflow',
location: 'subreddit',
onPress: async (_event, context) => {
// Step 1: Get user data from server
const userData = await fetchUserData();
// Step 2: Show form with server data
const step1 = await context.ui.showForm({
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
},
],
data: { name: userData.name }
});
if (!step1) return;
// Step 3: Save and continue to next form
await saveUserName(step1.name);
const step2 = await context.ui.showForm({
fields: [
{
type: 'paragraph',
name: 'review',
label: 'How was your experience?',
},
],
});
if (step2) {
await saveReview(step2.review);
context.ui.showToast('Thank you for your feedback!');
}
},
});The form object enables you to customize the form container and the list of form fields included. The form object structure is the same for both Devvit Web and Devvit Blocks.
const myForm = {
title: 'My form',
description: 'This is my form. There are many like it, but this one is mine.',
fields: [
{
type: 'string',
name: 'food',
label: 'What is your favorite food?',
},
{
type: 'string',
name: 'drink',
label: 'What is your favorite drink?',
},
],
acceptLabel: 'Submit',
cancelLabel: 'Cancel',
};| Property | Supported types | Description |
|---|---|---|
title |
string undefined |
An optional title for the form |
description |
string undefined |
An optional description for the form |
fields |
FormField[] |
The fields that will be displayed in the form |
acceptLabel |
string undefined |
An optional label for the submit button |
cancelLabel |
string undefined |
An optional label for the cancel button |
The following field types are supported: String, Select, Paragraph, Number, Boolean, Image, and Group.
A single-line text input.
const stringField = {
type: 'string',
name: 'title',
label: 'Tournament title',
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
name |
string |
The name of the field. This will be used as the key in the values object when the form is submitted. |
label |
string |
The label of the field. This will be displayed to the user. |
helpText |
string undefined |
An optional help text that will be displayed below the field. |
required |
boolean undefined |
If true the field will be required and the user will not be able to submit the form without filling it in. Defaults to false. |
disabled |
boolean undefined |
If true the field will be disabled. Defaults to false. |
defaultValue |
ValueType undefined |
The default value of the field. |
scope |
SettingScopeType undefined |
This indicates whether the field (setting) is an app level or install level setting. App setting values can be used by any installation. undefined by default. |
placeholder |
string undefined |
Placeholder text for display before a value is present. |
isSecret |
boolean undefined |
Makes the form field secret. |
A dropdown menu with predefined options.
const selectField = {
type: 'select',
name: 'interval',
label: 'Update the leaderboard',
options: [
{ label: 'Hourly', value: 'hourly' },
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' },
{ label: 'Yearly', value: 'yearly' },
],
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
name |
string |
The name of the field. This will be used as the key in the values object when the form is submitted. |
label |
string |
The label of the field. This will be displayed to the user. |
options |
FieldConfig_Selection_Item[] |
The list of options available. |
helpText |
string undefined |
An optional help text that will be displayed below the field. |
required |
boolean undefined |
If true the field will be required and the user will not be able to submit the form without filling it in. Defaults to false. |
disabled |
boolean undefined |
If true the field will be disabled. Defaults to false. |
defaultValue |
string[] undefined |
The default value of the field. Note that the default value is wrapped in an array to support multiple selected values. |
scope |
SettingScopeType undefined |
This indicates whether the field (setting) is an app level or install level setting. App setting values can be used by any installation. undefined by default. |
multiSelect |
boolean undefined |
Enables users to select more than 1 item from the set. |
A multi-line text input for longer responses.
const paragraphField = {
type: 'paragraph',
name: 'description',
label: 'Description',
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
name |
string |
The name of the field. This will be used as the key in the values object when the form is submitted. |
label |
string |
The label of the field. This will be displayed to the user. |
helpText |
string undefined |
An optional help text that will be displayed below the field. |
required |
boolean undefined |
If true the field will be required and the user will not be able to submit the form without filling it in. Defaults to false. |
disabled |
boolean undefined |
If true the field will be disabled. Defaults to false. |
defaultValue |
ValueType undefined |
The default value of the field. |
scope |
SettingScopeType undefined |
This indicates whether the field (setting) is an app level or install level setting. App setting values can be used by any installation. undefined by default. |
placeholder |
string undefined |
Placeholder text for display before a value is present. |
lineHeight |
number undefined |
Sets the field height by number of lines. |
An input for numerical values.
const numberField = {
type: 'number',
name: 'tokens',
label: 'Token balance',
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
name |
string |
The name of the field. This will be used as the key in the values object when the form is submitted. |
label |
string |
The label of the field. This will be displayed to the user. |
helpText |
string undefined |
An optional help text that will be displayed below the field. |
required |
boolean undefined |
If true the field will be required and the user will not be able to submit the form without filling it in. Defaults to false. |
disabled |
boolean undefined |
If true the field will be disabled. Defaults to false. |
defaultValue |
ValueType undefined |
The default value of the field. |
scope |
SettingScopeType undefined |
This indicates whether the field (setting) is an app level or install level setting. App setting values can be used by any installation. undefined by default. |
A yes/no or true/false type input.
const booleanField = {
type: 'boolean',
name: 'enable',
label: 'Enable the event',
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
name |
string |
The name of the field. This will be used as the key in the values object when the form is submitted. |
label |
string |
The label of the field. This will be displayed to the user. |
helpText |
string undefined |
An optional help text that will be displayed below the field. |
disabled |
boolean undefined |
If true the field will be disabled. Defaults to false. |
defaultValue |
ValueType undefined |
The default value of the field. |
scope |
SettingScopeType undefined |
This indicates whether the field (setting) is an app level or install level setting. App setting values can be used by any installation. undefined by default. |
An image upload field.
const imageField = {
type: 'image', // This tells the form to expect an image
name: 'myImage',
label: 'Image goes here',
required: true,
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
name |
string |
The name of the field. This will be used as the key in the values object when the form is submitted. |
label |
string |
The label of the field. This will be displayed to the user. |
helpText |
string undefined |
An optional help text that will be displayed below the field. |
required |
boolean undefined |
If true the field will be required and the user will not be able to submit the form without filling it in. Defaults to false. |
disabled |
boolean undefined |
If true the field will be disabled. Defaults to false. |
scope |
SettingScopeType undefined |
This indicates whether the field (setting) is an app level or install level setting. App setting values can be used by any installation. undefined by default. |
placeholder |
string undefined |
Placeholder text for display before a value is present. |
isSecret |
boolean undefined |
Makes the form field secret. |
- The formats supported are PNG, JPEG, WEBP, and GIF.
- The maximum file size allowed is 20 MB.
- When uploading a WEBP image, it will be converted to JPEG. As such, the Reddit URL returned points to a JPEG image.
A collection of related fields that allows for better readability.
const groupField = {
type: 'group',
label: 'This is a group of input fields',
fields: [
{
type: 'paragraph',
name: 'description',
label: 'How would you describe what happened?',
},
{
type: 'number',
name: 'score',
label: 'How would you rate your meal on a scale from 1 to 10?',
},
],
};| Property | Supported types | Description |
|---|---|---|
type |
string |
The desired field type. |
label |
string |
The label of the group that will be displayed to the user. |
fields |
FormField[] |
The fields that will be displayed in the group. |
helpText |
string undefined |
An optional help text that will be displayed below the group. |
Below is a collection of common use cases and patterns.
Client-side approach:
import { showForm } from '@devvit/web/client';
// Get user data and show form with dynamic default values
const user = await reddit.getCurrentUser();
const result = await showForm({
form: {
fields: [
{
type: 'string',
name: 'username',
label: 'Username',
},
],
},
data: {
username: user?.username || ''
}
});
if (result) {
// Handle the form result
console.log(`Hello ${result.username}`);
}Server-side approach:
{
"forms": {
"dynamicForm": "/internal/form/dynamic-submit"
}
}<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
type DynamicFormRequest = { username: string };
// Endpoint that shows form with dynamic data
app.post('/internal/menu/show-dynamic-form', async (c) => {
const _input = await c.req.json<MenuItemRequest>();
const user = await reddit.getCurrentUser();
return c.json<UiResponse>({
showForm: {
name: 'dynamicForm',
form: {
fields: [
{
type: 'string',
name: 'username',
label: 'Username',
},
],
},
data: {
username: user?.username || ''
}
}
});
});
// Form submission handler
app.post('/internal/form/dynamic-submit', async (c) => {
const { username } = await c.req.json<DynamicFormRequest>();
return c.json<UiResponse>({
showToast: `Hello ${username}`
});
});import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
type DynamicFormRequest = { username: string };
// Endpoint that shows form with dynamic data
router.post<string, never, UiResponse, MenuItemRequest>("/internal/menu/show-dynamic-form", async (_req, res) => {
const user = await reddit.getCurrentUser();
res.json({
showForm: {
name: 'dynamicForm',
form: {
fields: [
{
type: 'string',
name: 'username',
label: 'Username',
},
],
},
data: {
username: user?.username || ''
}
}
});
});
// Form submission handler
router.post<string, never, UiResponse, DynamicFormRequest>("/internal/form/dynamic-submit", async (req, res) => {
const { username } = req.body;
res.json({
showToast: `Hello ${username}`
});
});Devvit.configure({ redditAPI: true, });
const myForm = Devvit.createForm(
(data) => {
return {
fields: [
{
type: 'string',
name: 'username',
label: 'Username',
defaultValue: data.username,
},
],
// Adding as const helps you get accurate types in the onSubmit function below
// This will only work if the function does not have any branching logic
} as const;
},
(event, context) => {
context.ui.showToast({
text: Hello ${event.values.username},
});
}
);
Devvit.addMenuItem({ label: 'Show a dynamic form', location: 'subreddit', onPress: async (_event, context) => { const user = await context.reddit.getCurrentUser(); const username = user?.username; context.ui.showForm(myForm, { username }); }, });
</TabItem>
</Tabs>
### Multi-step forms
<Tabs>
<TabItem value="web" label="Devvit Web">
**Client-side approach (Promise chaining):**
```ts title="client/index.ts"
import { showForm } from '@devvit/web/client';
async function multiStepForm() {
// Step 1: Get name
const step1Result = await showForm({
form: {
fields: [
{
type: 'string',
name: 'name',
label: "What's your name?",
required: true,
},
],
}
});
if (!step1Result) return; // User cancelled
// Step 2: Get food preference
const step2Result = await showForm({
form: {
fields: [
{
type: 'string',
name: 'food',
label: "What's your favorite food?",
required: true,
},
],
},
data: { name: step1Result.name } // Pass data from previous step
});
if (!step2Result) return; // User cancelled
// Step 3: Get drink preference
const step3Result = await showForm({
form: {
fields: [
{
type: 'string',
name: 'drink',
label: "What's your favorite drink?",
required: true,
},
],
},
data: {
name: step1Result.name,
food: step2Result.food
}
});
if (step3Result) {
// All steps completed - save or process data
const finalData = {
...step1Result,
...step2Result,
...step3Result
};
console.log(`Thanks ${finalData.name}! You like ${finalData.food} and ${finalData.drink}.`);
}
}
Server-side approach (Separate endpoints):
{
"forms": {
"step1Form": "/internal/form/step1-submit",
"step2Form": "/internal/form/step2-submit",
"step3Form": "/internal/form/step3-submit"
}
}<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { UiResponse } from '@devvit/web/shared';
type Step1FormRequest = { name: string };
type Step2FormRequest = { name: string; food: string };
type Step3FormRequest = { name: string; food: string; drink: string };
// Step 1: Name form
app.post('/internal/form/step1-submit', async (c) => {
const { name } = await c.req.json<Step1FormRequest>();
return c.json<UiResponse>({
showForm: {
name: 'step2Form',
form: {
fields: [
{
type: 'string',
name: 'food',
label: "What's your favorite food?",
required: true,
},
],
},
data: { name } // Pass data to next step
}
});
});
// Step 2: Food form
app.post('/internal/form/step2-submit', async (c) => {
const { name, food } = await c.req.json<Step2FormRequest>();
return c.json<UiResponse>({
showForm: {
name: 'step3Form',
form: {
fields: [
{
type: 'string',
name: 'drink',
label: "What's your favorite drink?",
required: true,
},
],
},
data: { name, food } // Pass accumulated data
}
});
});
// Step 3: Final form
app.post('/internal/form/step3-submit', async (c) => {
const { name, food, drink } = await c.req.json<Step3FormRequest>();
return c.json<UiResponse>({
showToast: `Thanks ${name}! You like ${food} and ${drink}.`
});
});import type { UiResponse } from '@devvit/web/shared';
type Step1FormRequest = { name: string };
type Step2FormRequest = { name: string; food: string };
type Step3FormRequest = { name: string; food: string; drink: string };
// Step 1: Name form
router.post<string, never, UiResponse, Step1FormRequest>("/internal/form/step1-submit", async (req, res) => {
const { name } = req.body;
res.json({
showForm: {
name: 'step2Form',
form: {
fields: [
{
type: 'string',
name: 'food',
label: "What's your favorite food?",
required: true,
},
],
},
data: { name } // Pass data to next step
}
});
});
// Step 2: Food form
router.post<string, never, UiResponse, Step2FormRequest>("/internal/form/step2-submit", async (req, res) => {
const { name, food } = req.body;
res.json({
showForm: {
name: 'step3Form',
form: {
fields: [
{
type: 'string',
name: 'drink',
label: "What's your favorite drink?",
required: true,
},
],
},
data: { name, food } // Pass accumulated data
}
});
});
// Step 3: Final form
router.post<string, never, UiResponse, Step3FormRequest>("/internal/form/step3-submit", async (req, res) => {
const { name, food, drink } = req.body;
res.json({
showToast: `Thanks ${name}! You like ${food} and ${drink}.`
});
});Devvit.configure({ redditAPI: true, });
// addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. View the announcement below this example. Devvit.addCustomPostType({ name: 'Multi-step Form', render: (context) => { const [name, setName] = useState(''); const [food, setFood] = useState(''); const [drink, setDrink] = useState('');
const form3 = useForm(
{
fields: [
{
type: 'string',
name: 'drink',
label: "What's your favorite drink?",
required: true,
},
],
},
(values) => {
setDrink(values.drink);
}
);
const form2 = useForm(
{
fields: [
{
type: 'string',
name: 'food',
label: "What's your favorite food?",
required: true,
},
],
},
(values) => {
setFood(values.food);
context.ui.showForm(form3);
}
);
const form1 = useForm(
{
fields: [
{
type: 'string',
name: 'name',
label: "What's your name?",
required: true,
},
],
},
(values) => {
setName(values.name);
context.ui.showForm(form2);
}
);
function restart() {
setName('');
setFood('');
setDrink('');
context.ui.showForm(form1);
}
const isAnswered = name && food && drink;
return (
<vstack height="100%" alignment="center middle" gap="none">
{isAnswered && (
<>
<text>Name: {name}</text>
<text>Favorite food: {food}</text>
<text>Favorite drink: {drink}</text>
<spacer size="large" />
<button onPress={restart}>Restart</button>
</>
)}
{!isAnswered && <button onPress={restart}>Take questionnaire</button>}
</vstack>
);
},
});
[View `addCustomPostType` deprecation announcement.](https://www.reddit.com/r/Devvit/comments/1r3xcm2/devvit_web_and_the_future_of_devvit/)
</TabItem>
</Tabs>
### One of everything
This example includes one of each of the [supported field types](#supported-fields-types).
<Tabs>
<TabItem value="web" label="Devvit Web">
**Client-side approach:**
```ts title="client/index.ts"
import { showForm } from '@devvit/web/client';
const result = await showForm({
form: {
title: 'My favorites',
description: 'Tell us about your favorite food!',
fields: [
{
type: 'string',
name: 'food',
label: 'What is your favorite food?',
helpText: 'Must be edible',
required: true,
},
{
label: 'About that food',
type: 'group',
fields: [
{
type: 'number',
name: 'times',
label: 'How many times a week do you eat it?',
defaultValue: 1,
},
{
type: 'paragraph',
name: 'what',
label: 'What makes it your favorite?',
},
{
type: 'select',
name: 'healthy',
label: 'Is it healthy?',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
{ label: 'Maybe', value: 'maybe' },
],
defaultValue: ['maybe'],
},
],
},
{
type: 'boolean',
name: 'again',
label: 'Can we ask again?',
},
],
acceptLabel: 'Submit',
cancelLabel: 'Cancel',
}
});
if (result) {
console.log('Form values:', result);
// Handle form submission
}
Server-side approach:
{
"forms": {
"everythingForm": "/internal/form/everything-submit"
}
}<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
type EverythingFormRequest = {
food: string;
times?: number;
what?: string;
healthy?: string[];
again?: boolean;
};
app.post('/internal/form/everything-submit', async (c) => {
const formValues = await c.req.json<EverythingFormRequest>();
console.log('Form values:', formValues);
return c.json<UiResponse>({
showToast: 'Thanks!'
});
});
// Example showing the form
app.post('/internal/menu/show-everything-form', async (c) => {
const _input = await c.req.json<MenuItemRequest>();
return c.json<UiResponse>({
showForm: {
name: 'everythingForm',
form: {
title: 'My favorites',
description: 'Tell us about your favorite food!',
fields: [
{
type: 'string',
name: 'food',
label: 'What is your favorite food?',
helpText: 'Must be edible',
required: true,
},
{
label: 'About that food',
type: 'group',
fields: [
{
type: 'number',
name: 'times',
label: 'How many times a week do you eat it?',
defaultValue: 1,
},
{
type: 'paragraph',
name: 'what',
label: 'What makes it your favorite?',
},
{
type: 'select',
name: 'healthy',
label: 'Is it healthy?',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
{ label: 'Maybe', value: 'maybe' },
],
defaultValue: ['maybe'],
},
],
},
{
type: 'boolean',
name: 'again',
label: 'Can we ask again?',
},
],
acceptLabel: 'Submit',
cancelLabel: 'Cancel',
}
}
});
});import type { MenuItemRequest, UiResponse } from '@devvit/web/shared';
type EverythingFormRequest = {
food: string;
times?: number;
what?: string;
healthy?: string[];
again?: boolean;
};
router.post<string, never, UiResponse, EverythingFormRequest>("/internal/form/everything-submit", async (req, res) => {
console.log('Form values:', req.body);
res.json({
showToast: 'Thanks!'
});
});
// Example showing the form
router.post<string, never, UiResponse, MenuItemRequest>("/internal/menu/show-everything-form", async (_req, res) => {
res.json({
showForm: {
name: 'everythingForm',
form: {
title: 'My favorites',
description: 'Tell us about your favorite food!',
fields: [
{
type: 'string',
name: 'food',
label: 'What is your favorite food?',
helpText: 'Must be edible',
required: true,
},
{
label: 'About that food',
type: 'group',
fields: [
{
type: 'number',
name: 'times',
label: 'How many times a week do you eat it?',
defaultValue: 1,
},
{
type: 'paragraph',
name: 'what',
label: 'What makes it your favorite?',
},
{
type: 'select',
name: 'healthy',
label: 'Is it healthy?',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
{ label: 'Maybe', value: 'maybe' },
],
defaultValue: ['maybe'],
},
],
},
{
type: 'boolean',
name: 'again',
label: 'Can we ask again?',
},
],
acceptLabel: 'Submit',
cancelLabel: 'Cancel',
}
}
});
});const exampleForm = Devvit.createForm( { title: 'My favorites', description: 'Tell us about your favorite food!', fields: [ { type: 'string', name: 'food', label: 'What is your favorite food?', helpText: 'Must be edible', required: true, }, { label: 'About that food', type: 'group', fields: [ { type: 'number', name: 'times', label: 'How many times a week do you eat it?', defaultValue: 1, }, { type: 'paragraph', name: 'what', label: 'What makes it your favorite?', }, { type: 'select', name: 'healthy', label: 'Is it healthy?', options: [ { label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }, { label: 'Maybe', value: 'maybe' }, ], defaultValue: ['maybe'], }, ], }, { type: 'boolean', name: 'again', label: 'Can we ask again?', }, ], acceptLabel: 'Submit', cancelLabel: 'Cancel', }, (event, context) => { console.log(event.values); context.ui.showToast('Thanks!'); } );
Devvit.addMenuItem({ location: 'subreddit', label: 'One of everything form', onPress: (_event, context) => { context.ui.showForm(exampleForm); }, });
</TabItem>
</Tabs>
### Image uploads
<Tabs>
<TabItem value="web" label="Devvit Web">
**Client-side approach:**
```ts title="client/index.ts"
import { showForm } from '@devvit/web/client';
const result = await showForm({
form: {
title: 'Upload an image!',
fields: [
{
name: 'myImage',
type: 'image', // This tells the form to expect an image
label: 'Image goes here',
required: true,
},
],
}
});
if (result) {
const { myImage } = result;
// returns an i.redd.it URL
console.log('Image uploaded:', myImage);
// Process the image further
await fetch('/api/process-image', {
method: 'POST',
body: JSON.stringify({ imageUrl: myImage })
});
}
Server-side approach:
{
"forms": {
"imageForm": "/internal/form/image-submit"
}
}<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { UiResponse } from '@devvit/web/shared';
type ImageFormRequest = { myImage: string };
app.post('/internal/form/image-submit', async (c) => {
const { myImage } = await c.req.json<ImageFormRequest>();
// Use the mediaUrl to store in redis and display it in an <image> block, or send to external service to modify
console.log('Image uploaded:', myImage);
return c.json<UiResponse>({
showToast: 'Image uploaded successfully!'
});
});import type { UiResponse } from '@devvit/web/shared';
type ImageFormRequest = { myImage: string };
router.post<string, never, UiResponse, ImageFormRequest>("/internal/form/image-submit", async (req, res) => {
const { myImage } = req.body;
// Use the mediaUrl to store in redis and display it in an <image> block, or send to external service to modify
console.log('Image uploaded:', myImage);
res.json({
showToast: 'Image uploaded successfully!'
});
});const form = Devvit.createForm( { title: 'Upload an image!', fields: [ { name: 'myImage', type: 'image', // This tells the form to expect an image label: 'Image goes here', required: true, }, ], }, (event, context) => { // returns an i.redd.it URL const imageUrl = event.values.myImage; } );
</TabItem>
</Tabs>






