Custom Form Elements

Lecture notes: display: inline-block, the <select> dropdown, background properties, radio buttons, checkboxes, custom styling, pseudo-elements, the + and ~ selectors

display: inline-block

Besides block, inline, and flex, there is one more value — inline-block. It combines the behavior of inline and block elements.

What comes from where?

From inline elements From block elements
Elements sit on the same line You can set width and height
Without width, the width is defined by the content padding and margin work fully
Positioned via text-align Not split into parts when the line wraps
The <button> element has display: inline-block by default. That is why you can center it with text-align: center on its parent.

Centering a button

.form-row:last-child {
text-align: center;  /* the button inside becomes centered */
}

The gap between inline-block elements

A gap (~4px) appears between inline-block elements. It is not “created” — it comes from the line-break character or space in your HTML code.

How does it appear?

<!-- There is a line break between these divs → the browser shows 1 space -->
<div class="card">...</div>
<div class="card">...</div>

How to remove it?

Method Drawback
Write tags with no spaces between them Fragile solution — any space breaks the layout
font-size: 0 on the parent, restored on the children Breaks relative units (em), inconvenient
Flexbox instead of inline-block Best solution — no hacks
If you need elements to sit tightly against each other — use display:
flex
instead of inline-block. Flex items have no gaps between them.

Dropdown list: <select> + <option>

<select> is a form element for choosing one option from a list. <option> is each choice (it can only appear inside <select>).

<label>
<span class="form-hint">Choose a city</span>
<select class="select" name="city">
<option>Kyiv</option>
<option selected>Lviv</option>
<option>Odesa</option>
<option>Kharkiv</option>
</select>
</label>
Attribute Purpose
selected Marks the option selected by default
multiple Allows selecting several (rarely used — awkward UX)
Without selected, the first <option> becomes selected. If selected is on several, the last one in the code wins.

When to use select vs. radio?

<select>

  • More than 5 options
  • Takes up minimal space
  • Requires 2 clicks (open + choose)
radio

  • 2–5 options
  • All options visible at once
  • Choice in 1 click

appearance: none — resetting the styling

Browsers have their own styling for form elements (<select>, some <input> types) that is hard to override. The appearance property with the value none disables this default styling.

.select {
-webkit-appearance: none;  /* Safari, old Chrome */
-moz-appearance: none;     /* Firefox */
appearance: none;          /* standard */
}
Modern browsers (Chrome 84+, Firefox 80+, Safari 15.4+) support appearance:
none
without prefixes. But for full compatibility, add all three lines.

Where to apply it?

  • <select> — removes the default arrow, allows full styling
  • input[type="search"] — removes the non-standard styling in Safari
Don’t apply appearance: none everywhere. Buttons and ordinary <input>s style perfectly well without it.

background — the element’s background

A group of properties for controlling the background: color, image, repetition, position, size.

background-color

Sets the background color. It is recommended to always place a color under the background-image — in case the picture fails to load.

background-image

background-image: url("icons/arrow.svg");

Loads an image and shows it as the element’s background. Without extra settings, it repeats and fills the whole block.

background-repeat

Value Behavior
repeat (default) Repeats horizontally and vertically
repeat-x Horizontally only
repeat-y Vertically only
no-repeat No repetition (a single copy)
no-repeat is used most often — for icons, decorative elements, and unique background images.

background-position

Defines the position of the background image. Two values: horizontal and vertical.

/* 9 anchor points */
background-position: left top;       /* top-left corner */
background-position: center center;  /* strictly centered */
background-position: right center;   /* right, vertically centered */
/* With an offset from the anchor point */
background-position: right 10px center;  /* right, -10px to the left */
Pick the anchor point closest to the spot you need. For an icon on the right side of a field — right
center
with an offset to the left.

background-size

Value Behavior
200px auto Width 200px, height — proportional
auto 100px Height 100px, width — proportional
cover Covers the entire block (part of the image may be cropped)
contain Fits fully into the block (part of the block may be empty)
cover — for decorative backgrounds where details don’t matter.
contain — for images where it’s important to show everything (portraits, diagrams).

Styling <select>: a full example

After resetting with appearance: none, the <select> element becomes an ordinary rectangle. Now we add our own arrow via background-image:

