diff --git a/modules/label/commerce_pos_label.info.yml b/modules/label/commerce_pos_label.info.yml
new file mode 100644
index 0000000..d93f47b
--- /dev/null
+++ b/modules/label/commerce_pos_label.info.yml
@@ -0,0 +1,8 @@
+name: Commerce Point of Sale Labels
+type: module
+description: Provides the ability to generate and print product labels.
+core: 8.x
+package: Commerce
+dependencies:
+  - commerce
+  - commerce_pos
diff --git a/modules/label/commerce_pos_label.label_formats.yml b/modules/label/commerce_pos_label.label_formats.yml
new file mode 100644
index 0000000..72b7bdb
--- /dev/null
+++ b/modules/label/commerce_pos_label.label_formats.yml
@@ -0,0 +1,6 @@
+commerce_pos_label_30334:
+  title: Dymo 30334 - 1 1/4" x 2 1/4"
+  css: FALSE
+  dimensions:
+    width: 2.25
+    height: 1.0
diff --git a/modules/label/commerce_pos_label.module b/modules/label/commerce_pos_label.module
new file mode 100644
index 0000000..b69b233
--- /dev/null
+++ b/modules/label/commerce_pos_label.module
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains commerce_pos_label.module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function commerce_pos_label_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the commerce_pos_label module.
+    case 'help.page.commerce_pos_label':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Provides the ability to generate and print product labels.') . '</p>';
+
+      $label_formats = commerce_pos_label_get_label_formats();
+
+      // Print out our currently defined label formats.
+      if (!empty($label_formats)) {
+        $output .= '<strong>' . t('Defined label format(s):') . '</strong>';
+
+        foreach ($label_formats as $label_format) {
+          $output .= '<br/>' . $label_format['title']->render();
+        }
+      }
+
+      return $output;
+
+    default:
+  }
+}
+
+/**
+ * Retrieves a list of possible formats that labels can be printed in.
+ *
+ * @param bool $reset
+ *   If TRUE, the cache will be rebuilt.
+ *
+ * @return array
+ *   A list of label formats.
+ */
+function commerce_pos_label_get_label_formats($reset = FALSE) {
+  $cid = 'commerce_pos_label_formats';
+
+  if ($reset) {
+    $cache = \Drupal::cache()->delete($cid);
+  }
+  else {
+    $cache = \Drupal::cache()->get($cid);
+  }
+
+  if ($cache) {
+    $formats = $cache->data;
+  } else {
+    $label_manager = \Drupal::service('plugin.manager.label_format');
+    $formats = $label_manager->getDefinitions();
+
+    \Drupal::cache()->set($cid, $formats);
+  }
+
+  return $formats;
+
+}
+
+/**
+ * Retrieves the definition for a specific label format.
+ */
+function commerce_pos_label_format_load($format_name) {
+  $formats = commerce_pos_label_get_label_formats();
+  return (isset($formats[$format_name]) ? $formats[$format_name] : FALSE);
+}
diff --git a/modules/label/commerce_pos_label.permissions.yml b/modules/label/commerce_pos_label.permissions.yml
new file mode 100644
index 0000000..138c859
--- /dev/null
+++ b/modules/label/commerce_pos_label.permissions.yml
@@ -0,0 +1,2 @@
+commerce pos print labels:
+  title: 'Print labels with Commerce Point of Sale'
diff --git a/modules/label/commerce_pos_label.routing.yml b/modules/label/commerce_pos_label.routing.yml
new file mode 100644
index 0000000..c2ee7be
--- /dev/null
+++ b/modules/label/commerce_pos_label.routing.yml
@@ -0,0 +1,15 @@
+commerce_pos_label.print_labels:
+ path: 'admin/commerce/pos/labels'
+ defaults:
+   _title: 'Print labels'
+   _form: '\Drupal\commerce_pos_label\Form\PrintLabelsForm'
+ requirements:
+   _permission: 'commerce pos print labels'
+
+commerce_pos_label.print_label:
+ path: 'admin/commerce/pos/labels/{product_variation_id}'
+ defaults:
+   _title: 'Print labels'
+   _form: '\Drupal\commerce_pos_label\Form\PrintLabelsForm'
+ requirements:
+   _permission: 'commerce pos print labels'
diff --git a/modules/label/commerce_pos_label.services.yml b/modules/label/commerce_pos_label.services.yml
new file mode 100644
index 0000000..dc6bffd
--- /dev/null
+++ b/modules/label/commerce_pos_label.services.yml
@@ -0,0 +1,4 @@
+services:
+  plugin.manager.label_format:
+    class: Drupal\commerce_pos_label\LabelFormatManager
+    arguments: ['@module_handler', '@cache.discovery']
diff --git a/modules/label/composer.json b/modules/label/composer.json
new file mode 100644
index 0000000..94f2adb
--- /dev/null
+++ b/modules/label/composer.json
@@ -0,0 +1,10 @@
+{
+  "name": "drupal/commerce_pos_label",
+  "type": "drupal-module",
+  "description": "Commerce POS label printing.",
+  "license": "GPL-2.0+",
+  "require": {
+    "picqer/php-barcode-generator": "0.2.2"
+  },
+  "minimum-stability": "dev"
+}
diff --git a/modules/label/src/Form/PrintLabelsForm.php b/modules/label/src/Form/PrintLabelsForm.php
new file mode 100644
index 0000000..fea80cf
--- /dev/null
+++ b/modules/label/src/Form/PrintLabelsForm.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\commerce_pos_label\Form\PrintLabelsForm.
+ */
+namespace Drupal\commerce_pos_label\Form;
+
+use Drupal\commerce_product\Entity\ProductVariation;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DomCrawler\Form;
+use Picqer\Barcode\BarcodeGeneratorHTML;
+
+class PrintLabelsForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'print_labels_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $product_variation_id = NULL) {
+    $product_variation = FALSE;
+    if ($product_variation_id) {
+      if ($product_variation = ProductVariation::load($product_variation_id)) {
+
+        // If we don't have labels to create, add in our passed in product.
+        if (!$form_state->get('labels_to_create')) {
+          $labels_to_create = array();
+          $labels_to_create[$product_variation_id] = $this->build_info_array($product_variation);
+          $form_state->set('labels_to_create', $labels_to_create);
+        }
+      }
+    }
+
+    // If we couldn't find passed in product variation.
+    if ($product_variation === NULL) {
+      drupal_set_message(t('The given product variation (ID: %s) does not exist.', array('%s' => $product_variation_id)), 'error');
+      $product_variation = FALSE;
+    }
+
+    $format_options = array();
+
+    foreach (commerce_pos_label_get_label_formats() as $name => $format) {
+      $format_options[$name] = $format['title'];
+    }
+
+    // We need at least 1 label format to proceed.
+    if (empty($format_options)) {
+      drupal_set_message(t('There are no available label formats. Please enable at least one POS label format module.'), 'error');
+      return $form;
+    }
+
+    $form_wrapper_id = 'commerce-pos-label-form-container';
+
+    $form['#prefix'] = '<div id="' . $form_wrapper_id . '" class="commerce-pos-form-container">';
+    $form['#suffix'] = '</div>';
+
+    $form['label_format'] = array(
+      '#type' => 'select',
+      '#title' => t('Label format'),
+      '#options' => $format_options,
+      '#required' => TRUE,
+    );
+
+    if ($product_variation === FALSE) {
+      // TODO: product search.
+      $form['product_search'] = array(
+        '#type' => 'entity_autocomplete',
+        '#target_type' => 'commerce_product_variation',
+        '#title' => t('product search'),
+        '#title_display' => 'invisible',
+        '#size' => 60,
+        '#description' => t('Search by product title, start typing to begin your search.'),
+        '#attributes' => array(
+          'class' => array(
+            'commerce-pos-product-autocomplete',
+            'commerce-pos-product-search',
+          ),
+          'placeholder' => t('Product Search'),
+        ),
+      );
+
+      // TODO: product add submit
+      $form['product_search_add'] = array(
+        '#type' => 'submit',
+        '#value' => t('Add'),
+        '#validate' => array('::productAddValidate'),
+        '#submit' => array('::productAddSubmit'),
+        '#attributes' => array(
+          'class' => array('commerce-pos-label-btn-add commerce-pos-btn fixed-width btn-success'),
+        ),
+        '#ajax' => array(
+          'wrapper' => $form_wrapper_id,
+          'callback' => '::labelsFormAjax',
+        ),
+      );
+    }
+
+    $form['label_options'] = array(
+      '#type' => 'container',
+      '#id' => 'commerce-pos-label-label-options-container',
+      '#prefix' => '<br>',
+    );
+
+    $form['label_options']['label_list'] = array(
+      '#type' => 'container',
+      '#tree' => TRUE,
+    );
+
+    if ($labels_to_create = $form_state->get('labels_to_create')) {
+      foreach ($labels_to_create as $product_variation_id => $label_info) {
+        
+        $form['label_options']['label_list'][$product_variation_id]['quantity'] = array(
+          '#type' => 'textfield',
+          '#title' => t('Quantity'),
+          '#value' => $label_info['quantity'],
+          '#required' => TRUE,
+          '#size' => 5,
+        );
+
+        $form['label_options']['label_list'][$product_variation_id]['title'] = array(
+          '#title' => t('Title'),
+          '#type' => 'textfield',
+          '#required' => TRUE,
+          '#value' => $label_info['title'],
+        );
+
+        $form['label_options']['label_list'][$product_variation_id]['description'] = array(
+          '#title' => t('Description'),
+          '#type' => 'textfield',
+          '#value' => $label_info['description'],
+        );
+
+        $form['label_options']['label_list'][$product_variation_id]['price'] = array(
+          '#type' => 'textfield',
+          '#title' => t('Price'),
+          '#value' => $label_info['price'],
+          '#required' => TRUE,
+          '#size' => 5,
+        );
+
+        if ($product_variation === FALSE) {
+          // TODO: remove submit
+          $form['label_options']['label_list'][$product_variation_id]['remove'] = array(
+            '#type' => 'submit',
+            '#value' => t('Remove'),
+            '#name' => 'remove-' . $product_variation_id,
+            '#validate' => array('::productRemoveValidate'),
+            '#submit' => array('::productRemoveSubmit'),
+            '#product_id' => $product_variation_id,
+            '#ajax' => array(
+              'wrapper' => $form_wrapper_id,
+              'callback' => '::labelsFormAjax',
+            ),
+            '#attributes' => array(
+              'class' => array('commerce-pos-btn fixed-width btn-danger'),
+            ),
+          );
+        }
+      }
+
+      // TODO: print label submit
+      $form['label_options']['print_labels'] = array(
+        '#type' => 'button',
+        '#value' => t('Print'),
+        '#suffix' => '<div id="product-barcode"></div>',
+        '#ajax' => array(
+          'wrapper' => $form_wrapper_id,
+          'callback' => '::generate_barcode',
+        ),
+        '#attributes' => array(
+          'class' => array('commerce-pos-btn fixed-width'),
+        ),
+      );
+    }
+
+    return $form;
+  }
+
+  /**
+   * Generate barcode from product variation title.
+   */
+  public function generate_barcode($form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    $label_list = current($values['label_list']);
+    $label = $label_list['title'];
+
+    // Generate barcode.
+    $generator = new BarcodeGeneratorHTML();
+    $barcode = $generator->getBarcode($label, $generator::TYPE_CODE_128, 1, 80);
+    $response = new AjaxResponse();
+    $response->addCommand(new ReplaceCommand('#product-barcode', $barcode));
+    return $response;
+  }
+
+  /**
+   * Ajax callback for labels form.
+   *
+   * @param $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *
+   * @return array
+   */
+  public function labelsFormAjax($form, FormStateInterface $form_state) {
+    return $this->buildForm($form, $form_state);
+  }
+
+  /**
+   * Validation for adding a product.
+   *
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public function productAddValidate(array &$form, FormStateInterface $form_state) {
+
+    $product_id = $form_state->getValue('product_search');
+
+    // Load the product to make sure it's valid
+    if (ProductVariation::load($product_id)) {
+      $form_state->set('product_id_to_add', $product_id);
+    }
+    else {
+      $form_state->setError($form['product_search'], t('Invalid product.'));
+    }
+  }
+
+  /**
+   * Submit handler for adding a product.
+   *
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public function productAddSubmit(array &$form, FormStateInterface $form_state) {
+
+    if ($product_id = $form_state->get('product_id_to_add')) {
+
+      // Grab our product info and stick it in storage to be used when the form is rebuilt.
+      $product = ProductVariation::load($product_id);
+      $labels_to_create = (array) $form_state->get('labels_to_create');
+      $labels_to_create[$product_id] = $this->build_info_array($product);
+      $form_state->set('labels_to_create', $labels_to_create);
+
+      // Remove product_id_to_add from storage since it is no longer needed.
+      $storage = $form_state->getStorage();
+      unset($storage['product_id_to_add']);
+      $form_state->setStorage($storage);
+    }
+  }
+
+  /**
+   * Validation for removing a product.
+   *
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public function productRemoveValidate(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * Submit handler for removing a product.
+   *
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_State
+   */
+  public function productRemoveSubmit(array &$form, FormStateInterface $form_State) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    // TODO
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // TODO
+  }
+
+  /**
+   * Build an array of product info used for printing labels via the labels form.
+   *
+   * @param ProductVariation $product_variation
+   *   The product variation to create the array for.
+   *
+   * @return array
+   *   The info array.
+   */
+  protected function build_info_array(ProductVariation $product_variation) {
+    return array(
+      'title' => $product_variation->getTitle(),
+      'quantity' => 1,
+      'price' => round($product_variation->getPrice()->getNumber(), 2),
+      'description' => 'TODO',
+    );
+  }
+
+}
diff --git a/modules/label/src/LabelFormatManager.php b/modules/label/src/LabelFormatManager.php
new file mode 100644
index 0000000..4c075fe
--- /dev/null
+++ b/modules/label/src/LabelFormatManager.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\commerce_pos_label;
+
+use Drupal\commerce_pos_label\Plugin\LabelFormat\LabelFormat;
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+
+/**
+ * Manages discovery and instantiation of label_format plugins.
+ *
+ * @see \Drupal\commerce_pos\Plugin\LabelFormat\LabelFormatInterface
+ * @see plugin_api
+ */
+class LabelFormatManager extends DefaultPluginManager {
+
+  /**
+   * Default values for each plugin.
+   *
+   * @var array
+   */
+  protected $defaults = [
+    'id' => '',
+    'title' => '',
+    'css' => TRUE,
+    'dimensions' => [
+      'width' => 0,
+      'height' => 0,
+    ],
+    'class' => LabelFormat::class,
+  ];
+
+  /**
+   * Constructs a new LabelFormatManager object.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
+    $this->moduleHandler = $module_handler;
+    $this->setCacheBackend($cache_backend, 'label_format', ['label_format']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDiscovery() {
+    if (!isset($this->discovery)) {
+      $this->discovery = new YamlDiscovery('label_formats', $this->moduleHandler->getModuleDirectories());
+      $this->discovery->addTranslatableProperty('title', 'title_context');
+      $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
+    }
+    return $this->discovery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+    $definition['id'] = $plugin_id;
+    if (empty($definition['title'])) {
+      throw new PluginException(sprintf('The label format %s must define the title property.', $plugin_id));
+    }
+    if (empty($definition['dimensions']['width'])) {
+      throw new PluginException(sprintf('The label format %s must define the dimension width property.', $plugin_id));
+    }
+    if (empty($definition['dimensions']['height'])) {
+      throw new PluginException(sprintf('The label format %s must define the dimension height property.', $plugin_id));
+    }
+  }
+
+}
diff --git a/modules/label/src/Plugin/LabelFormat/LabelFormat.php b/modules/label/src/Plugin/LabelFormat/LabelFormat.php
new file mode 100644
index 0000000..79bf0c3
--- /dev/null
+++ b/modules/label/src/Plugin/LabelFormat/LabelFormat.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\commerce_pos_label\Plugin\LabelFormat;
+
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Provides the label format class.
+ */
+class LabelFormat extends PluginBase implements LabelFormatInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getId() {
+    return $this->pluginDefinition['id'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->pluginDefinition['title'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCss() {
+    return $this->pluginDefinition['css'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDimensions() {
+    return $this->pluginDefinition['dimensions'] == TRUE;
+  }
+
+}
diff --git a/modules/label/src/Plugin/LabelFormat/LabelFormatInterface.php b/modules/label/src/Plugin/LabelFormat/LabelFormatInterface.php
new file mode 100644
index 0000000..b2dd95c
--- /dev/null
+++ b/modules/label/src/Plugin/LabelFormat/LabelFormatInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\commerce_pos_label\Plugin\LabelFormat;
+
+/**
+ * Defines the interface for label formats.
+ */
+interface LabelFormatInterface {
+
+  /**
+   * Gets the label format ID.
+   *
+   * @return string
+   *   The label format ID.
+   */
+  public function getId();
+
+  /**
+   * Gets the label format title.
+   *
+   * @return string
+   *   The label format title.
+   */
+  public function getTitle();
+
+  /**
+   * Gets the label format css.
+   *
+   * @return mixed
+   *   The label format css or FALSE if none.
+   */
+  public function getCss();
+
+  /**
+   * Gets the label format dimensions: width and height.
+   *
+   * @return array
+   *   An array containing the width and height.
+   */
+  public function getDimensions();
+
+}
