Design System

v0.3

A collection of components built with HTML and CSS variables, the same ones used in this portfolio. Copy the markup, customize the tokens, and ship faster.

Components are stable for copy-paste use today. The npm package is coming soon.
CSS Variables

Every token is a CSS custom property, one file to override the entire system.

Zero dependencies

Plain HTML + CSS. No bundler, no framework, no runtime required.

React ready

React components coming as @achmadalimin/ui, same API, typed props.

Dark & light mode

Built-in dark default with a single data-theme="light" switch.

Installation

Two ways to use it: copy the CSS directly into your project, or install via npm once the package is live.

Start in 30 seconds

Copy design-system.css into your project, add the CSS tokens, then copy the component markup below.

CSS Tokens
$ curl -O https://achmadalimin.com/assets/css/design-system.css

npm (React — coming soon)

$ npm install @achmadalimin/ui

Theming

Override any token to match your brand. Set data-theme="light" on <html> to switch to light mode.

CSS

:root {
/* Backgrounds */
  --ds-bg:              #0f0f0f;
  --ds-surface:         #1a1a1a;
  --ds-surface-hover:   #222222;
  --ds-code-bg:         #161616;

  /* Borders */
  --ds-border:          #2e2e2e;
  --ds-border-subtle:   #1f1f1f;

  /* Text */
  --ds-text:            #f0f0f0;
  --ds-text-muted:      #777777;
  --ds-text-faint:      #3d3d3d;

  /* Accent */
  --ds-accent:          #e8e8e8;

  /* Semantic */
  --ds-success:         #4ade80;
  --ds-warning:         #fbbf24;
  --ds-error:           #f87171;

  /* Shape & type */
  --ds-radius:          0.5rem;
  --ds-font:            system-ui, sans-serif;
  --ds-mono:            ui-monospace, monospace;
}

[data-theme="light"] {
  --ds-bg:              #ffffff;
  --ds-surface:         #f0f0f0;
  --ds-surface-hover:   #e8e8e8;
  --ds-code-bg:         #f4f4f4;
  --ds-border:          #e2e2e2;
  --ds-border-subtle:   #ebebeb;
  --ds-text:            #1a1a1a;
  --ds-text-muted:      #6b6b6b;
  --ds-text-faint:      #9c9c9c;
  --ds-accent:          #111111;
}

Border

Two widths cover every use case. Color is set separately via --color-border tokens — keep them composable rather than pre-baked.

--border-width-1 1px · Default for cards, inputs, dividers
--border-width-2 2px · Focus rings, emphasized states
CSS
:root {
  --border-width-1: 1px;
  --border-width-2: 2px;
}

Radius

Four corner-radius steps cover everything from buttons to full pills.

--radius-sm 0.5rem · Inputs, small badges
--radius-md 0.75rem · Buttons, menu items
--radius-lg 1rem · Cards, modals, panels
--radius-full 9999px · Pills, avatars, chips
CSS
:root {
  --radius-sm:   0.5rem;
  --radius-md:   0.75rem;
  --radius-lg:   1rem;
  --radius-full: 9999px;
}

Colors

Every color is a CSS custom property. Add your own :root overrides to rebrand the whole system. Swatches reflect the active theme.

Base palette

--ds-bg Page background
--ds-surface Card / panel
--ds-surface-hover Hovered surface
--ds-border Component border
--ds-text Primary text
--ds-text-muted Secondary text
--ds-accent Primary action

Semantic

--ds-info #60a5fa
--ds-success #4ade80
--ds-warning #fbbf24
--ds-error #f87171

Icons

All icons used in this site. Each is a 16×16 SVG with currentColor fill so they inherit from their parent's color.

apps
arrow-down
arrow-up
check-circle
chevron-left
chevron-down
close
copy
check
delete
design-system
dislike
dislike-fill
edit
email
figma
github
info
lets-connect
like
like-fill
linkedin
medium
mcp
mic
moon
more
new-chat
pin
portfolio
reload
search
share
store
sidebar
stop
sun
trash
unpin
warning

Motion

Keyframe animations and easing curves used across the design system. All durations and curves are applied directly — no token abstraction needed.

Keyframes

mic-btn-pulse Box-shadow pulse on mic listening state · 1.5s ease-in-out infinite
mic-session-breathe Opacity breathe on active session · 2.5s ease-in-out infinite
cursor-ripple Expanding ring on click feedback · 0.45s ease-out forwards
cursor-dot-bounce Staggered dot bounce for listening indicator · 1.2s infinite
shimmer-sweep Left-to-right gradient sweep for skeleton loading · 1.4s infinite
typing-bounce Staggered dot bounce for typing indicator · 1.2s infinite
cursor-blink Step-function on/off for typewriter cursor · 0.6s step-end infinite
👋
wave Realistic wrist-wave rotation on greeting emoji · looped

Easing curves

