Natural Language Form with Custom Input Elements

An experimental form that uses natural language instead of the usual form display. Values are entered using custom input elements.

Natural Language Form

There is something really interesting about Natural Language UIs so we wanted to play around with NL forms and custom form elements. The idea is to turn a classic form into one that uses natural language to obtain information from the user. For that we’ll construct a sentence where some words and parts are select elements and text inputs. We’ll then transform the select elements so that we can customize them.

Natural language forms are quite suitable for some UI cases, not for all, of course. They have a couple of limitations but they can also be more engaging for users if used in the right place and in the right way. Anyway, we wanted to give this concept a try and we’ll show you how we did it.

Please note: this only works as intended in browsers that support the respective CSS properties.

The Markup

The HTML will be composed of a form with a sentence and some select and text input elements:

<form id="nl-form" class="nl-form">
	I feel like eating 
	<select>
		<option value="1" selected>any food</option>
		<option value="2">Indian</option>
		<option value="3">French</option>
		<option value="4">Japanese</option>
		<option value="2">Italian</option>
	</select>
	<br />in a
	<select>
		<option value="1" selected>standard</option>
		<option value="2">fancy</option>
		<option value="3">hip</option>
		<option value="4">traditional</option>
		<option value="2">fine</option>
	</select>
	restaurant
	<select>
		<option value="1" selected>anytime</option>
	 	<option value="1">at 7 p.m.</option>
	 	<option value="2">at 8 p.m.</option>
	 	<option value="3">at 9 p.m.</option>
	</select>
	in <input type="text" value="" placeholder="any city" data-subline="For example: <em>Los Angeles</em> or <em>New York</em>"/>
	<div class="nl-submit-wrap">
		<button class="nl-submit" type="submit">Find a restaurant</button>
	</div>
	<div class="nl-overlay"></div>
</form>

We want to transform the select elements to a custom drop-down list, like this:

<div class="nl-field nl-dd">
	<a class="nl-field-toggle">any food</a>
	<ul>
		<li class="nl-dd-checked">any food</li>
		<li>Indian</li>
		<li>French</li>
		<li>Japanese</li>
		<li>Italian</li>
	</ul>
</div>

NaturalLanguageForm01
The toggle is the visual part in the sentence. We’ll hide the list and when we click on the toggle we want the list to appear with a nice transition. Once we click on one of the list items, we’ll replace the toggle value with the newly selected one.

The text input will also be transformed into a similar element:

<div class="nl-field nl-ti-text">
	<a class="nl-field-toggle">any city</a>
	<ul>
		<li class="nl-ti-input">
			<input type="text" value="" placeholder="any city" />
			<button class="nl-field-go">Go</button>
		</li>
		<li class="nl-ti-example">For example: <em>Los Angeles</em> or <em>New York</em></li>
	</ul>
</div>

NaturalLanguageForm02

The CSS

Note that the CSS will not contain any vendor prefixes, but you will find them in the files. Some proprietary browser rules will be listed, though.

Let’s first add some icons (checkmark and arrow):

@font-face {
	font-family: 'nlicons';
	src:url('../fonts/nlicons/nlicons.eot');
	src:url('../fonts/nlicons/nlicons.eot?#iefix') format('embedded-opentype'),
		url('../fonts/nlicons/nlicons.woff') format('woff'),
		url('../fonts/nlicons/nlicons.ttf') format('truetype'),
		url('../fonts/nlicons/nlicons.svg#nlicons') format('svg');
	font-weight: normal;
	font-style: normal;
}

The form will have some general styles:

/* general style for the form */
.nl-form {
	width: 100%;
	margin: 0.3em auto 0 auto;
	font-size: 4em;
	line-height: 1.5;
}

Let’s remove the default styling for all the lists:

.nl-form ul {
	list-style: none;
	margin: 0;
	padding: 0;
}

And we’ll also normalize the form elements and make them look like everything else:

