Sooner or later, most Magento 2 stores need to collect information the default admin doesn't ask for — internal notes, supplier details, custom requests. Building a form inside the admin panel is the clean way to do it, and Magento's module system gives you everything required: a menu entry, an ACL resource, a controller, and a template. In this guide we'll build a working admin form from scratch, step by step. (If it's customer-facing forms you're after — contact forms, surveys, quote requests — you can skip the code entirely with the Magento 2 Custom Form extension; more on that at the end.)
Step 1: Create the Module
Create the module directory structure at app/code/Mageants/CustomForm — replace Mageants and CustomForm with your own vendor and module names throughout.
Create the registration.php file:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Mageants_CustomForm',
__DIR__
);
|
Create the module.xml file:
<!-- app/code/Mageants/CustomForm/etc/module.xml -->
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Mageants_CustomForm" setup_version="1.0.0"/>
</config>
|
Step 2: Define the Admin Menu, ACL, and Route
Create the menu.xml file:
This adds the menu item under Content that links to our form page. Note the action customform/index/index — route, controller folder, action — which must match the route and controller we create next:
<!-- app/code/Mageants/CustomForm/etc/adminhtml/menu.xml -->
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<menu>
<add id="Mageants_CustomForm::custom_form" title="Custom Form" module="Mageants_CustomForm" sortOrder="10" parent="Magento_Backend::content" action="customform/index/index" resource="Mageants_CustomForm::custom_form"/>
</menu>
</config>
|
Create the acl.xml file:
<!-- app/code/Mageants/CustomForm/etc/acl.xml -->
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resource id="Magento_Backend::admin">
<resource id="Mageants_CustomForm::custom_form" title="Custom Form" sortOrder="10"/>
</resource>
</resources>
</acl>
</config>
|
Create the admin router configuration:
The frontName here is the "customform" part of the URL and of the menu action above — they have to match, and by convention it's lowercase:
<!-- app/code/Mageants/CustomForm/etc/adminhtml/routes.xml -->
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="admin">
<route id="customform" frontName="customform">
<module name="Mageants_CustomForm"/>
</route>
</router>
</config>
|
Step 3: Create the Page Controller
This is the controller the menu item opens — it renders the admin page that our layout and template define. Create app/code/Mageants/CustomForm/Controller/Adminhtml/Index/Index.php:
<?php
namespace Mageants\CustomForm\Controller\Adminhtml\Index;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Index extends Action
{
/**
* ACL resource that protects this page
*/
const ADMIN_RESOURCE = 'Mageants_CustomForm::custom_form';
/**
* @var PageFactory
*/
protected $resultPageFactory;
public function __construct(
Context $context,
PageFactory $resultPageFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
public function execute()
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->prepend(__('Custom Form'));
return $resultPage;
}
}
|
Step 4: Create the Layout and Form Template
Create layout/customform_index_index.xml:
The file name follows the route: customform (route) + index (controller folder) + index (action). It attaches our template to the page content:
<!-- app/code/Mageants/CustomForm/view/adminhtml/layout/customform_index_index.xml -->
<?xml version="1.0" ?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Magento\Backend\Block\Template" name="custom_form_block" template="Mageants_CustomForm::form.phtml"/>
</referenceContainer>
</body>
</page>
|
Create templates/form.phtml:
Two details matter here: the form action points at our save controller (customform/index/save), and the form key is included — Magento rejects any admin POST without it:
<!-- app/code/Mageants/CustomForm/view/adminhtml/templates/form.phtml -->
<div class="custom-form">
<form action="<?= $block->getUrl('customform/index/save') ?>" method="post">
<?= $block->getBlockHtml('formkey') ?>
<div class="admin__field">
<label class="admin__field-label" for="name"><?= __('Name') ?></label>
<input class="admin__control-text" type="text" name="name" id="name" required>
</div>
<div class="admin__field">
<label class="admin__field-label" for="email"><?= __('Email') ?></label>
<input class="admin__control-text" type="email" name="email" id="email" required>
</div>
<div class="admin__field">
<button class="action-primary" type="submit"><?= __('Submit') ?></button>
</div>
</form>
</div>
|
Step 5: Handle the Form Submission
Create app/code/Mageants/CustomForm/Controller/Adminhtml/Index/Save.php. This example just confirms the submission and redirects back — in a real module you'd validate and persist the data here:
<?php
namespace Mageants\CustomForm\Controller\Adminhtml\Index;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\Controller\ResultFactory;
class Save extends Action
{
/**
* ACL resource that protects this action
*/
const ADMIN_RESOURCE = 'Mageants_CustomForm::custom_form';
public function __construct(
Context $context
) {
parent::__construct($context);
}
public function execute()
{
$postData = $this->getRequest()->getPostValue();
if ($postData) {
$this->messageManager->addSuccessMessage(__('Form submitted successfully.'));
} else {
$this->messageManager->addErrorMessage(__('Unable to submit form. Please try again.'));
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$resultRedirect->setUrl($this->_redirect->getRefererUrl());
return $resultRedirect;
}
}
|
Step 6: Enable the Module and Test
Run these commands from the Magento root, then log in to the admin and open Content > Custom Form:
php bin/magento module:enable Mageants_CustomForm php bin/magento setup:upgrade php bin/magento setup:di:compile php bin/magento cache:flush |
Submitting the form should redirect you back with the "Form submitted successfully." message.
Conclusion
Creating a custom form in the Magento 2 admin panel comes down to five pieces working together: a module, a menu entry with its ACL resource and route, a page controller, a layout with the form template, and a save controller to handle the submission. With this skeleton in place, extending it — more fields, validation, saving to a database table — is straightforward.
If what you actually need are forms for your store visitors and customers — contact forms, surveys, feedback, quote requests — there's no reason to build them by hand. The MageAnts Custom Form extension for Magento 2 gives you a drag-and-drop form builder in the admin: any field types, embedded on any page, with submissions collected and managed from a straightforward backend interface — no module development required.