Standard cubic-bezier(0.4, 0, 0.2, 1) · Sidebar, pill morph, cursor movement
Spring cubic-bezier(0.34, 1.56, 0.64, 1) · Overshoot pop for modals & tooltips
Ease out ease-out · Ripple, slide-up entrance animations
Ease in-out ease-in-out · Pulse, breathe, wave looping animations

Shadow

Four shadow levels built on --ds-shadow-* tokens. Each token adapts automatically to the active theme — darker in dark mode, softer in light mode.

--ds-shadow-sm Menus, tooltips
--ds-shadow-md Dropdowns, popovers
--ds-shadow-lg Modals, dialogs
--ds-shadow-xl Cards, page panels
CSS
:root {
  --ds-shadow-sm:  0 2px 8px rgba(0, 0, 0, 0.32);
  --ds-shadow-md:  0 4px 12px rgba(0, 0, 0, 0.32);
  --ds-shadow-lg:  0 12px 40px rgba(0, 0, 0, 0.40);
  --ds-shadow-xl:  0 20px 60px rgba(0, 0, 0, 0.48);
}

/* light theme overrides */
[data-theme="light"] {
  --ds-shadow-sm:  0 2px 8px rgba(0, 0, 0, 0.08);
  --ds-shadow-md:  0 4px 12px rgba(0, 0, 0, 0.10);
  --ds-shadow-lg:  0 12px 40px rgba(0, 0, 0, 0.12);
  --ds-shadow-xl:  0 20px 60px rgba(0, 0, 0, 0.15);
}

Space

4px-based scale for padding, margin, and gap. Numeric naming (--space-1 through --space-16) removes guesswork — multiply the number by 4 to get pixels.