.nl-form input,
.nl-form select,
.nl-form button {
	border: none;
	background: transparent;
	font-family: inherit;
	font-size: inherit;
	color: inherit;
	font-weight: inherit;
	line-height: inherit;
	display: inline-block;
	padding: 0;
	margin: 0;
}

We don’t want any outline on a focussed element:

.nl-form input:focus {
	outline: none;
}

Now, let’s take care of the custom field element. Remember, this is the custom class that we give to the transformed drop-down list and the text input element:

.nl-field {
	display: inline-block;
	position: relative;
}

Using inline-block will keep the element in the flow of our sentence.

When we click on the toggle, we want the whole thing to open. For that we also need to set a high z-index to the custom field:

.nl-field.nl-field-open {
	z-index: 10000;
}

The toggle itself, which is the visible part in the sentence, will have the following style:

.nl-field-toggle,
.nl-form input,
.nl-form select  {
	line-height: inherit;
	display: inline-block;
	color: #b14943;
	cursor: pointer;
	border-bottom: 1px dashed #b14943;
}

The same style we’ll apply to the default form elements if JS is disabled.

The unordered list will be hidden and positioned absolutely. We will use the visibility transition trick to be able to transition the opacity properly. The trick is about setting a delay for the visibility transition by default (when it disappears) and no delay when the .nl-field-open class is applied to its parent (when it appears):

/* drop-down list / text element */
.nl-field ul {
	position: absolute;
	visibility: hidden;
	background: #76C3BD;
	left: -0.5em;
	top: 50%;
	font-size: 80%;
	opacity: 0;
	transform: translateY(-40%) scale(0.9);
	transition: visibility 0s 0.3s, opacity 0.3s, transform 0.3s;
}

.nl-field.nl-field-open ul {
	visibility: visible;
	opacity: 1;
	transform: translateY(-50%) scale(1);
	transition: visibility 0s 0s, opacity 0.3s, transform 0.3s;
}

The style for the list elements and specifically for the ones of the drop-down are the following:

.nl-field ul li {
	color: #fff;
	position: relative;
}

.nl-dd ul li {
	padding: 0 1.5em 0 0.5em;
	cursor: pointer;
	white-space: nowrap;
}

.nl-dd ul li.nl-dd-checked {
	color: #478982;
}

.no-touch .nl-dd ul li:hover {
	background: rgba(0,0,0,0.05);
}

.no-touch .nl-dd ul li:hover:active {
	color: #478982;
}

Let’s define the icons for some of our elements:

/* icons for some elements */
.nl-dd ul li.nl-dd-checked:before,
.nl-submit:before,
.nl-field-go:before {
	font-family: 'nlicons';
	speak: none;
	font-style: normal;
	font-weight: normal;
	font-variant: normal;
	text-transform: none;
	-webkit-font-smoothing: antialiased;
}

.nl-dd ul li.nl-dd-checked:before {
	content: "e000";
	position: absolute;
	right: 1em;
	font-size: 50%;
	line-height: 3;
}

The text input replacement field will have a minimum width:

.nl-ti-text ul {
	min-width: 8em;
}

The text input field, which we are still using but wrapped in our overlay field structure, will have the following style:

.nl-ti-text ul li.nl-ti-input input {
	width: 100%;
	padding: 0.2em 2em 0.2em 0.5em;
	border-bottom: none;
	color: #fff;
}

The arrow button will be positioned absolutely and we’ll out it on the right side:

.nl-form .nl-field-go {
	position: absolute;
	right: 0;
	top: 0;
	height: 100%;
	cursor: pointer;
	background: rgba(0,0,0,0.1);
	width: 1.8em;
	text-align: center;
	color: transparent;
}

The icon itself will have the following style:

.nl-field-go:before {
	content: "e001";
	font-size: 75%;
	color: #fff;
	width: 100%;
	line-height: 2.5;
	display: block;
}

Let’s change the placeholder text colors to fit into the color scheme:

