- Reguirements/Technologies
- Magento app installation
- Setup frontend workflow
- Magento file system structure
- Theme anatomy
- Theme fallback
- Create custom theme
- Layout XML: how to edit your changes
- Layout XML: Syntax
- Layout XML: XSD
- Page layouts
- PHTML
- Excursus: Blocks
- Pre2processing in Magento 2
- How to compile your CSS
- How to edit your CSS
- Customizing the built in workflow
- Media Queries
- Built in CSS Library
- Built-in jQuery widgets
- Where to go
Check the official docuentation for detailed system requirements. Test
❗Always use PHP7 in favour, since development will be waaaaaaaaay faster. ❗
Official Developer Documentation
Have a look at my Blog Post with detailed instructions.
Initialize new project:
composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition <installation directory name>
Change to directory and ensure Magento CLI is executable:
cd <installation directory name> && chmod +x bin/magento
Install App using CLI:
bin/magento setup:install --base-url=http://awsome-shop.local/ --language=de_DE --backend-frontname=admin --db-host=localhost --db-name=DBNAME --db-user=DBUSER --db-password=DBPASSWORD --admin-email=max@mustermann.de --admin-user=maxmuster --admin-password=PASSWORD --admin-firstname=Max --admin-lastname=Mustermann
If you encounter issues with XDebug maximum nesting level, try setting xdebug.max_nesting_level
in php.ini or use following bash command:
php -d xdebug.max_nesting_level[=250] bin/magento setup:install <args>
Set Developer Mode:
bin/magento deploy:mode:set developer
Install Demo Data (optional):
bin/magento sampledata:deploy && bin/magento setup:upgrade
Verify Node.js and Grunt CLI are installed on your system:
$ grunt
Running "default" task
I'm default task and at the moment I'm empty, sorry :/
Done, without errors.
Execution Time (2016-02-01 08:54:44 UTC)
loading tasks 393ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 98%
default 5ms ▇▇ 1%
Total 400ms
Create Gruntfile and Package file from samples:
cp Gruntfile.js.sample Gruntfile.js && cp package.json.sample package.json
Install Magento dependencies (defined in package.json)
npm install
Summary of the most important directories:
<installation directory name>/
├── <app>/
│ │ ├── <code>/ // Root for custom modules (project scope)
│ │ │ ├── ...
│ │ ├── <design>/
│ │ │ ├── <adminhtml> // Root for backend themes
│ │ │ ├── <frontend> // Root for frontend themes
│ │ ├── <etc>/
│ │ │ ├── conf.php // Module configuration (auto-generated)
│ │ │ ├── env.php // System configuration (former local.xml)
│ │ │ ├── ...
├── <bin>/
├── <dev>/
│ │ ├── <tools>/
│ │ │ ├── <grunt> // Grunt configuration files
├── <lib>/
│ │ ├── <web>/
│ │ │ ├── ...
│ │ │ ├── <css>
│ │ │ │ ├── <docs> // CSS library documentation
│ │ │ │ ├── <source> // CSS library source code
├── <pub>/
├── <var>/
│ │ ├── <cache>/
│ │ ├── <view_preprocessed>/ // Collected, not yet compiled Less source files
│ │ ├── ... // Magento 2 has a some more caches for you.. ;)
├── <vendor>/ // Composer Modules including Magento core & themes
Location | Usage |
---|---|
etc/view.xml | Configuration of media files and some vars |
i18n | Localisation files (CSV) like in Magento 1 |
media (preview.jpg) | Preview image |
[Vendor_Module] | Module Context Folders |
[Vendor_Module]/layout | Module layout XML files |
[Vendor_Module]/templates | Module PHTML files |
[Vendor_Module]/web | Module static files |
web | static theme files (former skin folder) |
web/images | static images |
web/images/logo.svg | Default logo file |
web/css | Less and/or static CSS sources |
- You can create nesting levels at will using
<parent>Vendor/theme</parent>
in your theme.xml - Leaving
<parent/>
empty will fallback to Core Theme Module. Be cautious, using this you are literally starting with an unstyled page. Make sure to define at least CSS entry points.
Register your theme in app/design/frontend/<Vendor>/<Theme>/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::THEME,
'frontend/<Vendor>/<Theme>',
__DIR__
);
and app/design/frontend/<Vendor>/<Theme>/theme.xml
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
<title>Vendor: Theme Name</title>
<parent>Magento/luma</parent>
</theme>
Assign your Theme to a store in Magento backend under Content > Design > Configuration
, open the frontend in a browser and enjoy yourself a cup of coffee – you should now see the theme you inherited from.
Defines page structure and its order/hierarchy of elements. There
- Page type like
catalog_product_view
=> Controller actions - Page layout like
catalog_product_view_type_simple_id_128
=> Controller actions with parameters - Other like
2columns-left
- Containers for structure
- Blocks for features, implementing PHTML templates
Magento 2 extends layout files (as known from Magento 1 local.xml) by default if you put a corresponding, same named file in your theme:
Example for Page Config or generic layout
<your_theme_dir>/
├- Magento_Catalog/
├- layout/
├- catalog_product_view.xml
would extend
<Magento_Catalog_module_dir>/
├- view/
├- frontend/
├- layout/
├- catalog_product_view.xml
Example for Page Layouts
Add an extending layout file to the following location:
<your_theme_dir>/<Namespace_Module>/page_layout/<layout_file>.xml
Base Layout:
<your_theme_dir>/<Namespace_Module>/layout/override/base/<layout_file>.xml
Theme Layout:
<your_theme_dir>/<Namespace_Module>/layout/override/theme/<Parent_Vendor>/<parent_theme>/<layout_file>.xml
In general, definition of layout XML is quite similar to Magento 1.
<block class="Magento\Theme\Block\Html\Header\Logo" name="custom.footer.logo" as="custom.footer.logo">
<arguments>
<argument name="logo_img_width" xsi:type="number">189</argument>
<argument name="logo_img_height" xsi:type="number">64</argument>
</arguments>
</block>
Note, that the former type attribute is now called class and contains its full path, which is consistent with many other places where Magento 2 uses namespaces etc.
As you can see in this example, custom arguments may be passed to the block to be accessed in a corresponding PHTML file by using $block->get{ArgumentName}()
or $block->getHas{ArgumentName}
. The latter would return a boolean value.
Moving elements was quite complicted in Magento 1. Now you can do this easily with the new node:
<move element="block.name" destination="another.block.name" before="-" after="-"></move>
Removing elements
Although it is IMHO a little bit inconsistent, elements can be removed by setting the attribute remove="true"
:
<referenceBlock name="top.links" remove="true"/>
Both blocks and containers can be referenced, if they already exist. Other than in Magento 1, there is no unique <reference/>
node anymore. Instead you use the following syntax:
<referenceBlock attributes />
<referenceContainer attributes />
The usage of XML Scheme Definitions is new. In PHPStorm you can get auto completion by using the Magento 2 Plugin for PhpStorm
Depending on your environment you might have to tell PhpStorm manually where to find the corresponding Definition. It helps you in doing so:
<page xmlns:xsi="..." xsi:noNamespaceSchemaLocation="urn:.../page_configuration.xsd">
If the URN part is displayed in red, PhpStorm hasn't found it. Place your Cursor on it and hit ALT + ENTER
(Mac) and chose "Manually setup external resource". In the popup window you can look for the corresponding file (type and search does work).
The URN will turn green. Open it and check for sub-sequent Scheme definitions. Then you will have suggestions if you start typing.
Containers are new elements used to group blocks. Usage is quite simple:
<container name="..." as="..." label="..." htmlTag="div" htmlClass="class1 class2">
<block attributes/>
<block attributes/>
</container>
The mindful reader will notice two seemingly unconspicious new attributes htmlTag
and htmlClass
.
Yes, they do exactly, what you'd expect. And No it wouldn't be of good practice to use them a lot!
❌❌❌ No, seriously! Resist the temptation and stick to the strict separation of the originally intended layers:
- PHTML is for rendering Markup
- PHP Block classes are for preparing data for rendering
- Layout XML is to control which block should be displayed where and how
IMHO Magento Core Developers did more potential harm than good in weakening this doctrine by allowing this.
- Can inherit from existing layouts via
<update handle="1column"/>
- Can be assigned to the page tag of any Layout XML
<page layout="3columns" ... />
Example to extend default 1column layout:
<your_theme_dir>/<Namespace_Module>/page_layout/<layout_file>.xml
:
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<update handle="1column"/>
<!-- Do some custom stuff here... -->
</layout>
New page layouts need to be registered in a module context:
<your_theme_module>/view/frontend/layouts.xml
<?xml version="1.0" encoding="UTF-8"?>
<page_layouts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/PageLayout/etc/layouts.xsd">
<layout id="homepage">
<label translate="true">Home Page</label>
</layout>
</page_layouts>
These are the template files, rendering the markup for a certain layout or feature (e.g. Block).
Compared to Magento 1 very few things changed here. You write HTML Markup and fill it with <?php ?>
tags.
- The former
$this
becomes$block
to reference the corresponding block class - The former
$this->__('Translation')
becomes__('Translation')
- There is no more "global" usage of
$this->helper()
possible anymore. They can only be used within blocks context via Dependency Injection. But you could use "headless" blocks for the same purpose
PHP CLasses to implement feature logic, to be rendered via PHTML templates.
in app/code/<Vendor>/<Theme>/registration.php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Vendor_Theme',
__DIR__
);
and app/code/<Vendor>/<Theme>/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_Theme" setup_version="0.3.0">
...
</module>
</config>
For instance in app/code/<Vendor>/<Theme>/Block/MyBlock.php
namespace Vendor\Theme\Block;
use Magento\Framework\View\Element\AbstractBlock;
use Magento\Framework\View\Element\Context;
/**
* Class MyBlock
* @package Vendor\Theme\MyBlock
*/
class MyBlock extends AbstractBlock
{
public function __construct(Context $context, array $data = []) {
parent::__construct($context, $data);
}
/**
* @return string
*/
public function getSomething() {
return 'Something';
}
}
The basic usage is again similar to Magento 1, except the use of namespaces and constructor dependency injection. For further details consult the official developer documentation, as we focus on frontend here.
/* @var \Vendor\Theme\Block\MyBlock $myBlock */
$myBlock = $block->getLayout()->createBlock('Vendor\Theme\Block\MyBlock');
echo $myBlock->getSomething();
In Magento 2 LESS is not used the way you are possibly familiar with:
- It is tied to the core system via server side implementation. Shipped Grunt tasks partially call Magento CLI
- It can be invoked on layout loading, http request of static CSS files or the shipped Grunt tasks
- Implementation respects theme fallback
- Additional @magento_import directive; allows to fetch files with same name from different locations (e.g. modules)
It is a actually a two-step processing:
- Collect resources in
var/view_preprocessed
- Compile collected resources to
pub/static
❗ Always use provided Grunt tasks in favour of performance!
I attached a workflow visualisation SVG to this gist.
First, register your theme to Grunt, by adding an entry to dev/tools/grunt/configs/themes.js
module.exports = {
blank: {...},
luma: {...},
backend: {...},
myTheme: {
area: 'frontend',
name: 'Vendor/theme',
locale: 'de_DE',
files: [
'css/styles-m',
'css/styles-l'
],
dsl: 'less'
}
};
Note that you will have to add an entire object with an unique identifier for every locale you want to address, since static files are compiled in locale context.
Now you can start using the shipped Grunt tasks i the following manner.
Initially and every time you add or remove .less files run the following tasks in that specific order:
grunt clean:myTheme
grunt exec:myTheme
grunt less:myTheme
Thereafter you can start grunt watch
task and your CSS gets compiled every time you change an existing .less file.
Always! use the tasks in the scope of your theme/locale by using :myTheme
. Here is why:
As you already may have noticed from the above declaration in themes.js
, the default themes are also known to grunt. If you just execute the tasks, they will run in every context, which takes a lot longer.
But most importantly this will also affect the Backend, since it is treated as a normal theme, too.
So an unthoughtful grunt clean
will wipe out all backend styles. Although they will be re-compiled in general, you certainly don't want to do that. And I personally experienced a case, when I broke the Backend irrevocably - leaving a complete re-install the only option.
Sometimes things can go wrong, especially when changing preprocessing modes or locales. In case of general “unidentified” problems, work the following checklist to reset the system:
-
Delete the static files relevant for the theme in:
pub/static/frontend/
var/view_preprocessed/less/
var/view_preprocessed/source/
-
Flush the Magento caches (Backend, bin/magento CLI or n98-magerun2)
-
Run
bin/magento setup:static-content:deploy <lang_LANG>
-
Start Grunt task workflow as described above
I would recommend a workflow in the following order
- lookup in
magento/lib/web/css/source/lib/variables
- set new values in your
web/css/source/_variables.less
- set new values in your
web/css/source/_theme.less
- If you don't inherit from blank/luma, you can define theme relevant variables here as well to provide a hook for developers inheriting from your theme
Place a web/css/source/_extend.less
file in your theme. This is recommended by Magento as the "simplest way to extend parent styles".
As shown in this gist it is called at the very end by using a @magento_import directive iterating over all modules. So you could use this in your cutom module, too.
A best practice would be to use `_extend.less' as a bootstrapping file to import your own resources like this:
@import '_extend/_buttons.less';
@import '_extend/_header.less';
// ...
You can place an own equivalent Vendor_Module/web/css/source/_module.less
or Vendor_Module/web/css/source/_widget.less
file in your theme.
Every base lib file can be overriden by putting an equivalently named file in your theme's web/css/source/
directory
You can define own entry points in Magento_Theme/layout/default_head_blocks.xml
and basically do what ever you want ;)
Make sure to properly remove the default CSS files to prevent triggering of server side preprocessing.
❗Always use one of the following patterns to prevent multiple rendering of your styles. This is due to the fact that everything is executed both in styles-m.less
and styles-l.less
. The variables below control if a directive will be rendered on each run.
Common Styles:
& when (@media-common = true) {}
Breakpoint related styles:
.media-width(@extremum, @break) when (@extremum = 'max|max') and (@break = @screen__m) {}
This seems a little bit odd and my first, almost natural, reflex was to use it in a much more readable (and seemingly equivalent, yet less redundant) form:
.media-width('min|max', @screen__m) {}
But this doesn't work out well! This is because the .media-width()
mixin is called in a recursive way to collect all Queries. As a result there are no duplicates and the size of compiled files is somewhat stable.
Documentation besides sources can be found in magento/lib/web/css/
.
At the time writing I still find it an issue that there is no proper framework documentation available on the official Developer Website. The only posiibility to get in-depth information is to take a look into the codebase addressed above. The provided HTML documentation is nothing to be compared with similar resources from twitter bootstrap or other frameworks. I would like to see it integrated with devDocs, especially since the new layout seems to be much more feasible.
In principle it is pretty simple. Once you found a desired component, it will be most likely presented to you in the form of a mixin with several paramters. So just call it on your selector of choice and you're done.
Biggest issue for me is the framework layout itself. You will find it not very well documented. Naming is often not optimal or self-explanatory. Use of concepts and language features are high-level but overly complicated or abstract.
Let me show you an actual use case to guide you through and understand my point above:
Let's say we would like to have some fancy CSS arrows to decorate some hyper links. If we take a look at magento/lib/web/css/source/lib/_utilities.less
we find the .lib-arrow()
mixin at line 353:
.lib-arrow( @_position, @_size, @_color ) {
border: @_size solid transparent;
height: 0;
width: 0;
._lib-abbor_el(@_position, @_color);
}
Okay, looks promising. It accepts parameters for its orientation, size and color, as we would kind of expect for this purpose. But wait! There is another mixin involved! One with an un-pronouncable name. Does this even have a meaning!?
So, if we want to find out the type of our parameters, we would have to rely on the documentation or we follow the mixin definition. Happily it's defined right below .lib-arrow()
:
._lib-abbor_el( @_position, @_color ) when (@_position = left) {
.lib-css(border-right-color, @_color);
}
._lib-abbor_el( @_position, @_color ) when (@_position = right) {
.lib-css(border-left-color, @_color);
}
._lib-abbor_el( @_position, @_color ) when (@_position = up) {
.lib-css(border-bottom-color, @_color);
}
._lib-abbor_el( @_position, @_color ) when (@_position = down) {
.lib-css(border-top-color, @_color);
}
From what we can tell now, it seems to simply set a border color. We can vaguely guess now, that abbor_el
could mean something like "Add border to element". Anyway...
We also see a concept the core developers decided to use in many places. AFAIK Less doesn't come with the possibility to have some kind of value list like an array. They work around by defining the mixin several times with a different guarding expression. So the first one would only be rendered if @_position
would match the expression left
.
IMHO this approach generates a lot of redundant code. I can see no clear advantage in favor of using a "flat" mixin without real validation. But at least, we know our possible orientation values.
But how does it actually set the color? There is another mixin involved. And this is truly my "favorite" one:
.lib-css( @_property, @_value, @_prefix: 0 ) when (@_prefix = 1)
and not (@_value = '')
and not (@_value = false)
and not (extract(@_value, 1) = false)
and not (extract(@_value, 2) = false)
and not (extract(@_value, 3) = false)
and not (extract(@_value, 4) = false)
and not (extract(@_value, 5) = false) {
-webkit-@{_property}: @_value;
-moz-@{_property}: @_value;
-ms-@{_property}: @_value;
}
.lib-css( @_property, @_value, @_prefix: 0 ) when not (@_value = '')
and not (@_value = false)
and not (extract(@_value, 1) = false)
and not (extract(@_value, 2) = false)
and not (extract(@_value, 3) = false)
and not (extract(@_value, 4) = false)
and not (extract(@_value, 5) = false) {
@{_property}: @_value;
}
Behold, the .lib-css()
mixin! What it does in a nutshell is mainly passing through arbitrary CSS attribute:property pairs. So instead of writing border-color: black
you are now involving a mixin .lib-css(border-color,black)
. Awesome, right?
Okay, it does some sanity checks. Still heavily complicated for this task.
And yes, it has another flavour. If the third parameter is set, it works as a prefixer.
This is my very personal point of view, but this is a great example of why CSS in Magento 2 is such a struggle and why so many are complaining. It provides heavily complex and hardly documented solutions to no-problems. Here we have a particular task which could be done perfectly by an autprefixer plugin. No need to re-invent the wheel. It blows up code and binds capacities to create and maintain codebase.
I see absolutely no disadvantage in flatten out those three mixin layers into one flat .lib-arrow()
mixin, which could directly be understood and used.
That took me a little bit away, but I hope by using this example, you can get a better/faster hang on how CSS works in Magento 2. It is up to you to decide how to leverage the framework. In many cases (by now) I see no real advantage in using the components besides from the initial styling of elements in blank/luma theme. But you should totally dive in a bit and play around!
A nice little feature in Magento 2 are built-in jQuery widgets. These are basically implementations of elements of the jQuery UI library. You can use widgets for the most common tasks like collapsibles, modals or tabs right out of the box. Consult the JavaScript Developer Guide for detailed information.
Magento 2 provides different ways of using the built in widgets as well as own libraries and modules. I will show the most simple approach using data-attributes.
Let's add a collapsible to some Magento form. We basically need two things:
- a wrapping element holding plugin initialization and configuration
- alternating elements for title and content inside that container
So this would be our (simplyfied) markup:
<fieldset class="fieldset director">
<div class="fieldset" data-mage-init='{
"collapsible":{
"active": false,
"animate": 200,
"collapsible": true,
"openedState": "opened"
}}'>
<div class="fieldset" data-role="title">...</div>
<div class="fieldset" data-role="content">...</div>
<div class="fieldset" data-role="title">...</div>
<div class="fieldset" data-role="content">...</div>
</div>
</fieldset>
The outer fieldset holds widget configuration by passing an array to data-mage-init
attribute. Then we have sub-sequent fieldsets with alternating data-role
attributes. Each title
will be clickable and open the following content
. Simple as hell.
- Be welcome to use this script as a starting point for your work
- I neither recommend nor discourage you to use the Magento 2 frontend standards as
- there is much movement in the community by now
- I haven't found my personal "best approach" yet
- Magentos roadmap is unknown to me
- Better try to understand the Magento way first, before you start implementing your own
- Be creative, contribute yourself, share your mind and help improve!