.select {
display: block;
width: 100%;
padding: 10px 35px 10px 15px;  /* more padding-right for the arrow */
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
font-family: inherit;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("icons/arrow-down.svg");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px auto;
}
Don’t forget to wrap the <select> in a <label> — so that clicking the caption activates the field. And add :focus styles identical to your other fields.

Radio buttons: input type=”radio”

Radio buttons let you choose one option from a group. The name “radio” comes from old radio receivers: you press one button and another pops out.

<label>
<input type="radio" name="gender" class="radio">
<span class="radio-text">Male</span>
</label>
<label>
<input type="radio" name="gender" class="radio">
<span class="radio-text">Female</span>
</label>

The name attribute — creating a radio group

Without name, each radio button works independently — you can select them all at once. The name attribute binds radio buttons into a group: only one in the group is selected.

The name value should reflect the group’s purpose: "gender", "subscription", "delivery" — not the abstract "radiogroup".
Wrap radio buttons in a <label> — then clicking the text also activates the choice. Plus, <label> is an inline element, so the radio buttons will automatically stay on the same line.

Pseudo-elements ::before and ::after (practice)

Pseudo-elements let you add virtual blocks to any element — without changing the HTML code. They are created exclusively through CSS.

The required content property

.heading::before {
content: "";  /* required! without content, the element won't appear */
display: block;
width: 60px;
height: 4px;
background-color: #4caf50;
margin-bottom: 10px;
}

Limitations

Pseudo-elements do not work on these tags:

  • <input> — self-closing, no content
  • <textarea>
  • <select>
  • <img> — self-closing
Don’t insert text content through pseudo-elements — it is invisible to screen readers and not indexed by search engines. Pseudo-elements are only for decorative styling.

Layer priority

::before appears before the content and has lower priority (the content overlaps it). ::after appears after the content and has higher priority (it can overlap the content).

vertical-align — vertical alignment

Works for inline and inline-block elements. Defines how the element aligns relative to the line.

Value Behavior
baseline (default) Along the text baseline (the bottom edge of the letters)
top Along the top edge of the line
middle Along the center of the line
bottom Along the bottom edge of the line
vertical-align: middle is perfect for aligning icons, radio buttons, and checkboxes next to text.

The + (adjacent) and ~ (general sibling) selectors

These selectors work between “siblings” — elements at the same nesting level (one parent). They are the analog of the > (child) and space (descendant) selectors, but horizontal.

The + (adjacent) selector

Styles an element if its immediate older sibling matches the condition:

/* Style .radio-text if it is immediately preceded by .form-hint */
.form-hint + .radio-text {
color: red;
}

The ~ (general sibling) selector

Styles an element if any of its older siblings matches the condition:

/* Style .radio-text if there is a .form-hint somewhere earlier in the code */
.form-hint ~ .radio-text {
color: red;
}

Comparison of all four “horizontal-vertical” selectors

Selector Direction Strictness
A B (space) Parent → descendant (any depth) Loose
A > B Parent → immediate child Strict
A ~ B Older sibling → any younger one Loose
A + B Older sibling → immediate next one Strict
The ~ selector is more reliable than +: if someone inserts an extra element between the siblings, + breaks, but ~ does not.
These selectors work only from an older to a younger sibling. Styling an older sibling based on a younger one is impossible in CSS.

Custom styling of radio buttons

Standard radio buttons cannot be styled: browsers don’t allow changing their color, size, or shape. The solution is to create a mock and hide the original.

Step 1: create a mock via ::before

Since <input> doesn’t support pseudo-elements, we create them on the neighboring <span>:

.radio-text::before {
content: "";
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: middle;
background-image: url("icons/radio.svg");
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}

Step 2: pass the :checked state via the ~ selector

/* If the radio button is selected → swap the picture on the mock */
.radio:checked ~ .radio-text::before {
background-image: url("icons/radio-active.svg");
}
Read it right to left: style the ::before of the .radio-text element if somewhere before it there is a .radio in the :checked state.

Step 3: hide the original accessibly

.radio {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}
Don’t use display: none or visibility: hidden — the element would become inaccessible to screen readers and keyboard navigation. clip-path: inset(50%) hides it visually but preserves accessibility.

Step 4: the focus state

