Set up Custom Dimensions using GA, GTM, and the Data Layer

This article was written for Universal Analytics, which will stop processing data in July 2023 as Google switches to GA4. As GA4 requires almost completely new configuration, check out Google's migration guide here, or, for a complete done-for-you solution, I recommend Mediauthentic.

Adding a custom dimension to Google Analytics involves several interconnected parts: the data layer, tag manager setup, and admin settings in GA. This post walks through each step of the process to show how they fit together.


Step 1: Push Values into the Data Layer
Step 2: Set up Custom Dimensions in Google Analytics Admin
Step 3: Set up Google Tag Manager Variables
Step 4: Add Dimensions to the GA Tags in GTM
Step 5: Add a Trigger to your Tag


The first step is to set up the data layer. The data layer is a key part of working with Google Tag Manager. It holds information you want to process in an object that can be read by Google Tag Manager, where it then gets pushed to Google Analytics.

The data layer needs to be configured by someone with access to your site code, typically a web developer. They should add the data layer to your site's code before the GTM script, using the following syntax:

window.dataLayer = window.dataLayer || [];
  'event' : 'eventName',
  'dim1' : 'value1',
  'dim2' : 'value2'

Simo Ahava's always-helpful blog discusses the specifics of this syntax: Also, this post provides some examples of different data layer pushes: GTM dataLayer.push Examples (Standard, Ecommerce, Advanced)

So, if you were interested in tracking page types and login status, it would look something like this:

window.dataLayer = window.dataLayer || [];
  'event' : 'pageview',
  'loginStatus' : 'logged in',
  'pageType' : 'product details page'
Note: while event keys aren't strictly necessary for a data layer push, every tag in GTM needs an event key to trigger it. If you push dimensions without an event key, you can't access the dimension values at that current state -- they will be inaccessible to GTM until the next event key gets set. So as a best practice, always include an event key on every data layer push.


For the next step, enter GA and go to Admin > Property > Custom Definitions > Custom Dimensions > +New Custom Dimension and fill out the name and scope for your variable.  Click Create.


On the following screen, click Done and note the Index number on the summary screen. This index number is assigned automatically and we'll need it later.


Next, set up Data Layer Variables in Google Tag Manager (GTM). These Variables will collect the values pushed into the data layer in step 1.

To set up these Variables, navigate in GTM to Variables > User-Defined Variables > New > Variable Configuration > Choose variable type = Data Layer Variable. Enter in the name of the data layer variable to read from, making sure the name you use here exactly matches the name used in the data layer.



The fourth step is sending the dimension value to GA. To send these values, dimensions need to be attached to other hits like pageviews, events, or ecommerce.

In GTM, find the tag you want to attach the dimensions to. Usually this will be the GA page view tag, but other hit types will work exactly the same way. Click the "Enable overriding settings in this tag " checkbox > More Settings > Custom Dimensions. (Or, alternately, you can set up these custom dimensions in the Google Analytics Settings Variable).

In the Index slot, use the same index number as assigned in GA Admin in step 2. Under Dimension Value, add add the new Variable you created in step 3.


As described above, all custom dimensions need to be attached to a tag.

Every tag needs a trigger to fire, so in this step make sure you've chosen the right trigger. Specifically, you need to choose one that doesn't fire too soon. For example, if you're tracking page-related dimensions like page categories, those values need to be ready and available when the page view tag fires. The data layer push often occurs after the initial page load, so if you choose All Pages as your trigger, the values would still be null at the time the tag fired, resulting in page category values of "(not set)" in Google Analytics.

The best way to avoid this problem is to create a Custom Event Trigger for your data layer event key (in the example in step 1, it would be 'pageview'), like this:


Then, use that trigger to fire your Pageview tag, like this:


This way will ensure that the Pageview tag fires only after your dimensions are pushed into data layer.

(If you don't have an event key in your data layer push, you can alternately choose a delayed page view trigger like DOM Ready. However it's better to use a Custom Event Trigger since it fires your tag more quickly).


Following these 5 steps will enable you to populate custom dimensions in Google Analytics via the data layer and Google Tag Manager. Once these dimensions are collecting data, you can view these dimensions as a Secondary Dimension in any regular report, or create a custom report using your new dimension as a primary dimension.

