Components · Atom

Button

The primary action element in HolidaySeva. Buttons trigger bookings, submit forms, and guide users through every flow — from search to checkout.

New to buttons?
A button is an HTML element that users click to do something — book a trip, submit a form, save a listing. In code, it looks like <button class="btn btn-primary">Book Now</button>. You give it a class to choose its colour and style. That's it. Everything else — hover effects, animations, focus rings — is handled automatically by button.css and button.js.
Live demo — hover and click each button

Step-by-step

Quick Start — 3 steps to your first button

Follow these three steps and you'll have a working button in under 2 minutes, even if you've never done this before.

1
Link the stylesheet in your <head>
This one line loads all the button styles. Paste it inside the <head> section of your HTML file, after your main design-system.css link.
2
Add the script before </body> (optional but recommended)
This enables loading spinners, click ripples, and toggle behaviour. You don't need it if your buttons are purely decorative — but you almost always do.
3
Write a <button> with two classes
Always use .btn as the base class, then add a variant class like .btn-primary. The label inside the tag is what the user sees. Done.
HTML — complete setup
<!-- Step 1: In <head> -->
<link rel="stylesheet" href="https://holidayseva.com/UI/Atom/button/button.css">

<!-- Step 2: Before </body> -->
<script src="https://holidayseva.com/UI/Atom/button/button.js"></script>

<!-- Step 3: Your button -->
<button class="btn btn-primary">Book Now</button>
💡 Tip Always use a native <button> HTML element. Never use a <div> or <span> styled to look like a button — that breaks keyboard navigation and screen readers.

How it's built

Anatomy

Every button is made of up to four parts. You control which parts appear by adding classes or child elements.

.btn (container)
.btn-icon
label (text)
.btn
Required. The wrapper element. Controls padding, shape, colour, shadows, and transitions. Every button must have this class.
.btn-icon
Optional. An SVG icon placed before or after the label. Automatically inherits the button's text colour — no extra colour code needed.
label
Required. The text the user reads. Keep it short and action-oriented: "Book Now", "Save", "Cancel". Avoid vague labels like "Click here".
.btn-badge
Optional. A small chip showing a number — e.g. a cart count. Place it as a <span class="btn-badge"> at the end of the label.
.btn-spinner
Auto-injected. When a button is loading, button.js adds this animated spinner automatically. You never need to write it yourself.

Design tokens

Specifications

These are the exact values used in button.css for the default md size. You should never need to override these — they're already correct.

height 44px — matches Apple's minimum touch target size so buttons are easy to tap on mobile
padding-inline 20px — space on the left and right sides inside the button
border-radius 999px — creates a pill/capsule shape; a very large value ensures it's always fully rounded
border-width 1.5px — thin enough to look refined, thick enough to be visible
font-weight 600 — semi-bold; labels are intentionally heavier than body text
font-size 15px — slightly larger than body for readability
letter-spacing −0.01em — slight tightening gives a polished, premium feel
icon gap 8px — space between icon and label text
press animation scale(0.97) over 120ms — subtle shrink on click gives physical feedback
transition 160ms for colour & background · 200ms for shadow
--color-primary #FF385C — HolidaySeva rose
--color-secondary #222222 — near black
--color-accent #00A699 — HolidaySeva teal
focus ring 3px · rgba(255,56,92,0.30) — only appears when navigating with a keyboard, never on mouse click

All variants

Variants

There are 10 variants. Each has a single, specific role. Using the wrong variant in the wrong place will confuse users — so read each "When to use" carefully.

⚠️ Rule Use only one Primary button per screen. If you put two Primary buttons side by side, the user won't know which action matters most. Use a Secondary or Outline button for the less important action.
✅ Do
  • One btn-primary per view
  • Pair with btn-outline or btn-ghost for secondary actions
  • Use btn-danger only for delete/cancel
  • Use btn-accent only for messaging
❌ Don't
  • Two btn-primary buttons next to each other
  • Use btn-danger for a regular edit action
  • Mix btn-accent with btn-primary in the same action row
  • Use btn-link to trigger actions (use it only for navigation)