/* custom placeholder color */
input::-webkit-input-placeholder {
	color: rgba(255,255,255,0.8);
}

input:active::-webkit-input-placeholder ,
input:focus::-webkit-input-placeholder {
	color: rgba(255,255,255,0.2);
}

input::-moz-placeholder {
	color: rgba(255,255,255,0.8);
}

input:active::-moz-placeholder,
input:focus::-moz-placeholder {
	color: rgba(255,255,255,0.2);
}

input:-ms-input-placeholder {  
	color: rgba(255,255,255,0.8);
}

input:active::-ms-input-placeholder ,
input:focus::-ms-input-placeholder {
	color: rgba(255,255,255,0.2);
}

The description text item will have a smaller font size:

/* example field below text input */
.nl-ti-text ul li.nl-ti-example {
	font-size: 40%;
	font-style: italic;
	font-weight: 400;
	padding: 0.4em 1em;
	color: rgba(0,0,0,0.2);
	border-top: 1px dashed rgba(255,255,255,0.7);
}

.nl-ti-text ul li.nl-ti-example em {
	color: #fff
}

Let’s style the submit button and the icon:

.nl-submit-wrap {
	margin-top: 0.4em;
}

.nl-form .nl-submit {
	line-height: 3;
	text-transform: uppercase;
	cursor: pointer;
	position: relative;
	background: #76C3BD;
	color: #fff;
	padding: 0 1em 0 0;
	font-size: 40%;
	font-weight: bold;
	letter-spacing: 3px;
}

.nl-submit:before {
	content: "e001";
	color: #fff;
	float: left;
	padding: 0 0.7em;
	margin: 0 0.8em 0 0;
	background: #69B1A9;
}

.no-touch .nl-form .nl-submit:hover,
.no-touch .nl-form .nl-submit:active {
	background: #69B1A9;
}

.no-touch .nl-form .nl-submit:hover:before {
	background: #58a199;
}

The overlay will be a fixed element and we’ll show it when one of the field elements are opened using the general sibling selector:

.nl-overlay {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: rgba(0,0,0,0.5);
	opacity: 0;
	z-index: 9999;
	visibility: hidden;
	transition: visibility 0s 0.3s, opacity 0.3s;
}

.nl-field.nl-field-open ~ .nl-overlay {
	opacity: 1;
	visibility: visible;
	-webkit-transition-delay: 0s;
	-moz-transition-delay: 0s;
	transition-delay: 0s;
}

Last, but not least, let’s add some media queries for adjusting the size on smaller devices:

@media screen and (max-width: 45em) {
	.nl-form {
		font-size: 3em;
	}
}

@media screen and (max-width: 25em) {
	.nl-form {
		font-size: 2em;
	}
}

And that’s all the style. Now, let’s have a look at the JavaScript.

The JavaScript

Let’s start by defining the object representing the form element:

function NLForm( el ) {	
	// the form element
	this.el = el;
	// the overlay
	this.overlay = this.el.querySelector( '.nl-overlay' );
	// array with all the possible custom fields
	this.fields = [];
	// counter for each custom field
	this.fldOpen = -1;
	this._init();
}

We will start by creating some custom structure by replacing all the select and input elements inside the form. We will define a object that represents each one of these custom fields – NLField.

NLForm.prototype = {
	_init : function() {
		var self = this;
		Array.prototype.slice.call( this.el.querySelectorAll( 'select' ) ).forEach( function( el, i ) {
			self.fldOpen++;
			self.fields.push( new NLField( self, el, 'dropdown', self.fldOpen ) );
		} );
		Array.prototype.slice.call( this.el.querySelectorAll( 'input' ) ).forEach( function( el, i ) {
			self.fldOpen++;
			self.fields.push( new NLField( self, el, 'input', self.fldOpen ) );
		} );
	},
	...
}

