Overview
The Natours form component features elegant text inputs with floating labels and custom-styled radio buttons. Forms include focus states, validation indicators, and smooth transitions for a polished user experience.
Complete booking form example from the project:
< form action = "#" class = "form" >
< div class = "u-margin-bottom-medium" >
< h2 class = "heading-secondary" > Start booking now! </ h2 >
</ div >
< div class = "form__group" >
< input type = "text" class = "form__input"
placeholder = "Full name" id = "name" required >
< label for = "name" class = "form__label" > Full name </ label >
</ div >
< div class = "form__group" >
< input type = "email" class = "form__input"
placeholder = "Email address" id = "email" required >
< label for = "email" class = "form__label" > Email address </ label >
</ div >
< div class = "form__group u-margin-bottom-medium" >
< div class = "form__radio-group" >
< input type = "radio" class = "form__radio-input"
id = "small" name = "size" >
< label for = "small" class = "form__radio-label" >
< span class = "form__radio-button" ></ span >
Small tour group
</ label >
</ div >
< div class = "form__radio-group" >
< input type = "radio" class = "form__radio-input"
id = "large" name = "size" >
< label for = "large" class = "form__radio-label" >
< span class = "form__radio-button" ></ span >
Large tour group
</ label >
</ div >
</ div >
< div class = "form__group" >
< button type = "submit" class = "btn btn--green" >
Next step →
</ button >
</ div >
</ form >
The label comes after the input in the HTML. This is crucial for the CSS sibling selector that creates the floating label effect.
Text Inputs
sass/components/_form.scss:9-35
& __input {
font-size : 1.5 rem ;
font-family : inherit ;
color : inherit ;
padding : 1.5 rem 2 rem ;
border-radius : 4 px ;
background-color : rgba ( $color-white , .5 );
border : none ;
border-bottom : 3 px solid transparent ;
width : 90 % ;
display : block ;
transition : all .3 s ease ;
& :focus {
outline : none ;
box-shadow : 0 1 rem 2 rem rgba ( $color-black , .1 );
border-bottom : 3 px solid $color-primary ;
}
& :focus:invalid {
border-bottom : 3 px solid $color-secondary-dark ;
}
& ::-webkit-input-placeholder {
color : $color-grey-dark-2 ;
}
}
Default State
Focus State
Invalid State
Semi-transparent white background (rgba(#FFF, .5))
Transparent bottom border (3px)
90% width for consistent sizing
Grey placeholder text
Removes default outline
Adds subtle shadow for elevation
Bottom border turns green ($color-primary)
Visual feedback for active field
Only appears when focused
Bottom border turns orange-red ($color-secondary-dark)
Uses HTML5 validation states
Provides immediate visual feedback
Font inheritance: font-family: inherit and color: inherit ensure inputs match the surrounding text styling automatically.
Floating Labels
Labels float above the input when text is entered:
sass/components/_form.scss:37-50
& __label {
font-size : 1.2 rem ;
font-weight : 700 ;
margin-left : 2 rem ;
margin-top : .7 rem ;
display : block ;
transition : all .3 s ease ;
}
& __input : placeholder-shown + & __label {
opacity : 0 ;
visibility : hidden ;
transform : translateY ( -4 rem );
}
How It Works
Initial State
When input is empty (:placeholder-shown is true), label is:
Hidden (opacity: 0, visibility: hidden)
Moved up by 4rem (translateY(-4rem))
User Types
As soon as user enters text, placeholder is no longer shown
Label Appears
CSS selector no longer matches, label animates to visible state:
Opacity: 0 → 1
Transform: -4rem → 0
Transition smooths the animation (0.3s)
Adjacent Sibling Selector: The + combinator selects the label that immediately follows the input: .form__input:placeholder-shown + .form__labelThis only works because the label comes after the input in the HTML.
Label Positioning
margin-left: 2rem; // Aligns with input padding
margin-top: .7rem ; // Space below input
The margins position the label just below and aligned with the input’s text area.
Form groups manage spacing between form elements:
sass/components/_form.scss:5-7
& __group :not ( :last-child ) {
margin-bottom : 1 rem ;
}
Usage: Applies spacing to all groups except the last one (submit button group).
Radio buttons are completely custom-styled, hiding the native input:
Radio Structure
< div class = "form__radio-group" >
< input type = "radio" class = "form__radio-input"
id = "small" name = "size" >
< label for = "small" class = "form__radio-label" >
< span class = "form__radio-button" ></ span >
Small tour group
</ label >
</ div >
sass/components/_form.scss:57-59
& __radio-input {
display : none ;
}
The native radio input is completely hidden. The custom visual is created with the label and span.
Radio Label
sass/components/_form.scss:61-66
& __radio-label {
font-size : $default-font-size ;
cursor : pointer ;
position : relative ;
padding-left : 4.5 rem ;
}
Cursor pointer: Shows it’s clickable
Position relative: Allows absolute positioning of the custom button
Padding-left 4.5rem: Creates space for the custom radio circle
The visible circular indicator:
sass/components/_form.scss:68-90
& __radio-button {
height : 3 rem ;
width : 3 rem ;
border : 5 px solid $color-primary ;
border-radius : 50 % ;
display : inline-block ;
position : absolute ;
left : 0 ;
top : -.4 rem ;
& ::after {
content : "" ;
display : block ;
height : 1.3 rem ;
width : 1.3 rem ;
border-radius : 50 % ;
background-color : $color-primary ;
opacity : 0 ;
transition : opacity .2 s ease ;
@include absCenter ;
}
}
Outer Circle
Inner Circle (::after)
// The visible ring
height: 3rem;
width: 3rem;
border: 5px solid $color-primary ; // Green ring
border-radius: 50%; // Makes it circular
Radio Checked State
sass/components/_form.scss:92-94
& __radio-input :checked ~ & __radio-label & __radio-button ::after {
opacity : 1 ;
}
Understanding the Selector
This complex selector breaks down as:
.form__radio-input:checked - When radio is checked
~ - General sibling combinator (any following sibling)
.form__radio-label - Finds the label
.form__radio-button::after - Targets the inner circle pseudo-element
Result: When radio input is checked, the inner circle becomes visible.
Visual States
Unchecked
Outer circle visible (green border)
Inner circle hidden (opacity: 0)
User Clicks
Label click triggers the hidden input
Input becomes :checked
Checked
CSS selector matches
Inner circle fades in (opacity: 1)
Transition animates the change (0.2s)
Centering the Inner Circle
Uses the absCenter mixin:
@include absCenter ;
// Expands to:
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
This perfectly centers the inner filled circle within the outer ring.
Radio Group Layout
sass/components/_form.scss:52-55
& __radio-group {
width : 49 % ;
display : inline-block ;
}
Result: Two radio buttons sit side-by-side, each taking ~49% width with a small gap.
HTML5 Validation
< input type = "text" class = "form__input"
placeholder = "Full name" id = "name" required >
The required attribute provides native validation.
CSS Validation States
& __input :focus:invalid {
border-bottom : 3 px solid $color-secondary-dark ;
}
Validation only on focus: By combining :focus:invalid, validation styling only appears when the user is actively interacting with the field, preventing premature error states.
Color Indicators
Accessibility Features
Proper Label Association
< input type = "text" class = "form__input" id = "name" required >
< label for = "name" class = "form__label" > Full name </ label >
The for attribute connects the label to the input, allowing:
Screen readers to announce the label
Clicking the label focuses the input
< input type = "radio" class = "form__radio-input"
id = "small" name = "size" >
< label for = "small" class = "form__radio-label" >
< span class = "form__radio-button" ></ span >
Small tour group
</ label >
Hidden input still receives focus and can be keyboard-navigated
Label click triggers the input
Name attribute groups radio buttons together
Focus Management
& __input :focus {
outline : none ;
box-shadow : 0 1 rem 2 rem rgba ( $color-black , .1 );
border-bottom : 3 px solid $color-primary ;
}
Removing default outline (outline: none) requires providing alternative focus indicators. This form uses:
Shadow elevation
Colored bottom border
Ensure these provide sufficient contrast for keyboard users.
Real-World Usage
< section class = "section-book" id = "section-book" >
< div class = "row" >
< div class = "book" >
< div class = "book__form" >
< form action = "#" class = "form" >
< div class = "u-margin-bottom-medium" >
< h2 class = "heading-secondary" > Start booking now! </ h2 >
</ div >
< div class = "form__group" >
< input type = "text" class = "form__input"
placeholder = "Full name" id = "name" required >
< label for = "name" class = "form__label" > Full name </ label >
</ div >
< div class = "form__group" >
< input type = "email" class = "form__input"
placeholder = "Email address" id = "email" required >
< label for = "email" class = "form__label" > Email address </ label >
</ div >
< div class = "form__group u-margin-bottom-medium" >
< div class = "form__radio-group" >
< input type = "radio" class = "form__radio-input"
id = "small" name = "size" >
< label for = "small" class = "form__radio-label" >
< span class = "form__radio-button" ></ span >
Small tour group
</ label >
</ div >
< div class = "form__radio-group" >
< input type = "radio" class = "form__radio-input"
id = "large" name = "size" >
< label for = "large" class = "form__radio-label" >
< span class = "form__radio-button" ></ span >
Large tour group
</ label >
</ div >
</ div >
< div class = "form__group" >
< button type = "submit" class = "btn btn--green" >
Next step →
</ button >
</ div >
</ form >
</ div >
</ div >
</ div >
</ section >
Styling Details
padding: 1 .5rem 2rem; // Vertical: 15px, Horizontal: 20px
width: 90%; // Responsive width
border-radius: 4px; // Subtle rounding
Background Transparency
background-color: rgba( $color-white , .5 );
The semi-transparent background allows the form’s background image to show through slightly, creating visual depth.
Transition Timing
transition: all .3s ease; // Inputs
transition: opacity .2s ease; // Radio button indicator
Faster transitions for radio buttons create snappier feedback; slightly slower for inputs feels more deliberate.
Browser Compatibility
Placeholder-shown Selector
The :placeholder-shown pseudo-class has good modern browser support:
Chrome/Edge: Yes
Firefox: Yes
Safari: Yes
IE11: No (fallback needed)
Fallback for Older Browsers
For IE11 support, consider JavaScript to toggle label visibility:
const input = document . querySelector ( '.form__input' );
const label = input . nextElementSibling ;
input . addEventListener ( 'input' , function () {
if ( this . value ) {
label . classList . add ( 'visible' );
} else {
label . classList . remove ( 'visible' );
}
});
Customization Examples
.form__input {
width : 100 % ; // Full width inputs
}
Different Color Scheme
.form__input:focus {
border-bottom : 3 px solid $color-tertiary-light ; // Blue focus
}
.form__radio-button {
border : 5 px solid $color-tertiary-light ; // Blue radio
& ::after {
background-color : $color-tertiary-light ;
}
}
The input styles work for multiple input types:
<!-- Textarea -->
< div class = "form__group" >
< textarea class = "form__input" id = "message"
placeholder = "Your message" rows = "5" ></ textarea >
< label for = "message" class = "form__label" > Message </ label >
</ div >
<!-- Select -->
< div class = "form__group" >
< select class = "form__input" id = "country" >
< option value = "" > Choose country </ option >
< option value = "us" > United States </ option >
< option value = "uk" > United Kingdom </ option >
</ select >
< label for = "country" class = "form__label" > Country </ label >
</ div >
Best Practices
Placeholder vs Label: The placeholder serves as an initial hint, while the floating label remains visible after the user starts typing. This prevents users from forgetting what information they’re entering.
Validation Timing: Only show invalid states when the user is actively focused on the field (:focus:invalid). Showing errors before the user has finished typing creates a frustrating experience.
Radio Button Groups: Always use the same name attribute for radio buttons that are part of the same group. This ensures only one can be selected at a time.
Summary
The form component demonstrates:
Floating labels using :placeholder-shown and adjacent sibling selectors
Custom radio buttons with hidden native inputs and styled pseudo-elements
Validation states with color-coded borders
Smooth transitions for polished interactions
Accessibility through proper label associations
BEM methodology for clear component structure
Progressive enhancement with HTML5 validation