--space-1 4px
--space-2 8px
--space-3 12px
--space-4 16px
--space-5 20px
--space-6 24px
--space-8 32px
--space-10 40px
--space-12 48px
--space-16 64px
CSS
:root {
  --space-0:  0;
  --space-1:  0.25rem;  /*  4px */
  --space-2:  0.5rem;   /*  8px */
  --space-3:  0.75rem;  /* 12px */
  --space-4:  1rem;     /* 16px */
  --space-5:  1.25rem;  /* 20px */
  --space-6:  1.5rem;   /* 24px */
  --space-8:  2rem;     /* 32px */
  --space-10: 2.5rem;   /* 40px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
}

Typography

All components use a shared font stack. Override --ds-font or --ds-mono in your own :root to swap in a custom typeface.

Type scale

2.5rem · 700 Display
1.75rem · 700 Heading
1rem · 600 Subheading
0.9375rem · 400 Body, the primary reading size.
0.875rem · 400 Small / secondary for labels and hints.
0.8125rem · mono const value = "code text";

Font stacks

CSS
--ds-font:
  -apple-system, BlinkMacSystemFont, "Inter",
  "Segoe UI", ui-sans-serif, system-ui, sans-serif;

--ds-mono:
  ui-monospace, "SF Mono", "Cascadia Code",
  "Fira Code", Consolas, monospace;

Accordion

Collapsible content sections. Click a trigger to expand or collapse the panel below it.

Button, Badge, Input, Card, Tabs, Accordion, and Tooltip are in v0.3. More components are being added continuously.
Yes, copy the CSS and apply the classes to your JSX. React component wrappers are coming as @achmadalimin/ui.
Yes. Everything on this page is MIT licensed. Use it freely in personal and commercial projects.
<div class="ui-accordion">
  <div class="ui-accordion-item">
    <button class="ui-accordion-trigger" aria-expanded="false">
      Accordion title
      <svg class="ui-accordion-chevron" width="14" height="14"
        viewBox="0 0 16 16" fill="none">
        <path d="M4 6l4 4 4-4" stroke="currentColor"
          stroke-width="1.5" stroke-linecap="round" />
      </svg>
    </button>
    <div class="ui-accordion-content">
      Accordion content goes here.
    </div>
  </div>
</div>

<script>
  document.querySelectorAll(".ui-accordion-trigger").forEach(t => {
    t.addEventListener("click", () => {
      const open = t.getAttribute("aria-expanded") === "true";
      t.setAttribute("aria-expanded", !open);
      t.nextElementSibling.classList.toggle("open", !open);
    });
  });
</script>

Badge

Small status and label indicator. Use it to highlight state, category, or metadata.

Default Success Warning Error Outline
<span class="ui-badge ui-badge--default">Default</span>
<span class="ui-badge ui-badge--success">Success</span>
<span class="ui-badge ui-badge--warning">Warning</span>
<span class="ui-badge ui-badge--error">Error</span>
<span class="ui-badge ui-badge--outline">Outline</span>

Button

Interactive button with four variants and three sizes. Use <a> or <button> same class works on both.

<button class="ui-btn ui-btn--primary">Primary</button>
<button class="ui-btn ui-btn--secondary">Secondary</button>
<button class="ui-btn ui-btn--outline">Outline</button>
<button class="ui-btn ui-btn--ghost">Ghost</button>

Sizes

<button class="ui-btn ui-btn--primary ui-btn--sm">Small</button>
<button class="ui-btn ui-btn--primary">Default</button>
<button class="ui-btn ui-btn--primary ui-btn--lg">Large</button>

Disabled

<button class="ui-btn ui-btn--primary" disabled>Primary</button>

Icon Only

<button class="ui-btn ui-btn--primary ui-btn--icon" aria-label="Share">
  <img src="/assets/images/icons/share.svg" width="16" height="16" alt="">
</button>
<button class="ui-btn ui-btn--secondary ui-btn--icon" aria-label="Edit">
  <img src="/assets/images/icons/edit.svg" width="16" height="16" alt="">
</button>

Props

Prop / Class Type Default Description
variant primary | secondary | outline | ghost primary Visual style variant
size sm | md | lg | icon md Button size; icon collapses to a square with no padding
disabled boolean false Disabled and non-interactive

Card

Versatile container with an optional header, body, and footer. Compose freely, all three regions are optional.

Card title

Supporting description text

Card body content. Put anything here, text, media, or other components.
<div class="ui-card">
  <div class="ui-card-header">
    <p class="ui-card-title">Card title</p>
    <p class="ui-card-subtitle">Supporting description text</p>
  </div>
  <div class="ui-card-body">
    Card body content goes here.
  </div>
  <div class="ui-card-footer">
    <button class="ui-btn ui-btn--primary">Save changes</button>
    <button class="ui-btn ui-btn--ghost">Cancel</button>
  </div>
</div>

Input

Text input field with label, hint, and error state. Wrap in .ui-field for vertical stacking with a label.

<input class="ui-input" type="text" placeholder="Placeholder text" />

Props

Class Description
ui-field Flex column wrapper for label + input + hint
ui-label Field label
ui-input Base input field
ui-input--error Error border and focus ring
ui-hint Helper text below the input
ui-hint--error Error color for the hint

Tabs

Horizontal tab navigation for switching between content panels. JavaScript handles the active state and panel visibility.

Overview content, this tab is active by default.
Detailed information visible on the Details tab.
Settings and preferences for this component.
<div class="ui-tabs">
  <div class="ui-tabs-list" role="tablist">
    <div class="ui-tabs-slider"></div>
    <button class="ui-tab-trigger active" aria-selected="true" role="tab">Overview</button>
    <button class="ui-tab-trigger" aria-selected="false" role="tab">Details</button>
    <button class="ui-tab-trigger" aria-selected="false" role="tab">Settings</button>
  </div>
  <div class="ui-tab-panel active">Overview content</div>
  <div class="ui-tab-panel">Details content</div>
  <div class="ui-tab-panel">Settings content</div>
</div>

<script>
  document.querySelectorAll(".ui-tabs").forEach(tabs => {
    const list = tabs.querySelector(".ui-tabs-list");
    const slider = list.querySelector(".ui-tabs-slider");
    const triggers = list.querySelectorAll(".ui-tab-trigger");
    const panels = tabs.querySelectorAll(".ui-tab-panel");
    function moveSlider(btn) {
      const lr = list.getBoundingClientRect();
      const br = btn.getBoundingClientRect();
      slider.style.width = br.width + "px";
      slider.style.transform = "translateX(" + (br.left - lr.left - 3) + "px)";
    }
    slider.style.transition = "none";
    const initial = list.querySelector(".ui-tab-trigger.active");
    if (initial) requestAnimationFrame(() => { moveSlider(initial); requestAnimationFrame(() => { slider.style.transition = ""; }); });
    triggers.forEach((t, i) => t.addEventListener("click", () => {
      triggers.forEach(x => { x.classList.remove("active"); x.setAttribute("aria-selected", "false"); });
      panels.forEach(p => p.classList.remove("active"));
      t.classList.add("active"); t.setAttribute("aria-selected", "true");
      panels[i].classList.add("active");
      moveSlider(t);
    }));
  });
</script>

Sizes

Default

Small: ui-tabs--sm

<!-- Default -->
<div class="ui-tabs"> ... </div>

<!-- Small -->
<div class="ui-tabs ui-tabs--sm"> ... </div>

Tooltip

Informational label that appears on hover or focus. CSS-only, no JavaScript required for basic usage.

Top tooltip
Bottom tooltip
<!-- Top (default) -->
<div class="ui-tooltip-wrap">
  <button class="ui-btn ui-btn--secondary">Hover me</button>
  <div class="ui-tooltip">Tooltip text</div>
</div>

<!-- Bottom -->
<div class="ui-tooltip-wrap">
  <button class="ui-btn ui-btn--secondary">Hover me</button>
  <div class="ui-tooltip ui-tooltip--bottom">Tooltip text</div>
</div>

Props

Prop / Class Type Default Description
ui-tooltip-wrap class Wrapper that enables hover / focus detection
ui-tooltip class The tooltip bubble, positioned above by default
ui-tooltip--bottom modifier Positions the tooltip below the trigger
side "top" | "bottom" "top" React prop for tooltip placement