function NLField( form, el, type, idx ) {
	this.form = form;
	// the original HTML element
	this.elOriginal = el;
	this.pos = idx;
	this.type = type;
	this._create();
	this._initEvents();
}
NLField.prototype = {
	_create : function() {
		if( this.type === 'dropdown' ) {
			this._createDropDown();	
		}
		else if( this.type === 'input' ) {
			this._createInput();	
		}
	},
	...
}

The structure will be different depending if it’s a select box or an input field.

NLField.prototype = {
	...
	_createDropDown : function() {
		var self = this;
		this.fld = document.createElement( 'div' );
		this.fld.className = 'nl-field nl-dd';
		this.toggle = document.createElement( 'a' );
		this.toggle.innerHTML = this.elOriginal.options[ this.elOriginal.selectedIndex ].innerHTML;
		this.toggle.className = 'nl-field-toggle';
		this.optionsList = document.createElement( 'ul' );
		var ihtml = '';
		Array.prototype.slice.call( this.elOriginal.querySelectorAll( 'option' ) ).forEach( function( el, i ) {
			ihtml += self.elOriginal.selectedIndex === i ? '
  • ' + el.innerHTML + '
  • ' : '
  • ' + el.innerHTML + '
  • '; // selected index value if( self.elOriginal.selectedIndex === i ) { self.selectedIdx = i; } } ); this.optionsList.innerHTML = ihtml; this.fld.appendChild( this.toggle ); this.fld.appendChild( this.optionsList ); this.elOriginal.parentNode.insertBefore( this.fld, this.elOriginal ); this.elOriginal.style.display = 'none'; }, _createInput : function() { var self = this; this.fld = document.createElement( 'div' ); this.fld.className = 'nl-field nl-ti-text'; this.toggle = document.createElement( 'a' ); this.toggle.innerHTML = this.elOriginal.placeholder; this.toggle.className = 'nl-field-toggle'; this.optionsList = document.createElement( 'ul' ); this.getinput = document.createElement( 'input' ); this.getinput.setAttribute( 'type', 'text' ); this.getinput.placeholder = this.elOriginal.placeholder; this.getinputWrapper = document.createElement( 'li' ); this.getinputWrapper.className = 'nl-ti-input'; this.inputsubmit = document.createElement( 'button' ); this.inputsubmit.className = 'nl-field-go'; this.inputsubmit.innerHTML = 'Go'; this.getinputWrapper.appendChild( this.getinput ); this.getinputWrapper.appendChild( this.inputsubmit ); this.example = document.createElement( 'li' ); this.example.className = 'nl-ti-example'; this.example.innerHTML = this.elOriginal.getAttribute( 'data-subline' ); this.optionsList.appendChild( this.getinputWrapper ); this.optionsList.appendChild( this.example ); this.fld.appendChild( this.toggle ); this.fld.appendChild( this.optionsList ); this.elOriginal.parentNode.insertBefore( this.fld, this.elOriginal ); this.elOriginal.style.display = 'none'; }, ... }

    Finally we will bind some events to the custom fields. For the dropdown we want to update the form field with the value chosen from the list of options, and for the input the User will type the desired value and either press enter, the submit button or simply click the overlay to update the form value.

    NLField.prototype = {
    	...
    	_initEvents : function() {
    		var self = this;
    		this.toggle.addEventListener( 'click', function( ev ) { ev.preventDefault(); ev.stopPropagation(); self._open(); } );
    		this.toggle.addEventListener( 'touchstart', function( ev ) { ev.preventDefault(); ev.stopPropagation(); self._open(); } );
    
    		if( this.type === 'dropdown' ) {
    			var opts = Array.prototype.slice.call( this.optionsList.querySelectorAll( 'li' ) );
    			opts.forEach( function( el, i ) {
    				el.addEventListener( 'click', function( ev ) { ev.preventDefault(); self.close( el, opts.indexOf( el ) ); } );
    				el.addEventListener( 'touchstart', function( ev ) { ev.preventDefault(); self.close( el, opts.indexOf( el ) ); } );
    			} );
    		}
    		else if( this.type === 'input' ) {
    			this.getinput.addEventListener( 'keydown', function( ev ) {
    				if ( ev.keyCode == 13 ) {
    					self.close();
    				}
    			} );
    			this.inputsubmit.addEventListener( 'click', function( ev ) { ev.preventDefault(); self.close(); } );
    			this.inputsubmit.addEventListener( 'touchstart', function( ev ) { ev.preventDefault(); self.close(); } );
    		}
    
    	},
    	_open : function() {
    		if( this.open ) {
    			return false;
    		}
    		this.open = true;
    		this.form.fldOpen = this.pos;
    		var self = this;
    		this.fld.className += ' nl-field-open';
    	},
    	close : function( opt, idx ) {
    		if( !this.open ) {
    			return false;
    		}
    		this.open = false;
    		this.form.fldOpen = -1;
    		this.fld.className = this.fld.className.replace(/b nl-field-openb/,'');
    
    		if( this.type === 'dropdown' ) {
    			if( opt ) {
    				// remove class nl-dd-checked from previous option
    				var selectedopt = this.optionsList.children[ this.selectedIdx ];
    				selectedopt.className = '';
    
    				opt.className = 'nl-dd-checked';
    				this.toggle.innerHTML = opt.innerHTML;
    
    				// update selected index value
    				this.selectedIdx = idx;
    				// update original select element´s value
    				this.elOriginal.value = this.elOriginal.children[ this.selectedIdx ].value;
    			}
    		}
    		else if( this.type === 'input' ) {
    			this.getinput.blur();
    			this.toggle.innerHTML = this.getinput.value.trim() !== '' ? this.getinput.value : this.getinput.placeholder;
    			this.elOriginal.value = this.getinput.value;
    		}
    	}
    	...
    }
    

    And that’s it! I hope you enjoyed this tutorial and find it inspiring!

    Tagged with:

    Manoela Ilic

    Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

    Stay up to date with the latest web design and development news and relevant updates from Codrops.

    Feedback 58

    Comments are closed.
    1. As always, your tutorial is very clear and the demo just amazing! Thank you Manoela.

    2. Couple of errors in the grammar of the sentence.
      It should read:
      “I feel like eating [anything/Indian/French/Japanese/Italian]
      in a [normal/fancy/hip/traditional/romantic] restaurant.
      [anytime/at 7 p.m./at 8 p.m./at 9 p.m.] in [city name].”

    3. I love all of your tutorials, Manoela, but this is definitely one of my absolute favorites! Awesome design, idea, and UX!

    4. This is interesting. Can we make this work with multiple forms in a single page? =)

    5. I recently built a search form like this without realizing it was a “thing”. 🙂

      learnedbyme.com < the search results page.

      But anyways, your code is really nice!

    6. Another thing which complicates forms like this is that the grammer will need to change when certain words are chosen. For instance in that site I posted it can say “… at Night” or it can also say “… in the Morning”. It would require some additional logic to make the connecting words change in those cases.

    7. Hello,

      My team built a similair system for a full-text search engine : labs.seloger.com. Ours isn’t the main search interface but a combination of the “feedback phrase” and the search facets.
      Our objectif was to save screen space by combining the two, but also to “educate” the user on what phrases are understood by the motor.

      It’s nice to see this type of interface being used elsewhere ! I haven’t seen many examples so far.

      Thanks,
      David

    8. Another amazing tutorial and concept!!!
      Please continue as I think you are in a roll.

      Thank you very much for sharing!!!

      Raimundo

    9. Great form! I would like to create a contact form with it, so I need to create a textarea. As the Input type in the JS-file is “text”, how can I create “textfield”? Just copy the _createInput : function() { and change “text” to “textarea”?

    10. I want that when someone presses FIND A RESTAURANT, a sentence appear for each combination. How do?

    11. Great idea, could be implemented as a functional piece to quite a few sites and replace quite a few current sites input selectors, apps etc. Travel sites, restaurant site finders etc.

    12. Would be great to see an alternative demo as a Contact Form in this style

    13. It’s very pretty, and I do like the natural approach, but it’s not the most accessible thing.

      Sure I can disable styles and I get to native input boxes. But as-is I can’t tab through to the individual field elements, which make it hard for keyboard users to interact with the form.

    14. Hello, what a genious work!
      I have only one question with your work.
      I wanna use an ‘change()’ event with the drop down select changes.
      $(#'option-1').change(function(){ ...... });
      seems not to work. Am I missing something? – Maybe the options are invisible?
      Anyway Thanks for the impressive, inspiring work!

    15. Is there any way to have the input field not a pop up option. So if the user could simple type straight into the section? If so that would be fantastic! ha

    16. That’s nice. But how should I make it work, like sending the email with the content of the form to my email address?

    17. hey
      i i was working on your form and i wanted to include the datepicker of jquery, but have trouble executing the script. javascript for nl-form inserts class to div and inputs, because of which my datepicker is not working properly. how can i tell js to exclude only this protion of the form:

      please help me figure out a way to execute the datepicker. kindly help me.

      • form strips the code. here it is again

        (open div) id=jrange class=dates
        input field ——-no id or class
        (open div)(close div) ——empty div with no id or class
        (close div)

    18. sorry again. didnt notice code attribute
      <div id="jrange" class="dates"> <input /> <div></div> </div>

    19. I love the beautiful look of this form, but when I choose options within it, I don’t get any results.

      And I downloaded the demo to implement, I don’t see where I adjust to add the results per category of choice.

      Help!

      many thanks
      Andrea

    20. Hey! You forgot an overflow hidden on .nl-form to prevent showing extra characters from inputs if there’s another div closer. It isn’t natural write extra large words but… uhm! people can do it.

      Thank you!

    21. Uhm adding “overflow:hidden” breaks visible .nl-field ul. inputs.
      Better use a ‘word-break: break-all’ on “.nl-field-toggle, .nl-form input, .nl-form select”.

    22. How to make a new phrase to appear when a specific option is selected? For example, Japanese and move to the next sentence completely changes.

    23. I absolutely love your script, however i do have a question. Is there some sort of scroll function? I can use overflow:auto on the UL, but that looks horrible. I can’t reduce the list and autocomplete in this case isn’t the best way.

    24. Hi guys . Someone know how to program this for display the result . A link or something ? i tried with like and match again sentences but i can´t figure out how to do it . I will apreciate too much the help

    25. Hey, I am using this NL Form in one of my projects and i want to add some client side validation to this form in a manner so that if a user inputs something incorrect in a input field then on submitting the form (clicking on submit button/hitting enter) the incorrect input field will open (in overlay) as it opens when someone clicks on the input field for providing input.

      Can you tell me how can i achieve this?

    26. I needed to have the original selects trigger the change event on themselves when user clicks an option. I opted to simply utitlize jQuery for this.

      // in the close method, after setting the original select's value: // trigger change on the select jQuery(this.elOriginal).trigger('change');

      Also I needed to fix an issue in IE that was creating NL text fields for hidden inputs.

      In the _init method change:
      Array.prototype.slice.call( this.el.querySelectorAll( 'input' ) ).forEach( function( el, i ) {...
      to
      Array.prototype.slice.call( this.el.querySelectorAll( 'input[type=text]' ) ).forEach( function( el, i ) {...

      Hope this helps others who had the same needs.

    27. Hi!
      Great script!!
      I have a problem, I need to reload a second select … how could I do?

    28. Hi Mary,

      I am using this form in one of the website for category filtration of wordpress custom blog. However, I am using only 1 select box for listing blog categories. Now what I want was that when user selects the category from the overlay list I want the form to submit immediately rather it close overlay list and stay on the same page with selected category name. This is because my client doesn’t bother to click submit button.

      Looking forward for your suggestion.

      Many thanks,

      Rajesh