Primary btn-primary
The strongest call-to-action. Filled with HolidaySeva rose. Use once per screen for the single most important action — booking, confirming, submitting.
✅ Use for: Book, Confirm, Submit
HTML
<button class="btn btn-primary">Book Now</button>
Secondary btn-secondary
Dark filled. Strong, but clearly subordinate to Primary. Pair it alongside a Primary for a second important action like "Browse All".
✅ Use for: Browse All, Explore
HTML
<button class="btn btn-secondary">Explore Stays</button>
Outline btn-outline
Transparent background with a visible border. Great for secondary actions inside cards and forms. Less visually dominant than Primary or Secondary.
✅ Use for: Save, Edit, View Details
HTML
<button class="btn btn-outline">Save to Wishlist</button>
Ghost btn-ghost · btn-ghost-primary
No visible border or background until you hover over it. The "quietest" button — use for low-importance actions in toolbars, menus, or dense UIs.
⚠️ Low emphasis only
HTML
<button class="btn btn-ghost">Learn More</button>
<button class="btn btn-ghost-primary">View All Listings</button>
Soft btn-soft
Light rose-tinted background with rose text. Friendly and visible without being aggressive. Perfect for filter chips and category pills.
✅ Use for: Filters, Tags, Categories
HTML
<button class="btn btn-soft">Apply Filters</button>
Accent btn-accent
HolidaySeva teal — strictly reserved for messaging and communication actions. Complements the primary rose without competing with it.
✅ Use for: Message, Contact Host
HTML
<button class="btn btn-accent">Contact Host</button>
Danger btn-danger · btn-danger-outline
Red — for destructive actions that cannot be undone. Always show a confirmation dialog before executing the action. Never use this for a regular edit.
🚫 Destructive only
HTML
<button class="btn btn-danger">Cancel Booking</button>
<button class="btn btn-danger-outline">Remove Listing</button>
White btn-white
White filled — for use on dark or photo backgrounds only. Invisible on a white page background, so only use it inside dark containers or hero images.
⚠️ Dark backgrounds only
HTML
<button class="btn btn-white">Explore Destinations</button>
Link btn-link
Looks like a hyperlink. Use it inside body copy for navigation — not for triggering actions. If clicking it does something (like deleting data), use a different variant.
⚠️ Navigation only
HTML
<button class="btn btn-link">View full cancellation policy</button>

Sizing

Sizes

There are 5 sizes. You don't need a size class for the default — just .btn .btn-primary gives you the standard 44px button. Only add a size class when you need something bigger or smaller.

btn-xl
60px · Hero CTAs
btn-lg
52px · Section CTAs
default md
44px · Standard
btn-sm
36px · Cards & rows
btn-xs
28px · Tags only
HTML
<!-- No size class = default 44px -->
<button class="btn btn-primary">Default</button>

<!-- Bigger -->
<button class="btn btn-primary btn-xl">XL · 60px</button>
<button class="btn btn-primary btn-lg">Large · 52px</button>

<!-- Smaller -->
<button class="btn btn-primary btn-sm">Small · 36px</button>
<button class="btn btn-primary btn-xs">XS · 28px</button>
Full-width button

Add .btn-block to make a button stretch to fill its container's width. Commonly used in checkout modals, mobile forms, and bottom action bars.

HTML
<!-- Just add btn-block to any button -->
<button class="btn btn-primary btn-block">Confirm & Pay</button>

Composition

Icons & Badges

You can add an SVG icon before or after the label, an icon-only button (no text), or a badge showing a count. Icons inherit the button's colour automatically — you never need to set their colour manually.

HTML — all icon patterns
<!-- Leading icon: place the <svg> BEFORE the label text -->
<button class="btn btn-primary">
  <svg class="btn-icon" aria-hidden="true">...</svg>
  Search
</button>

<!-- Trailing icon: place the <svg> AFTER the label text -->
<button class="btn btn-outline">
  Save to List
  <svg class="btn-icon" aria-hidden="true">...</svg>
</button>

<!-- Icon-only: add btn-icon-only + always write aria-label -->
<button class="btn btn-outline btn-icon-only"
        aria-label="Share listing">
  <svg class="btn-icon" aria-hidden="true">...</svg>
</button>

<!-- Badge: wrap the count in a span.btn-badge at the end -->
<button class="btn btn-secondary">
  Cart <span class="btn-badge">3</span>
</button>
⚠️ Icon-only buttons need aria-label When there's no text, screen readers have nothing to announce. Always add aria-label="…" on the <button> element itself — this is what gets read aloud.

Interactivity

States

A button can be in six states. Hover, focus, and active are fully automatic. You only need to write code for Disabled and Loading.

default
hover
active / pressed
focus (keyboard)
disabled
loading
Disabled state

Add the disabled attribute directly on the <button> tag. CSS automatically reduces opacity to 38% and changes the cursor to "not-allowed". The button becomes unclickable — no extra JavaScript needed.

HTML
<!-- Just add the "disabled" attribute -->
<button class="btn btn-primary" disabled>Book Now</button>

<!-- For <a> tags (links styled as buttons), do this instead: -->
<a class="btn btn-primary"
   aria-disabled="true"
   tabindex="-1">Book Now</a>
Loading state — interactive demo

When a user clicks Book and your page sends a request to a server, show a loading state. Call ButtonSystem.loading(btn, true) to start it — the JS automatically injects the spinner, disables the button, and announces "busy" to screen readers. Call ButtonSystem.loading(btn, false) when the server responds to restore it.

JavaScript
// Manual control (most common approach)
const btn = document.querySelector('#bookBtn');

// When your fetch/API call starts:
ButtonSystem.loading(btn, true);

// When your fetch/API call finishes:
ButtonSystem.loading(btn, false);

// ── OR ── Auto-trigger with just HTML attributes (no JS needed)
<button class="btn btn-primary"
        data-loading-on-click
        data-loading-text="Booking…">
  Book Now
