I Built a Tool That Generates 160 Different Code Combinations from a Single Drag & Drop — Here's How

Published: (December 19, 2025 at 10:45 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

TL;DR

I was sick of writing the same event‑landing‑page code over and over. So I built PromoKit – drag buttons onto an image, pick your framework, get production‑ready code.

10 frameworks × 16 styling options.
Let me walk you through how I built this beast.

The Origin Story: “Can You Move That Button 3 Pixels Up?”

  1. Write the HTML.
  2. Add CSS for positioning, button styles, hover effects.
  3. Done.

Then the email arrives:

  • “Can we move the CTA button slightly to the right?” → adjust CSS → Done.
  • “Actually, can we try it 20 pixels higher?” → adjust CSS again → Done.

What I Built: PromoKit

  • Upload your promotional image
  • Drag & drop buttons, text, and image overlays
  • Pick your framework and styling method
  • Copy production‑ready code

That’s it. No more manual coordinate calculations. No more rewriting the same thing for React, Vue, and Vanilla JS.

Live Demo

https://promotion-page-editor.netlify.app/

Source Code

GitHub – seadonggyun4/promo-kit

The Numbers That Make Me Proud

CategoryDetails
FrameworksReact, Vue, Svelte, Angular, Solid.js, Preact, Astro, Qwik, Lit, Vanilla HTML
Styling OptionsCSS, SCSS, Tailwind, Styled Components, Emotion, CSS Modules, UnoCSS, Panda CSS, …
Presets27 button styles + 5 text styles + 12 image overlays = 44
CombinationsAny framework × any styling = 160 possible stacks

Technical Deep Dive: How It’s Built

src/
├── features/
│   ├── button-editor/            # Button creation & customization
│   ├── text-editor/              # Text element management
│   ├── image-overlay-editor/    # Image overlay handling
│   ├── download/                 # The 2200+‑line code generator
│   └── version-history/         # Undo/redo system
└── shared/
    └── store/                    # Zustand state management

Each feature is completely self‑contained. Want to add a new element type? Create a new feature folder – done. No touching existing code.

State Management: Zustand with Superpowers

// Every mutation automatically logs to history
const updateElement = (id, data) => {
  set(state => {
    const updated = state.elements.map(el =>
      el.id === id ? { ...el, ...data } : el
    );

    // Automatic history tracking!
    pushToHistory(updated, 'Style Changed', 'element_style');

    return { elements: updated };
  });
};

The magic: Users never think about saving state. Every action is automatically reversible. Ctrl+Z just works.

The History System: Command Pattern Done Right

interface HistoryStore {
  past: EditorSnapshot[];      // Previous states
  present: EditorSnapshot;    // Current state
  future: EditorSnapshot[];    // Redo states
}

Each snapshot captures:

  • All elements with their positions and styles
  • Background image (compressed)
  • Currently selected element
  • Timestamp and action description
  • Action type for visual distinction

Debounced History

// During drag operations we don't want 60 snapshots per second
debouncedPushToHistory(
  elements,
  'Position Changed',
  'element_move',
  500   // ms of inactivity before saving
);

When you drag a button, it waits 500 ms of inactivity before saving – smooth UX, no performance hit.

Code Generation Engine: 2200+ Lines of Framework Wizardry

function generateCode(
  framework: Framework,
  styling: StylingMethod,
  elements: Element[],
  options: CodeOptions
): string {
  // 1. Detect element types (gradient buttons need special handling)
  const hasGradients = elements.some(el => isGradientButton(el.style));

  // 2. Generate framework‑specific imports
  const imports = generateImports(framework, styling, hasGradients);

  // 3. Generate element markup per framework
  const markup = elements.map(el =>
    generateElementMarkup(el, framework, styling)
  );

  // 4. Generate styles per styling method
  const styles = generateStyles(elements, styling, options);

  // 5. Assemble into framework structure
  return assembleComponent(framework, imports, markup, styles);
}

The output adapts to everything:

FrameworkEvent SyntaxStyling Integration
ReactonClick={...}Styled Components, Tailwind, etc.
Vue@click="..."Scoped CSS, CSS Modules
Svelteon:click={...}“ blocks or Tailwind
Angular(click)="..."ngClass, ngStyle
Solid.js, Preact, Astro, Qwik, Lit, Vanillaanalogous conventions

Example Outputs

React

export const PromoPage: React.FC = () => {
  return (
    <button onClick={() => {
      window.location.href = '/deal';
    }}>
      Get 50% Off
    </button>
  );
};

Vue (Script Setup)

<template>
  <button @click="handleClick">Get 50% Off</button>
</template>

<script setup>
const handleClick = () => {
  window.location.href = '/deal';
};
</script>

Visual History Timeline

Not just undo/redo buttons – a full visual timeline where you can:

  • See what each action was (with icons)
  • Click any point to jump there
  • View how many past/future states exist

It’s like Git for your design.

Smart Presets

44 presets organized by category:

CategoryCountExamples
Simple Buttons11Clean, minimal designs
Gradient Buttons10Eye‑catching gradients
Animated Buttons6Bounce, glow, pulse, shake, slide, ripple

Each preset is fully customizable after placement.

Real‑Time Preview

(Live preview pane updates as you edit.)

Accessibility Built‑In

  • Semantic tags (<button>, <a>, etc.)
  • aria-label attributes
  • aria-describedby for extended descriptions
  • role attributes where needed
  • :focus-visible styles for keyboard users
  • Screen‑reader‑only content

Responsive Code Generation

Toggle “responsive” and the generated code includes media queries automatically:

@media (max-width: 768px) {
  .button {
    transform: scale(0.85);
    font-size: 0.9rem;
  }
}

@media (max-width: 640px) {
  .button {
    transform: scale(0.7);
    font-size: 0.8rem;
  }
}

Your landing page works everywhere, automatically.

SEO Meta Tags

Need Open Graph tags? Twitter Cards? Canonical URLs? One toggle. Done.

Architecture Choices

Zustand over Redux

  • Simpler API
  • Built‑in history tracking (as shown above)

Feature‑Sliced Design

  • Clear separation of concerns
  • Easy to scale

Composition over Inheritance for Hooks

// Base form logic
const baseForm = useButtonForm();

// Extended for animated buttons
const animatedForm = useAnimatedBtn(); // Internally uses useButtonForm

Clean, testable, and DRY.

Dynamic Imports for Circular Dependencies

const getBackgroundImage = async () => {
  const module = await import('./background');
  return module.default;
};

Miscellaneous Code Snippet

// Retrieve the uploaded image from the Zustand store
() => {
  const { useUploadImageStore } = require('./uploadImageStore');
  return useUploadImageStore.getState().uploadedImage;
};

Not the prettiest, but it solved a real problem elegantly.

Tech Stack

  • React 18 with TypeScript
  • Vite – blazing‑fast development server
  • Zustand – state management
  • styled‑components – styling
  • dnd‑kit – drag‑and‑drop
  • react‑i18next – Korean, English, Japanese support
  • JSZip + FileSaver – ZIP downloads

Demo & Source


⭐️ Drop a star if you think this is cool.
🐞 Open issues for any missing frameworks you’d like to see.
🔧 Pull requests are very welcome!

Built with mass frustration, mass coffee, and the firm belief that developers shouldn’t write the same CSS twice.

Back to Blog

Related posts

Read more »

Best SaaS Dashboard Templates

Modern SaaS Dashboard Templates – A Developer’s Decision Guide Modern SaaS products live and die by how clearly they show data. Founders, growth teams, and cus...