Customizing dApp Submission with FormKit: Building a Multi-Step Form for API3
Creating a smooth, on-brand experience for dApp submissions can be tough, especially when the form needs to look and feel like a seamless part of your site. API3’s Ecosystem platform required a multi-step submission form that matched its design language and handled everything from conditional steps to custom validations. Here’s how we used FormKit to make it happen—and the challenges we solved along the way.
Why We Chose FormKit
When we set out to build API3’s dApp submission form, we needed a toolkit that could handle multi-step navigation, complex validation, and high customizability. FormKit turned out to be exactly what we needed. Its built-in multi-step support provided a strong foundation, and its flexibility meant we could style and adapt it to match API3’s unique design without losing functionality.
Structuring the Multi-Step Experience
Our form had to walk users through a series of distinct steps, making the experience straightforward and intuitive. Here’s the breakdown of each step:
Basic Info: Collects essentials like the dApp name, logo, and description.
Images: Gathers cover images and screenshots.
Tags: Lets users select categories and blockchain networks.
Proxy Information: Only appears if the dApp is a data feed.
Links: Collects social media links and contact information.
Here’s a snippet of how we structured the main FormKit component to flow through each of these steps:
<FormKit
type="multi-step"
tab-style="progress"
:allow-incomplete="ui.isDev"
:hide-progress-labels="true"
valid-step-icon=""
>
<FormKit type="step" name="content">
<ContentStep :dappForm="dappForm" />
</FormKit>
<!-- Additional steps go here -->
</FormKit>
Custom FormKit Configuration
One of the standout aspects of this project was how we customized FormKit using plugins. By extending its default capabilities, we created a polished, intuitive experience that feels native to API3’s ecosystem. Here are the plugins we implemented:
const plugins = [
createMultiStepPlugin(),
createAutoHeightTextareaPlugin,
createAutoAnimatePlugin(autoAnimate.config, autoAnimate.targets),
addAsteriskPlugin,
addPrefixIconPlugin,
addSuffixHelpTooltipPlugin,
];
These custom plugins added everything from contextual tooltips to automatic icons and required field indicators, making the form both visually cohesive and highly functional.
Help Tooltips
To offer guidance on specific fields, we added help tooltips using floating-vue
. These appear when users hover over a field, providing contextual information without cluttering the interface.
function addSuffixHelpTooltipPlugin(node) {
if (!node.props.help) {
node.props.suffixIcon = null;
return;
}
let tooltip;
node.context.handlers.mouseEnter = (e) => {
const el = e.target;
tooltip = createTooltip(el, {
triggers: [],
content: node.props.help,
});
tooltip.show();
};
node.context.handlers.mouseLeave = (e) => {
tooltip.hide();
setTimeout(() => {
destroyTooltip(e.target);
}, 400);
};
}
These tooltips make it easy for users to understand field requirements or limitations, especially helpful in a multi-step form with different types of information.
Automatic Input Icons
To improve user recognition, we configured automatic icons for common input types, such as email and password fields. These icons appear automatically, based on field type, adding visual consistency to the form.
function addPrefixIconPlugin(node) {
node.on("created", () => {
const typesToApply = ["email", "password", "text", "url", "search"];
if (node.props.prefixIcon) return;
if (!typesToApply.includes(node.props.type)) return;
node.props.definition.schema = (extensions) => {
const localExtensions = {
...extensions,
prefix: {
$el: "label",
attrs: {
class: "formkit-prefix-icon formkit-icon",
innerHTML: icons[node.props.type],
},
},
};
return originalSchema(localExtensions);
};
});
}
This addition boosts usability by making fields instantly recognizable and guiding users through the form intuitively.
Required Field Indicators
To clearly signal which fields are required, we implemented an asterisk indicator plugin. This plugin adds a subtle but clear visual cue to help users understand what needs to be filled out.
function addAsteriskPlugin(node) {
const legends = ["checkbox_multi", "radio_multi", "repeater", "transferlist"];
if (["button", "submit", "hidden", "group"].includes(node.props.type)) return;
node.on("created", () => {
const legendOrLabel = legends.includes(
`${node.props.type}${node.props.options ? "_multi" : ""}`
) ? "legend" : "label";
node.props.definition.schema = (sectionsSchema = {}) => {
sectionsSchema[legendOrLabel] = {
children: [
{
$el: "span",
if: "$state.required",
attrs: {
class: "text-action-error-500 required-star",
},
children: ["*"],
},
"$label",
],
};
return schemaFn(sectionsSchema);
};
});
}
This small visual detail goes a long way in making the form clearer and more user-friendly, ensuring that users know exactly what’s required.
Customizing the Design
Matching API3’s design language meant modifying FormKit’s default styles to feel cohesive with the rest of the Ecosystem site. With a mix of CSS variables and custom components, we brought API3’s visual language into the form itself.
:root {
--formkit-theme: api3;
--fk-color-primary: var(--ink);
--fk-border-width: var(--line-width);
--fk-padding-input: var(--space-s);
}
Enhanced Validation and File Handling
We needed to ensure image uploads followed specific guidelines, so we created custom validation rules to check image dimensions and file size. This helped us keep user submissions clean and consistent.
const imageRatio = async function (node) {
if (!node.value) return true;
const imageRatios = await Promise.all(
node.value.map(async (file) => {
// Calculate the image ratio
const ratio = image.naturalWidth / image.naturalHeight;
return ratio;
})
);
return imageRatios.every((ratio) => {
const lowerBound = 16 / 6.5;
const upperBound = 16 / 5.5;
return ratio >= lowerBound && ratio <= upperBound;
});
};
This rule automatically validates that images have the right aspect ratio, ensuring that uploaded assets maintain a uniform look across the platform.
Dynamic Form Behavior
To keep things user-friendly, we added conditional rendering based on user selections. For example, the “Proxy Information” step only appears for data feed dApps, making the form experience customized and relevant.
<FormKit
type="step"
name="proxy"
v-if="dappForm.productType === 'datafeed'"
:next-attrs="nextClasses"
:previous-attrs="prevClasses"
>
<ProxyStep :dappForm="dappForm" :feedNameOptions="feedNameOptions" />
</FormKit>
This approach means that users only see what’s relevant to them, helping them move through the form faster without getting bogged down in irrelevant fields.
User Experience Enhancements
To make the form feel as seamless and engaging as possible, we added several UX touches:
Progress Tracking: Custom progress indicators let users know exactly where they are in the process.
Automatic Data Persistence: Using
useStorage
, we stored form data locally so that users don’t lose progress if they navigate away accidentally.
const dappForm = useStorage("dapp-form", {});
Clear Validation and Help Text: We customized validation messages and added helpful hints to guide users through each step.
Mobile-Responsive Design: The form adjusts itself based on device size, so the experience remains intuitive on mobile and desktop alike.
Wrapping Up
Building a custom multi-step form with FormKit allowed us to create a submission process that feels both functional and true to API3’s brand. Thanks to FormKit’s multi-step structure and flexibility, we were able to integrate custom validation, adaptive styling, and dynamic field behavior—all while keeping the codebase clean and manageable.
Key Takeaways:
Multi-Step Simplicity: FormKit’s multi-step support provided a strong foundation for our complex form.
Flexible Validation: Custom validation rules made it easy to meet API3’s unique requirements.
Seamless Styling: CSS variables allowed us to bring the design language of API3 into every part of the form.
Data Persistence: Leveraging local storage prevented users from losing data, making the form more forgiving and user-friendly.
The result? A form that doesn’t just look and feel like a natural extension of the API3 ecosystem but also makes it easy and intuitive for users to submit their dApps. FormKit’s adaptability helped us build a form that’s as robust as it is user-friendly.