/* Focus from the radio button → visual highlight of the text */
.radio:focus ~ .radio-text {
outline: 2px solid #1976d2;
outline-offset: 2px;
}

position: absolute — briefly

position: absolute pulls an element out of the normal document flow and places it on a separate “layer”:

  • Other elements behave as if the absolute element doesn’t exist
  • Width and height are calculated from the content (as in inline-block)
  • You can set width and height — they will work
  • The parent element doesn’t account for the height of the absolute child
position in detail (static, relative, absolute, fixed, sticky) is covered in later lectures. Here it is only needed to correctly hide the original radio/checkbox.

clip-path: inset() — visual clipping

The clip-path property lets you show only part of an element and hide the rest.

/* Clip 50% from each side — the element disappears visually */
clip-path: inset(50%);
/* Clip into a circle shape */
clip-path: circle(50%);
/* Clip into a polygon shape */
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
inset(50%) — clips 50% from each side of the element. The remaining area = nothing. The element disappears visually but stays accessible to screen readers.
The deprecated clip: rect() property did the same, but it only worked with position:
absolute
and is deprecated. Use clip-path — it is more modern and supports more clipping shapes.

Checkbox: input type=”checkbox”

A checkbox is a “yes/no” element: to agree or not agree. Essentially, it is two radio buttons in a single element.

<label>
<input type="checkbox" name="consent" class="checkbox" required>
<span class="checkbox-text">I agree to the processing of personal data</span>
</label>

The styling is absolutely identical to radio buttons:

  1. Create a mock via ::before on .checkbox-text
  2. Pass the :checked state via .checkbox:checked ~ .checkbox-text::before
  3. Hide the original via position: absolute + clip
  4. Add the :focus state
/* Hide together with radio — separated by a comma */
.radio,
.checkbox {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}

A modern alternative: accent-color

In file 06 we already mentioned accent-color — a property that lets you change the color of standard checkbox, radio, and range elements without custom styling:

input[type="radio"],
input[type="checkbox"] {
accent-color: #e53935;
}
accent-color

  • One line of CSS
  • Preserves the standard behavior
  • 100% accessibility
  • Limited control (color only)
Custom styling

  • Full control over the look
  • The designer decides the shape and animation
  • Requires complex CSS
  • You have to take care of accessibility
If the designer only wants to change the color — use accent-color. If full customization is needed — the mock-and-::before technique.

Summary

Topic Key point
display: inline-block Line + dimensions; centering via text-align; gap between elements
<select> + <option> Dropdown list; selected — pre-selected; > 5 options
appearance: none Resets the browser’s default styling for form elements
background image + repeat + position + size; cover vs contain
input type="radio" Choosing one from a group; name binds it into a radio group
::before / ::after Decorative blocks via CSS; required content; not for input/img
vertical-align Vertical alignment of inline/inline-block; middle for icons
The + and ~ selectors Sibling selectors; + strict, ~ loose; only from older to younger
Custom styling of radio/checkbox Mock via ::before + :checked state via ~ + accessible hiding
position: absolute Pulls the element out of the flow; needed for clip
clip-path: inset() Visual hiding while preserving accessibility; replaces the deprecated clip: rect()
accent-color A modern alternative: change the color of radio/checkbox in one line

Vitalii Kaplia

Founder, Web Developer & WordPress Expert

I became interested in programming back in 1997. The first acquaintance with a future profession was using Visual Basic. In…

More about author

A digital product engineer and web solutions architect

Free consultation + cost calculation

Let’s discuss your project?

Free consultation + cost calculation

More interesting articles

Customer login

This site uses cookies

We use cookies to personalize content and ads, provide social media features, and analyze our traffic. We also share information about your use of our website with our social media, advertising, and analytics partners, who may combine it with other information you have provided to them or collected when you use their services. By continuing to use our site, you consent to our use of cookies and accept our Privacy Policy and Terms of Use.

Virtual assistant
Hi! I'm the KAPLIA.PRO virtual assistant! I can tell you about our services, portfolio projects, ballpark pricing, and how we work — from websites and eCommerce to web apps, CRMs, bots, and AI agents. Ask away!

By continuing with the AI assistant, I agree to the site’s terms of use and privacy policy.

AI assistant may make mistakes in responses