31 thoughts on “Set up Custom Dimensions using GA, GTM, and the Data Layer”

  1. I followed your guide to set-up a pages topic - with the topic itself being auto-generated in the datalayer based upon the category selected when creating the WordPress page/post.

    However when I view the secondary dimension within Google Analytics the homepage (and other pages) seems to be picking up sessions against multiple topics which is incorrect as core can only be selected for it (and is confirmed in the HTML).

    Any idea what's going on?



    • Hi Sam, is your dimension in GA set to hit-scoped? Session and user-scoped dimensions will take on the final value in a session so could lead to some unexpected results. If that's not the issue and you'd like me to take a closer look, please email me your URL, a screenshot of your tag setup, and your custom dimension page in GA.

    • You don't need a data layer at all for custom dimensions, it's an optional but (usually) recommended step. If you don't have or don't want to use a data layer, you can instead capture values by scraping them off the page. For example, say you want to pass in a custom dimension for login status. You might find that you can identify whether a user is logged in or out by checking the navigation bar, as they have different menu options (For example, the 'logged out' menu has an option to sign in, while the 'logged in' menu has an option to check your account). In that case you would alter the above steps as follows:
      1) skip step 1 entirely
      2) complete step 2 as written
      3) instead of setting up a Data Layer variable, set up a Custom Javascript variable, and write a function that returns 'logged in' or 'logged out' based on which version of the menu bar is visible (or whatever other way you've found to identify login status).
      4) complete step 4 as written
      5) since you no longer have a data layer event, you'll need to instead use the built-in triggers like DOM Ready or Window Loaded to make sure your new JS Variable is resolved and available at the time you send it over to GA.

  2. Hi Ana! Thanks a lot for the detailed step-by-step explanation. I'm stuck at step 5 as I'm not sure what my best option is. I already have a GA page view tag that fires with the trigger type page view (but also the trigger type history change because of the way the website is coded). I'm worried that adding the custom event trigger might:
    - not count all page views since we're waiting for the datalayer push to be completed
    - not mix well with the history change trigger
    To mitigate this issue, I was thinking of creating a separate GA page view tag just for the custom dimensions but then I guess GA will be counting the page views twice?
    Thanks a ton for your feedback!

    • Hi Solal, the reason I suggest doing it with an event instead of pageview trigger is exactly to avoid the kind of issues you're mentioning. It won't affect your pageviews or history change trigger since it's a totally different tag & trigger, whereas if you create a new page view tag, you'll be double-counting page views unless you set up blocking triggers to avoid that. I usually just recommend sending this data in a pageview for sites that need to reduce their hit counts (due to GA's 10 million hits / month limit). If you're not in that category, I'd stick to the event method described in this post.
      Hope that helps clarify!

  3. Hi Ana, thank you for this guide! If my page is slow to load and the user exits before it is loaded correctly (due to heavy adv or other causes), with the trigger that you suggest the risk that the pageview is not counted in Google Analytics?

    • There's a small risk with any tag that the user will navigate away before the page loads (or a larger risk that the user will block the tag altogether with a script blocker). This setup isn't inherently slow, though, and in fact it could be faster if you push the 'pageview' event before GTM's built in 'Page View' trigger fires. The difference is unlikely to be noticeable either way, though.

  4. Ana, excellent guide.


    1. In GA, I added the a new dimension for "Content Zone"
    2. I've created a new track type: event with a distinct label "interaction click".
    3. I've mapped custom dimension index 1 to the value "Content Zone" in the event.

    Do I have to add the click event in GA to elements or can I now use dataLayer.push to track interactions? Like so:
    dataLayer.push(event: "interaction_click", "content_zone": 1 }

    Is the data formatted properly? All lowercase with underscores for spaces?

    • Do I have to add the click event in GA to elements or can I now use dataLayer.push
      I'm not sure I fully understand your question, but your datalayer push won't do anything by itself. You still need a) a custom event trigger set to read the "interaction_click" event, and b) an event tag to send your data to GA.

      Is the data formatted properly?
      It looks fine (and it doesn't need to be lowercase, btw) except it seems to have the wrong brackets. It should look like this:
      dataLayer.push({ 'event' : 'interaction_click', 'content_zone' : 1})

  5. Hi Ana!

    I have a question for you! I am hoping to use event level data (most likely login button click) to be able to segment my client vs. prospect data. What do you think the best solution would be?

    Any help would be appreciated

    • Hey Sarah, that seems fine. It won't be totally accurate since presumably people without an account could still click on the login button, but it should be close enough if you just want an idea. You can add a custom dimension to the click event tag where the Dimension Value is hard coded to "Client", and then use that dimension to identify your clients. Of course, if you have a more accurate way to identify clients vs prospects, e.g. via a cookie, then you can use that value instead. The specifics really depend on your site.

  6. Hi Ana,
    Thanks a lot for all the details. I have 1 day of experience with GTM / GA and this was super clear and saved a lot of time.

    Now, I have a question for you:
    I'm working with an old codebase and we are moving from GTM to GA. In our code we are using `ga('set', 'dim1', value)`. Is there a way for GTM to pick this custom dimensions from there or is it mandatory to use the dataLayer for that?

    The information set in the custom dimensions cannot be scrapped ATM.

    Thanks in advance!

    • Thanks for the comment, I'm glad you like the article.
      For your question, GTM will not pick up values like ga('set', 'dim1', value) since that code is specific for GA. GTM will only recognize a GTM data layer.

  7. Hi Ana,

    Very clear article! However, one thing I don't really understand:
    In step 5 you add a trigger to the GA Page view tag. Normally I would use the All pages trigger, but you advise not to use this. So I tried your option with the custom event trigger. But now the GA tag only gets triggered when there is actually a custom event on the page. But this is not always the case. If a dataLayer is only filled with that custom event on certain pages (certain content in my case), then on other pages the GA tag is not triggered anymore and GA does not track anything.

    What am I missing here?

    Thanks for your reply!

    • Hey Jason! It would need to be a custom event that fires on every page. That’s why my GTM event name is “pageview”, it’s an event that fires on every page view. You would set this event key at the same time you’re pushing your custom dimensions into the data layer.

      In your case, where you’re pushing a data layer only on certain pages, this approach won’t work. Can you push your data layer key on every page, and just leave the values as null for those content types you don’t want to populate? If not, you’ll have to use the native All Pages trigger, and double check that your data layer values are ready when that All Pages trigger fires.

  8. fantastic explanation! How can I scrape text from my pages to show in GA? For example, title tags, Header 1 and 2, title length by adding secondary dimensions? I know it's possible but I am not sure how exactly.

    • Hey Luca, what you're talking about is called DOM scraping. GTM has some built-in listeners for this, for example Click Text, if you want to track what someone clicked on. If you want to track elements of the page like titles and headers, you usually need to create custom JS variables that pull in the text you're looking for. You need to have some basic JS knowledge to do this, or if you know about CSS selectors you can modify this simple example script and include it in a custom JS var:
      function (){
      var entryTitle = document.querySelector(".entry-title").innerText;
      return entryTitle;

      You can see some more example scripts here: Once you have your custom JS variable, you attach it to the custom dimension field in a page view or event tag, just like you would any other dimension.

      Hope that helps point you in the right direction.

  9. Hi,

    After adding a custom dimension for login status we're seeing the proper results in Analytics. The only issue we have is that with maybe 15% of our visitors the custom dimension is simply not set. Which is strange, because we set the dimension with every pageview. The only hint we have is that all of these users also trigger other kinds of events, for example when they hover over our menu, or submit a form. What could be causing this?


    • It sounds like you may have a race condition, where the variable isn't reliably populated in time. This can happen if your variable is being done through a custom JS variable instead of being pushed in via the data layer, or if it's not getting populated until the second page view.

      Another possibility is there's an error in the code, that is less likely though.

      Can you do some investigating to find common threads among the 15% that isn't set? For example, are they on mobile, are they single-page visits, any specific browser? And can you make sure your logged in status is being pushed in through the data layer rather than through scraping the status into a custom JS var?

  10. Hello Ana, thank you for your amazing guide!

    I already have a tracking tag on the site (not implemented with GTM). So I set the tracking type from Page View to Event (to avoid track twice) and no interaction hit to "true". Did I do it right? I'm setting up custom dimensions to create remarketing audiences

Comments are closed.