</button>
Toggle / Selected state

Use .btn-toggle when a button can be switched between selected and unselected — like a room-type selector. Add the group inside a .btn-group wrapper. button.js automatically handles the selected highlight and ARIA state.

HTML
<!-- Wrap in btn-group, use btn-toggle on each button    -->
<!-- Add is-selected + aria-pressed="true" to the default -->
<div class="btn-group">
  <button class="btn btn-toggle is-selected"
          aria-pressed="true">Entire Place</button>
  <button class="btn btn-toggle"
          aria-pressed="false">Private Room</button>
  <button class="btn btn-toggle"
          aria-pressed="false">Shared Room</button>
</div>
💡 Tip — Listening for toggle events When a toggle button is clicked, button.js fires a custom event called btn-toggle with a detail.pressed value of true or false. You can listen to it exactly like a regular click or change event: btn.addEventListener('btn-toggle', e => console.log(e.detail.pressed))

Grouping

Button Groups

Two ways to group buttons: spaced (with gap between) or attached (merged into one segmented control with borders touching).

Spaced group — .btn-group

Wraps buttons in a flex container with 10px gaps. Use for action rows like Confirm + Save Draft + Cancel.

Attached group — .btn-group-attached

Buttons share borders and form a single segmented control. Use for mutually exclusive filter options like Day / Week / Month.

HTML — both group types
<!-- Spaced: gap between each button -->
<div class="btn-group">
  <button class="btn btn-primary">Confirm</button>
  <button class="btn btn-outline">Save Draft</button>
  <button class="btn btn-ghost">Cancel</button>
</div>

<!-- Attached: borders merge, looks like one control -->
<div class="btn-group-attached">
  <button class="btn btn-outline">Day</button>
  <button class="btn btn-outline">Week</button>
  <button class="btn btn-outline">Month</button>
</div>

JS Reference

JavaScript API

button.js loads automatically when you include the script tag. It exposes one public method and one custom event. Everything else runs silently in the background.

ButtonSystem .loading(btn, true) Enters loading: injects spinner animation, disables button, sets aria-busy="true" for screen readers
ButtonSystem .loading(btn, false) Exits loading: restores original button content, re-enables, removes aria-busy
data-loading-on-click HTML attribute — triggers loading automatically on click. No JavaScript required.
data-loading-text="…" HTML attribute — sets the label shown during loading, e.g. "Booking…"
btn-toggle (CustomEvent) Fires on every toggle click with event.detail.pressed (boolean). Listen with addEventListener('btn-toggle', fn)

Setup

Integration

Where the files live, and what to add to your colours file to keep tokens consistent across all components.

File structure
design_guidelines/
├── header.php
├── left_sidebar.php
├── right_sidebar.php
├── drawer_sidebar.php
├── footer.php
├── design-system.css
└── Atom/
    └── button/
        ├── button.php  ← this page
        ├── button.css  ← all styles
        └── button.js   ← loading/toggle/ripple
Required CSS token additions

These variables are used by button.css but aren't yet in your central colors.css. Add them once — they'll be picked up by buttons everywhere automatically.

CSS — add to colors.css
/* Primary hover & pressed states */
--color-primary-hover:    #E8314F;
--color-primary-pressed:  #CC2B47;

/* Ghost background on hover & press */
--color-ghost-hover-bg:   rgba(34, 34, 34, 0.06);
--color-ghost-pressed-bg: rgba(34, 34, 34, 0.10);

/* Danger button interaction states */
--color-danger-hover:     #A82C10;
--color-danger-pressed:   #8E240D;

/* Disabled state surfaces */
--color-surface-disabled: #F5F5F5;
--color-text-disabled:    #B0B0B0;
--color-border-disabled:  #E0E0E0;

A11y

Accessibility

The button system is built to WCAG 2.1 AA. The table below shows what's handled automatically and what you are responsible for. Green = automatic. Red = your job.

Requirement How it's handled Your job
Touch target size ✅ Auto — default md is 44px, the Apple minimum Don't override height below 44px on mobile
Keyboard focus ring ✅ Auto — 3px ring appears on keyboard navigation only Never write outline: none without adding your own ring
Colour contrast ✅ Auto — all variants pass 4.5:1 minimum ratio If you override colours, re-test contrast at webaim.org
Disabled state ✅ Auto — opacity and cursor handled by CSS ⚑ You — add native disabled attribute to the element
Loading state ✅ Autobutton.js sets aria-busy="true" Use ButtonSystem.loading() — don't manually add a spinner
Toggle state ✅ Autobutton.js keeps aria-pressed in sync ⚑ You — set the correct initial aria-pressed value in markup
Icon-only buttons ❌ Not automatic ⚑ You — always add aria-label="…" on the button element
⚠️ Important Always use a native <button> element. If you use a <div> or <span>, keyboard users can't tab to it, screen readers won't announce it as a button, and you'd need to manually write many lines of ARIA and event-handler code to replicate what the browser gives you for free.