diff --git a/commerce_pos.libraries.yml b/commerce_pos.libraries.yml
index 43de9dc..7b54374 100644
--- a/commerce_pos.libraries.yml
+++ b/commerce_pos.libraries.yml
@@ -8,3 +8,12 @@ form:
   dependencies:
     - core/jquery
     - core/drupal
+jQuery.print:
+  remote: https://github.com/DoersGuild/jQuery.print
+  version: 1.5.1
+  license:
+    name: CC BY 3.0
+    url: https://github.com/DoersGuild/jQuery.print/blob/master/LICENSE
+    gpl-compatible: no
+  js:
+    /libraries/jQuery.print/jQuery.print.js: {}
diff --git a/commerce_pos.services.yml b/commerce_pos.services.yml
index 61acd7c..12da1a8 100755
--- a/commerce_pos.services.yml
+++ b/commerce_pos.services.yml
@@ -8,6 +8,7 @@ services:
 
   commerce_pos.upc:
     class: Drupal\commerce_pos\UPC
+    arguments: ['@entity_type.manager']
 
   commerce_pos.current_order:
     class: Drupal\commerce_pos\CurrentOrder
diff --git a/modules/label/README.md b/modules/label/README.md
new file mode 100644
index 0000000..fa5a549
--- /dev/null
+++ b/modules/label/README.md
@@ -0,0 +1,6 @@
+# Commerce POS Label
+Provides the ability to print labels for products.
+
+## Using the module
+
+## Supporting new label types.
diff --git a/modules/label/commerce_pos_label.info.yml b/modules/label/commerce_pos_label.info.yml
new file mode 100644
index 0000000..5a0d71d
--- /dev/null
+++ b/modules/label/commerce_pos_label.info.yml
@@ -0,0 +1,7 @@
+name: Commerce Point of Sale Labels
+type: module
+description: Provides the ability to generate and print product labels.
+core: 8.x
+package: Commerce (POS)
+dependencies:
+  - 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..751dfe8
--- /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: 'css/commerce_pos_label.css'
+  dimensions:
+    width: 2.25
+    height: 1.0
diff --git a/modules/label/commerce_pos_label.libraries.yml b/modules/label/commerce_pos_label.libraries.yml
new file mode 100755
index 0000000..56379f0
--- /dev/null
+++ b/modules/label/commerce_pos_label.libraries.yml
@@ -0,0 +1,9 @@
+label:
+  version: 1.x
+  js:
+    js/commerce_pos_label.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupalSettings
+    - core/drupal.ajax
diff --git a/modules/label/commerce_pos_label.links.menu.yml b/modules/label/commerce_pos_label.links.menu.yml
new file mode 100755
index 0000000..6c60d0b
--- /dev/null
+++ b/modules/label/commerce_pos_label.links.menu.yml
@@ -0,0 +1,5 @@
+commerce_pos_label.print:
+  title: 'Print Labels'
+  parent: 'commerce_pos.main'
+  description: 'Print product labels'
+  route_name: 'commerce_pos_label.print_labels'
diff --git a/modules/label/commerce_pos_label.module b/modules/label/commerce_pos_label.module
new file mode 100644
index 0000000..60c51ce
--- /dev/null
+++ b/modules/label/commerce_pos_label.module
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Provides the ability to print labels for products.
+ */
+
+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:
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_pos_label_theme() {
+  $theme['commerce_pos_labels'] = [
+    'variables' => [
+      'labels' => [],
+    ],
+    'template' => 'commerce-pos-labels',
+  ];
+
+  $theme['commerce_pos_label'] = [
+    'variables' => [
+      'barcode' => NULL,
+      'description' => NULL,
+      'format' => NULL,
+      'price' => NULL,
+      'product_id' => NULL,
+      'title' => NULL,
+    ],
+    'template' => 'commerce-pos-label',
+  ];
+
+  return $theme;
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function commerce_pos_label_theme_suggestions_commerce_pos_label(array $variables) {
+  $suggestions = [];
+  if (isset($variables['format'])) {
+    $suggestions[] = 'commerce_pos_label__' . $variables['format'];
+  }
+  return $suggestions;
+}
+
+/**
+ * Retrieves a list of possible formats that labels can be printed in.
+ *
+ * @return array
+ *   A list of label formats.
+ */
+function commerce_pos_label_get_label_formats() {
+  /** @var \Drupal\commerce_pos_label\LabelFormatManager $label_manager */
+  $label_manager = \Drupal::service('plugin.manager.label_format');
+  return $label_manager->getDefinitions();
+}
+
+/**
+ * Retrieves the definition for a specific label format.
+ *
+ * @param string $format_name
+ *   The ID of a label format.
+ *
+ * @return array|false
+ *   A label format definition or FALSE.
+ */
+function commerce_pos_label_format_load($format_name) {
+  $formats = commerce_pos_label_get_label_formats();
+  return (isset($formats[$format_name]) ? $formats[$format_name] : FALSE);
+}
+
+/**
+ * Implements hook_preprocess_HOOK().
+ */
+function commerce_pos_label_preprocess_commerce_pos_label(&$variables) {
+  // Generate a barcode for a label's UPC.
+  /** @var \Drupal\commerce_pos_label\BarcodeGenerator $barcodeGenerator */
+  $barcodeGenerator = \Drupal::getContainer()->get('commerce_pos_label.barcode_generator');
+  if (!empty($variables['barcode'])) {
+    $barcode = $barcodeGenerator->png($variables['barcode']);
+    if ($barcode) {
+      $variables['barcode'] = '<img src="data:image/png;base64,' . base64_encode($barcode) . '">';
+    }
+    else {
+      // If we fail to generate the barcode, omit it.
+      $variables['barcode'] = '';
+    }
+  }
+}
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..5c63362
--- /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/{commerce_product_variation}'
+ 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..fa6ffd1
--- /dev/null
+++ b/modules/label/commerce_pos_label.services.yml
@@ -0,0 +1,6 @@
+services:
+  plugin.manager.label_format:
+    class: Drupal\commerce_pos_label\LabelFormatManager
+    arguments: ['@module_handler', '@cache.discovery']
+  commerce_pos_label.barcode_generator:
+    class: Drupal\commerce_pos_label\BarcodeGenerator
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/css/commerce_pos_label.css b/modules/label/css/commerce_pos_label.css
new file mode 100644
index 0000000..54b41ed
--- /dev/null
+++ b/modules/label/css/commerce_pos_label.css
@@ -0,0 +1,38 @@
+@page {
+  margin: 2mm 0 0 0;
+  padding: 0;
+  font-size: 12px; }
+body {
+  font-size: 12px; }
+
+.commerce-pos-label {
+  padding: 0;
+  font-family: Arial, sans-serif;
+  margin: 0 auto;
+  text-align: center;
+  font-size: 12px;
+  text-transform: uppercase; }
+  @media print {
+    .commerce-pos-label {
+      page-break-after: always; } }
+  .commerce-pos-label * {
+    margin: 0;
+    padding: 0; }
+  .commerce-pos-label .barcode {
+    margin: 0.4em 0; }
+  .commerce-pos-label .description {
+    font-size: 0.85em; }
+  .commerce-pos-label.no-description .barcode {
+    margin: 0.9em 0; }
+  .commerce-pos-label img {
+    display: inline-block; }
+  .commerce-pos-label div {
+    white-space: nowrap;
+    width: 100%;
+    overflow: hidden;
+    -ms-text-overflow: ellipsis;
+    text-overflow: ellipsis; }
+    .commerce-pos-label div.price {
+      font-size: 2em; }
+
+/*# sourceMappingURL=commerce_pos_label.css.map */
diff --git a/modules/label/css/commerce_pos_label.css.map b/modules/label/css/commerce_pos_label.css.map
new file mode 100644
index 0000000..1927ac8
--- /dev/null
+++ b/modules/label/css/commerce_pos_label.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAAA,KAIC;EAHC,MAAM,EAAE,SAAS;EACjB,OAAO,EAAE,CAAC;EACV,SAAS,EAAE,IAAI;AAGjB,IAAK;EACH,SAAS,EAAE,IAAI;;AAGjB,mBAAoB;EAClB,OAAO,EAAE,CAAC;EAWV,WAAW,EAAE,iBAAiB;EAC9B,MAAM,EAAE,MAAM;EACd,UAAU,EAAE,MAAM;EAClB,SAAS,EAAE,IAAI;EACf,cAAc,EAAE,SAAS;EAbzB,YAAa;IAHf,mBAAoB;MAIhB,gBAAgB,EAAE,MAAM;EAG1B,qBAAE;IACA,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,CAAC;EASZ,4BAAS;IACP,MAAM,EAAE,OAAO;EAGjB,gCAAa;IACX,SAAS,EAAE,MAAM;EAIjB,2CAAS;IACP,MAAM,EAAE,OAAQ;EAIpB,uBAAI;IACF,OAAO,EAAE,YAAY;EAGvB,uBAAI;IACF,WAAW,EAAE,MAAM;IACnB,KAAK,EAAE,IAAI;IACX,QAAQ,EAAE,MAAM;IAChB,iBAAiB,EAAE,QAAQ;IAC3B,aAAa,EAAE,QAAQ;IAEvB,6BAAQ;MACN,SAAS,EAAE,GAAG",
+"sources": ["../sass/commerce_pos_label.scss"],
+"names": [],
+"file": "commerce_pos_label.css"
+}
\ No newline at end of file
diff --git a/modules/label/js/commerce_pos_label.js b/modules/label/js/commerce_pos_label.js
new file mode 100644
index 0000000..2496076
--- /dev/null
+++ b/modules/label/js/commerce_pos_label.js
@@ -0,0 +1,20 @@
+(function ($, Drupal, drupalSettings) {
+
+  /**
+   * Ajax command to set the toolbar subtrees.
+   *
+   * @param {Drupal.Ajax} ajax
+   *   {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
+   * @param {object} response
+   *   JSON response from the Ajax request.
+   * @param {number} [status]
+   *   XMLHttpRequest status.
+   */
+  Drupal.AjaxCommands.prototype.printLabels = function (ajax, response, status) {
+    $(response.content).print({
+      globalStyles: false,
+      stylesheet: response.cssUrl
+    });
+  };
+
+}(jQuery, Drupal, drupalSettings));
diff --git a/modules/label/sass/commerce_pos_label.scss b/modules/label/sass/commerce_pos_label.scss
new file mode 100644
index 0000000..eb931bf
--- /dev/null
+++ b/modules/label/sass/commerce_pos_label.scss
@@ -0,0 +1,58 @@
+@page {
+  margin: 2mm 0 0 0;
+  padding: 0;
+  font-size: 12px;
+}
+
+body {
+  font-size: 12px;
+}
+
+.commerce-pos-label {
+  padding: 0;
+
+  @media print {
+    page-break-after: always;
+  }
+
+  * {
+    margin: 0;
+    padding: 0;
+  }
+
+  font-family: Arial, sans-serif;
+  margin: 0 auto;
+  text-align: center;
+  font-size: 12px;
+  text-transform: uppercase;
+
+  .barcode {
+    margin: 0.4em 0;
+  }
+
+  .description {
+    font-size: 0.85em;
+  }
+
+  &.no-description {
+    .barcode {
+      margin: 0.9em 0 ;
+    }
+  }
+
+  img {
+    display: inline-block;
+  }
+
+  div {
+    white-space: nowrap;
+    width: 100%;
+    overflow: hidden;
+    -ms-text-overflow: ellipsis;
+    text-overflow: ellipsis;
+
+    &.price {
+      font-size: 2em;
+    }
+  }
+}
diff --git a/modules/label/src/Ajax/PrintLabelsCommand.php b/modules/label/src/Ajax/PrintLabelsCommand.php
new file mode 100644
index 0000000..856fac0
--- /dev/null
+++ b/modules/label/src/Ajax/PrintLabelsCommand.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\commerce_pos_label\Ajax;
+
+use Drupal\Core\Url;
+use Drupal\Core\Ajax\CommandInterface;
+use Drupal\Component\Render\MarkupInterface;
+
+/**
+ * AJAX command for retrieving data and printing labels.
+ */
+class PrintLabelsCommand implements CommandInterface {
+
+  /**
+   * Rendered labels to print.
+   *
+   * @var \Drupal\Component\Render\MarkupInterface
+   */
+  protected $labels;
+
+  /**
+   * The path to the CSS file for the label format.
+   *
+   * @var string
+   */
+  protected $cssUrl;
+
+  /**
+   * PrintLabelsCommand constructor.
+   *
+   * @param \Drupal\Component\Render\MarkupInterface $labels
+   *   Rendered labels to print.
+   * @param string $format
+   *   The label format to print.
+   */
+  public function __construct(MarkupInterface $labels, $format) {
+    $this->labels = $labels;
+
+    $format = commerce_pos_label_format_load($format);
+    $css = $format['css'];
+    if ($css) {
+      $provider_path = \Drupal::service('module_handler')->getModule($format['provider'])->getPath();
+      $css = Url::fromUri('base:' . $provider_path . '/' . $css, ['absolute' => TRUE])->toString();
+    }
+    $this->cssUrl = $css;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return [
+      'command' => 'printLabels',
+      'content' => $this->labels,
+      'cssUrl' => $this->cssUrl,
+    ];
+  }
+
+}
diff --git a/modules/label/src/BarcodeGenerator.php b/modules/label/src/BarcodeGenerator.php
new file mode 100644
index 0000000..1b259ec
--- /dev/null
+++ b/modules/label/src/BarcodeGenerator.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\commerce_pos_label;
+
+use Picqer\Barcode\BarcodeGenerator as ExternalGenerator;
+use Picqer\Barcode\BarcodeGeneratorPNG;
+use Picqer\Barcode\Exceptions\BarcodeException;
+
+/**
+ * Class BarcodeGenerator.
+ *
+ * A service for generating barcodes.
+ *
+ * @package Drupal\commerce_pos_label
+ */
+class BarcodeGenerator implements BarcodeGeneratorInterface {
+
+  /**
+   * Determine the type of a UPC.
+   *
+   * @param string $upc
+   *   A UPC.
+   *
+   * @return string|false
+   *   A UPC type or false if none could be determined from the UPC length.
+   */
+  public function type($upc) {
+    $map = [
+      8 => ExternalGenerator::TYPE_EAN_8,
+      12 => ExternalGenerator::TYPE_UPC_A,
+      13 => ExternalGenerator::TYPE_EAN_13,
+    ];
+
+    $length = strlen($upc);
+
+    return (isset($map[$length]))? $map[$length] : FALSE;
+  }
+
+  /**
+   * Render a UPC into a PNG barcode.
+   *
+   * @param string $upc
+   *   A UPC.
+   * @param int $widthFactor
+   *   Width of a single bar element in pixels.
+   * @param int $totalHeight
+   *   Height of a single bar element in pixels.
+   *
+   * @return string
+   *   The barcode as raw PNG data.
+   */
+  public function png($upc, $widthFactor = 2, $totalHeight = 40) {
+    $type = $this->type($upc);
+    if ($type) {
+      $generator = new BarcodeGeneratorPNG();
+      try {
+        return $generator->getBarcode($upc, $type, $widthFactor, $totalHeight);
+      }
+      catch (BarcodeException $e) {
+        return FALSE;
+      }
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/modules/label/src/BarcodeGeneratorInterface.php b/modules/label/src/BarcodeGeneratorInterface.php
new file mode 100644
index 0000000..5c9f178
--- /dev/null
+++ b/modules/label/src/BarcodeGeneratorInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\commerce_pos_label;
+
+/**
+ * Defines the interface for a barcode generator.
+ *
+ * @package Drupal\commerce_pos_label
+ */
+interface BarcodeGeneratorInterface {
+
+  /**
+   * Render a UPC into a PNG barcode.
+   *
+   * @param string $upc
+   *   A UPC.
+   * @param int $widthFactor
+   *   Width of a single bar element in pixels.
+   * @param int $totalHeight
+   *   Height of a single bar element in pixels.
+   *
+   * @return string
+   *   The barcode as raw PNG data.
+   */
+  public function png($upc, $widthFactor, $totalHeight);
+
+}
diff --git a/modules/label/src/Form/PrintLabelsForm.php b/modules/label/src/Form/PrintLabelsForm.php
new file mode 100644
index 0000000..ab4b512
--- /dev/null
+++ b/modules/label/src/Form/PrintLabelsForm.php
@@ -0,0 +1,383 @@
+<?php
+
+namespace Drupal\commerce_pos_label\Form;
+
+use Drupal\commerce_pos\UPC;
+use Drupal\commerce_pos_label\Ajax\PrintLabelsCommand;
+use Drupal\commerce_price\NumberFormatterFactoryInterface;
+use Drupal\commerce_price\RounderInterface;
+use Drupal\commerce_product\Entity\ProductVariationInterface;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Class PrintLabelsForm.
+ *
+ * Builds a list of products to print labels for.
+ */
+class PrintLabelsForm extends FormBase {
+
+  /**
+   * Storage for commerce_product_variation entities.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $productVariationStorage;
+
+  /**
+   * Storage for currency entities.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $currencyStorage;
+
+  /**
+   * The rendering service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * A rounding services for prices.
+   *
+   * @var \Drupal\commerce_price\RounderInterface
+   */
+  protected $rounder;
+
+  /**
+   * A number formatter for currencies.
+   *
+   * @var \CommerceGuys\Intl\Formatter\NumberFormatterInterface
+   */
+  protected $formatter;
+
+  /**
+   * A service for looking up product UPCs.
+   *
+   * @var \Drupal\commerce_pos\UPC
+   */
+  protected $upc;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'commerce_pos_label_print_form';
+  }
+
+  /**
+   * PrintLabelsForm constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The rendering service.
+   * @param \Drupal\commerce_price\RounderInterface $rounder
+   *   A rounding services for prices.
+   * @param \Drupal\commerce_price\NumberFormatterFactoryInterface $formatterFactory
+   *   A factory for creating price formatters.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   *   If unable to get the storage for product variations.
+   */
+  public function __construct(EntityTypeManagerInterface $entityTypeManager, RendererInterface $renderer, RounderInterface $rounder, NumberFormatterFactoryInterface $formatterFactory, UPC $upc) {
+    $this->productVariationStorage = $entityTypeManager->getStorage('commerce_product_variation');
+    $this->currencyStorage = $entityTypeManager->getStorage('commerce_currency');
+    $this->renderer = $renderer;
+    $this->rounder = $rounder;
+    $this->formatter = $formatterFactory->createInstance();
+    $this->upc = $upc;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('renderer'),
+      $container->get('commerce_price.rounder'),
+      $container->get('commerce_price.number_formatter_factory'),
+      $container->get('commerce_pos.upc')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, ProductVariationInterface $commerce_product_variation = NULL) {
+    $product_variation = $commerce_product_variation;
+
+    $form['#attached']['library'][] = 'commerce_pos_label/label';
+    $form['#attached']['library'][] = 'commerce_pos/jQuery.print';
+
+    $labels_to_create = $this->getLabelList($form_state);
+    if ($product_variation && !$labels_to_create) {
+      $labels_to_create[$product_variation->id()] = $this->buildInfoArray($product_variation);
+      $form_state->setValue('label_list', $labels_to_create);
+    }
+
+    $format_options = array_map(function ($format) {
+      return $format['title'];
+    }, commerce_pos_label_get_label_formats());
+
+    // We need at least 1 label format to proceed.
+    if (empty($format_options)) {
+      drupal_set_message($this->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['#attributes'] = [
+      'id' => $form_wrapper_id,
+      'class' => ['commerce-pos-form-container'],
+    ];
+
+    $form['label_format'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Label format'),
+      '#options' => $format_options,
+      '#required' => TRUE,
+      '#default_value' => key($format_options),
+    ];
+
+    $form['product_search'] = [
+      '#type' => 'entity_autocomplete',
+      '#target_type' => 'commerce_product_variation',
+      '#title' => t('Product Search'),
+      '#title_display' => 'invisible',
+      '#size' => 60,
+      '#description' => $this->t('Search by product title.'),
+      '#attributes' => [
+        'class' => [
+          'commerce-pos-product-autocomplete',
+          'commerce-pos-product-search',
+        ],
+        'placeholder' => $this->t('Product Search'),
+      ],
+    ];
+
+    $form['product_search_add'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add'),
+      '#validate' => ['::productAddValidate'],
+      '#submit' => ['::productAddSubmit'],
+      '#attributes' => [
+        'class' => ['commerce-pos-label-btn-add commerce-pos-btn fixed-width btn-success'],
+      ],
+    ];
+
+    $form['label_options'] = [
+      '#type' => 'container',
+    ];
+
+    $form['label_options']['label_list'] = [
+      '#type' => 'table',
+      '#header' => [
+        $this->t('Quantity'),
+        $this->t('Title'),
+        $this->t('Description'),
+        $this->t('Price'),
+        $this->t('Remove'),
+      ],
+      '#tree' => TRUE,
+    ];
+
+    foreach ($labels_to_create as $product_variation_id => $label_info) {
+
+      $form['label_options']['label_list'][$product_variation_id]['quantity'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Quantity'),
+        '#value' => $label_info['quantity'],
+        '#required' => TRUE,
+        '#size' => 5,
+      ];
+
+      $form['label_options']['label_list'][$product_variation_id]['title'] = [
+        '#title' => $this->t('Title'),
+        '#type' => 'textfield',
+        '#required' => TRUE,
+        '#value' => $label_info['title'],
+      ];
+
+      $form['label_options']['label_list'][$product_variation_id]['description'] = [
+        '#title' => $this->t('Description'),
+        '#type' => 'textfield',
+        '#value' => $label_info['description'],
+      ];
+
+      $form['label_options']['label_list'][$product_variation_id]['price'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Price'),
+        '#value' => $label_info['price'],
+        '#required' => TRUE,
+        '#size' => 5,
+      ];
+
+      $form['label_options']['label_list'][$product_variation_id]['remove'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Remove'),
+        '#name' => 'remove-' . $product_variation_id,
+        '#validate' => ['::productRemoveValidate'],
+        '#submit' => ['::productRemoveSubmit'],
+        '#product_id' => $product_variation_id,
+        '#attributes' => [
+          'class' => ['commerce-pos-btn fixed-width btn-danger'],
+        ],
+      ];
+    }
+
+    if ($labels_to_create) {
+      $form['label_options']['print_labels'] = [
+        '#type' => 'button',
+        '#value' => $this->t('Print'),
+        '#ajax' => [
+          'callback' => '::printLabels',
+        ],
+        '#attributes' => [
+          'class' => ['commerce-pos-btn fixed-width'],
+        ],
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * Generate barcode from product variation title.
+   */
+  public function printLabels($form, FormStateInterface $form_state) {
+    $labels = [
+      '#theme' => 'commerce_pos_labels',
+      '#labels' => [],
+    ];
+    $label_values = $this->getLabelList($form_state);
+    $label_format = $form_state->getValue('label_format');
+
+    // Convert form values into a render array to theme the labels.
+    foreach ($label_values as $product_id => $label) {
+      unset($label['remove']);
+      foreach ($label as $key => $value) {
+        $label['#' . $key] = $value;
+        unset($label[$key]);
+      }
+
+      $label['#product_id'] = $product_id;
+      $label['#theme'] = 'commerce_pos_label';
+      $label['#format'] = $label_format;
+      $label['#barcode'] = $this->upc->get($product_id);
+
+      for ($i = 0; $i < $label['#quantity']; $i++) {
+        $labels['#labels'][] = $label;
+      }
+    }
+
+    $labels = $this->renderer->render($labels);
+
+    $response = new AjaxResponse();
+    $response->addCommand(new PrintLabelsCommand($labels, $label_format));
+    return $response;
+  }
+
+  /**
+   * Validation for adding a product.
+   */
+  public function productAddValidate(array &$form, FormStateInterface $form_state) {
+    $product_id = $form_state->getValue('product_search');
+
+    if (empty($product_id) || !$this->productVariationStorage->load($product_id)) {
+      $form_state->setError($form['product_search'], $this->t('Invalid product.'));
+    }
+  }
+
+  /**
+   * Submit handler for adding a product.
+   */
+  public function productAddSubmit(array &$form, FormStateInterface $form_state) {
+    $product_id = $form_state->getValue('product_search');
+
+    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $product */
+    $product = $this->productVariationStorage->load($product_id);
+    $labels = $this->getLabelList($form_state);
+    if (empty($labels[$product_id])) {
+      $labels[$product_id] = $this->buildInfoArray($product);
+      $form_state->setValue('label_list', $labels);
+    }
+    $form_state->setRebuild(TRUE);
+  }
+
+  /**
+   * Validation for adding a product.
+   */
+  public function productRemoveValidate(array &$form, FormStateInterface $form_state) {
+    $trigger = $form_state->getTriggeringElement();
+    if (!empty($trigger['#product_id'])) {
+      $labels = $this->getLabelList($form_state);
+      if (!isset($labels[$trigger['#product_id']])) {
+        $form_state->setError($form['label_list'], $this->t('Can not remove a product that is was added.'));
+      }
+    }
+  }
+
+  /**
+   * Submit handler for removing a product.
+   */
+  public function productRemoveSubmit(array &$form, FormStateInterface $form_state) {
+    $trigger = $form_state->getTriggeringElement();
+    if (!empty($trigger['#product_id'])) {
+      $labels = $this->getLabelList($form_state);
+      unset($labels[$trigger['#product_id']]);
+      $form_state->setValue('label_list', $labels);
+    }
+    $form_state->setRebuild(TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $form_state->setRebuild(TRUE);
+  }
+
+  /**
+   * Build an array of product info used for printing labels.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface $product_variation
+   *   The product variation to create the array for.
+   *
+   * @return array
+   *   The info array.
+   */
+  protected function buildInfoArray(ProductVariationInterface $product_variation) {
+    $price = $product_variation->getPrice();
+    /** @var \Drupal\commerce_price\Entity\Currency $currency */
+    $currency = $this->currencyStorage->load($price->getCurrencyCode());
+
+    return [
+      'title' => $product_variation->getSku(),
+      'quantity' => 1,
+      'price' => $this->formatter->formatCurrency($this->rounder->round($price)->getNumber(), $currency),
+      'description' => $product_variation->getTitle(),
+    ];
+  }
+
+  /**
+   * Get the label list value.
+   *
+   * Ensures that when label_list is empty we get an empty array instead of an
+   * empty string.
+   *
+   * @return array
+   *   An array of form elements for the label list.
+   */
+  protected function getLabelList(FormStateInterface $form_state) {
+    return $form_state->getValue('label_list') ?: [];
+  }
+
+}
diff --git a/modules/label/src/LabelFormatManager.php b/modules/label/src/LabelFormatManager.php
new file mode 100644
index 0000000..7a689ac
--- /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_label\Plugin\LabelFormat\LabelFormatInterface
+ * @see plugin_api
+ */
+class LabelFormatManager extends DefaultPluginManager {
+
+  /**
+   * Default values for each plugin.
+   *
+   * @var array
+   */
+  protected $defaults = [
+    'id' => '',
+    'title' => '',
+    'css' => FALSE,
+    '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..3f9381e
--- /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'];
+  }
+
+}
diff --git a/modules/label/src/Plugin/LabelFormat/LabelFormatInterface.php b/modules/label/src/Plugin/LabelFormat/LabelFormatInterface.php
new file mode 100644
index 0000000..6728fb9
--- /dev/null
+++ b/modules/label/src/Plugin/LabelFormat/LabelFormatInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\commerce_pos_label\Plugin\LabelFormat;
+
+/**
+ * Defines the interface for label formats.
+ */
+interface LabelFormatInterface {
+
+  /**
+   * Get the label format's ID.
+   *
+   * @return string
+   *   The label format ID.
+   */
+  public function getId();
+
+  /**
+   * Get the label formats' title.
+   *
+   * @return string
+   *   The label format title.
+   */
+  public function getTitle();
+
+  /**
+   * Get the path to the label format's css file.
+   *
+   * The path must be relative to the module that provides the format.
+   *
+   * @return string|false
+   *   The label format css or FALSE if none.
+   */
+  public function getCss();
+
+  /**
+   * Get the label format's dimensions: width and height.
+   *
+   * @return array
+   *   An array containing the width and height.
+   */
+  public function getDimensions();
+
+}
diff --git a/modules/label/templates/commerce-pos-label.html.twig b/modules/label/templates/commerce-pos-label.html.twig
new file mode 100644
index 0000000..3a0924f
--- /dev/null
+++ b/modules/label/templates/commerce-pos-label.html.twig
@@ -0,0 +1,29 @@
+{#
+/**
+ * @file
+ * Default template for a single labels.
+ *
+ * Available variables:
+ * - barcode
+ * - description
+ * - price
+ * - title
+ *
+ * @ingroup themeable
+ */
+#}
+
+<div class="commerce-pos-label">
+    <div class="title">
+        {{ title }}
+    </div>
+    <div class="description">
+        {{ description }}
+    </div>
+    <div class="price">
+        {{ price }}
+    </div>
+    <div class="barcode">
+        {{ barcode | raw }}
+    </div>
+</div>
diff --git a/modules/label/templates/commerce-pos-labels.html.twig b/modules/label/templates/commerce-pos-labels.html.twig
new file mode 100644
index 0000000..6b6c0f7
--- /dev/null
+++ b/modules/label/templates/commerce-pos-labels.html.twig
@@ -0,0 +1,15 @@
+{#
+/**
+ * @file
+ * Labels document.
+ *
+ * Available variables:
+ * - labels: The labels.
+ *
+ * @ingroup themeable
+ */
+#}
+
+<div class="commerce-pos-labels">
+    {{ labels }}
+</div>
diff --git a/src/UPC.php b/src/UPC.php
index e87df83..f1eed7e 100644
--- a/src/UPC.php
+++ b/src/UPC.php
@@ -2,12 +2,35 @@
 
 namespace Drupal\commerce_pos;
 
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
+
 /**
  * Get any product variations with the provided UPC.
  */
 class UPC {
 
   /**
+   * Entity storage for product variations.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $productVariationStorage;
+
+  /**
+   * UPC constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   An entity type manager to get entity storage.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   *   If unable to get the product variation storage.
+   */
+  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
+    $this->productVariationStorage = $entityTypeManager->getStorage('commerce_product_variation');
+  }
+
+  /**
    * Look up product variations by UPC.
    *
    * @param string $upc
@@ -23,9 +46,36 @@ class UPC {
 
     $ids = $query->execute();
 
-    $variations = \Drupal::entityTypeManager()->getStorage('commerce_product_variation')->loadMultiple($ids);
+    return $this->productVariationStorage->loadMultiple($ids);
+  }
+
+  /**
+   * Get a product's UPC.
+   *
+   * @param int $product_id
+   *   The ID of a commerce_product_variation entity.
+   *
+   * @return string|null
+   *   The UPC, if there is one.
+   */
+  public function get($product_id) {
+    $upc = NULL;
+    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $product */
+    $product = $this->productVariationStorage->load($product_id);
+    if ($product->hasField('field_upc')) {
+      try {
+        /** @var \Drupal\text\Plugin\Field\FieldType\TextItem $upc */
+        $upc = $product->get('field_upc')->first();
+      }
+      catch (MissingDataException $e) {
+        return NULL;
+      }
 
-    return $variations;
+      if ($upc) {
+        $upc = $upc->getValue()['value'];
+      }
+    }
+    return $upc;
   }
 
 }
