I am trying to create darkmode with sass work flow, i found an explanation online but its in React and i currently dont know react, I understand the code to an extent but the whole changing of state seems confusing, how can i convert to vanilla Javascript, using this in a vanilla JS situation os my issue now
HTML
<main id="app-root">
<div class="theme-light">
<div class="app-container">
<h1 class="title">Light theme</h1>
<button class="button">A button</button>
</div>
</div>
<div class="theme-dark">
<div class="app-container">
<h1 class="title">Dark theme</h1>
<button class="button">A button</button>
</div>
</div>
</main>
CSS
/*
* Theme definitions
*/
$themes: (
light: (
backgroundColor: white,
textColor: #408bbd,
buttonTextColor: #408bbd,
buttonTextTransform: none,
buttonTextHoverColor: #61b0e7,
buttonColor: #fff,
buttonBorder: 2px solid #408bbd,
),
dark: (
backgroundColor: #222,
textColor: #ddd,
buttonTextColor: #aaa,
buttonTextTransform: uppercase,
buttonTextHoverColor: #ddd,
buttonColor: #333,
buttonBorder: 1px solid #333,
),
);
/*
* Implementation of themes
*/
#mixin themify($themes) {
#each $theme, $map in $themes {
.theme-#{$theme} & {
$theme-map: () !global;
#each $key, $submap in $map {
$value: map-get(map-get($themes, $theme), '#{$key}');
$theme-map: map-merge($theme-map, ($key: $value)) !global;
}
#content;
$theme-map: null !global;
}
}
}
#function themed($key) {
#return map-get($theme-map, $key);
}
/*
* Actual styles for the app
*/
body {
margin: 0;
}
html, body {
height: 100%;
}
#app-root {
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
> div {
display: flex;
flex: 1;
}
}
.app-container {
display: flex;
flex-direction: column;
flex: 1;
align-items: center;
justify-content: center;
.title {
font-family: sans-serif;
font-weight: lighter;
}
#include themify($themes) {
color: themed('textColor');
background-color: themed('backgroundColor');
}
.button {
max-width: 20em;
cursor: pointer;
border-radius: 5px;
padding: 15px 32px;
display: inline-block;
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
#include themify($themes) {
border: themed('buttonBorder');
color: themed('buttonTextColor');
border-color: themed('buttonTextColor');
background-color: themed('buttonColor');
text-transform: themed('buttonTextTransform');
&:hover {
color: themed('buttonTextHoverColor');
border-color: themed('buttonTextHoverColor');
background-color: themed('buttonHoverColor');
}
}
}
}
How To Toggle Dark Mode
W3schools.com covers a nice example how to achieve this with vanilla JS
var element = document.body;
element.classList.toggle("dark-mode");
}
Just change classnames and you're good to go!
Related
I can't figure out why I'm getting this little bit of green when the window is an odd number of pixels wide. I think it has something to do with sub-pixel rendering, but I'm just not sure where the green is coming from. It's just the 2nd div too which is weird.
I have some script that is animating the BG of this div. I'm sure this is part of the issue, but I can't figure out why it's only happening to my 2nd div.
I tried to manually set the width of this div, but I was hoping it would be responsive and scale with the window size.
let currentStage = 1
function performAction(selectedStage) {
currentStage = selectedStage
let stages = document.body.getElementsByClassName('stage-flow-item')
let stageLines = document.body.getElementsByClassName('stage-flow-line')
console.log("selectedStage: " + selectedStage)
for (let stage of stages) {
if (stage.id > currentStage) {
stage.classList.remove('completed')
stage.classList.add('active')
} else {
stage.classList.remove('active')
stage.classList.add('completed')
}
}
for (let stageLine of stageLines) {
if (stageLine.id > currentStage) {
stageLine.classList.remove('lineCompleted')
stageLine.classList.add('lineActive')
} else {
stageLine.classList.remove('lineActive')
stageLine.classList.add('lineCompleted')
}
}
}
.stage-flow-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 70px;
padding: 0 30px;
}
.stage-flow-item {
width: 70px;
height: 70px;
min-width: 70px;
border-radius: 50%;
background-color: #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
color: #fff;
cursor: pointer;
}
.stage-flow-item.active {
background-color: #ddd;
}
.stage-flow-item.completed {
background-color: #6ab04c;
}
.stage-flow-line {
width: calc(100vw);
height: 6px;
background-color: #ddd;
/* default color */
background: linear-gradient(to left, #ddd 50%, #6ab04c 50%) right;
position: relative;
background-size: 200%;
transition: .5s ease-out;
}
.stage-flow-line.lineCompleted {
background-position: left;
background-color: #6ab04c;
}
.stage-flow-line.lineActive {
background-position: right;
background-color: #ddd;
}
<div class="stage-flow-container">
<div id=1 class="stage-flow-item" onclick="performAction(1)">1</div>
<div id=1 class="stage-flow-line"></div>
<div id=2 class="stage-flow-item" onclick="performAction(2)">2</div>
<div id=2 class="stage-flow-line"></div>
<div id=3 class="stage-flow-item" onclick="performAction(3)">3</div>
</div>
I'm not sure if this is on the right track, but I'd eliminate the odd 100vw width on the connectors and instead make them flex. I'd then remove the 200% background size multiplier. By setting the gradient points to 100% the problem is gone. I really don't know if this covers your use case, though.
I converted from background gradient to a pseudo-element solution for the color transition. I think it's simpler. You'd probably have to use CSS animations (as opposed to simple transitions) to make it work otherwise. Of course, you could apply the same principle to the stage items as well, implementing a delay to crate a consistent animation across the item and the line.
Note that duplicated ID values are invalid in HTML. They must be unique. I've refactored to use data attributes instead and an event listener instead of inline JavaScript.
const stageEls = document.querySelectorAll('.stage-flow-item')
const lineEls = document.querySelectorAll('.stage-flow-line')
let currentStage = 1
stageEls.forEach(el => {
el.addEventListener('click', () => {
performAction(el.dataset.stage)
})
})
function performAction(selectedStage) {
currentStage = selectedStage
for (let el of stageEls) {
if (el.dataset.stage > currentStage) {
el.classList.remove('completed')
el.classList.add('active')
} else {
el.classList.remove('active')
el.classList.add('completed')
}
}
for (let el of lineEls) {
if (el.dataset.stage > currentStage) {
el.classList.remove('lineCompleted')
el.classList.add('lineActive')
} else {
el.classList.remove('lineActive')
el.classList.add('lineCompleted')
}
}
}
.stage-flow-container {
display: flex;
align-items: center;
height: 70px;
padding: 0 30px;
}
.stage-flow-item {
width: 70px;
height: 70px;
min-width: 70px;
border-radius: 50%;
background-color: #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
color: #fff;
cursor: pointer;
}
.stage-flow-item.active {
background-color: #ddd;
}
.stage-flow-item.completed {
background-color: #6ab04c;
}
.stage-flow-line {
flex: 1;
height: 6px;
background: #ddd;
position: relative;
}
.stage-flow-line::after {
position: absolute;
content: '';
top: 0;
left: 0;
width: 0;
height: 100%;
background: #6ab04c;
transition: all 0.5s ease-out;
}
.stage-flow-line.lineCompleted::after {
width: 100%;
}
<div class="stage-flow-container">
<div data-stage=1 class="stage-flow-item">1</div>
<div data-stage=1 class="stage-flow-line"></div>
<div data-stage=2 class="stage-flow-item">2</div>
<div data-stage=2 class="stage-flow-line"></div>
<div data-stage=3 class="stage-flow-item">3</div>
</div>
What did I do?
Setup a NextJs project to ship different image sizes for different screens sizes with the help of next/image component with the following settings:
<Image
src={image}
alt={title}
objectFit='cover'
layout='responsive'
placeholder='blur'
width={320}
height={240}
sizes={width !== undefined ? `${Math.round(width)}px` : '100vw'}
/>
What did I expect?
That shipped images have the specified sizes for both desktop as well as mobile views in Chromium DevTools. For Desktop views, this seems to work as expected. As can be seen in the intrinsic (512px) and rendered (492px) width resolution of the image.
Desktop-View-Screenshot
What did happen?
For some reason I can not get my finger on, this does not happen for mobile views in Chromium. Here the intrinsic width resolution is 1024px although the rendered resolution is 492px as well.
Mobile-View-Screenshot
I honestly don't fully understand how next.config.js device and screen sizes relates to the sizes and layout settings on next/image components.
Hoping someone in this forum can enlighten me.
General Application Setting
Application Stack
With docker-compose on a dedicated Debian machine for staging with NextJs, Mongodb and some other micro services such as Redis and a Python worker with Fast-API.
next.config.js
const withBundleAnalyzer = require('#next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
const withCss = require('#zeit/next-css')
const withPurgeCss = require('next-purgecss')
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
const nextConfig = {
webpack(config) {
const fileLoaderRule = config.module.rules.find(
(rule) => rule.test && rule.test.test('.svg')
)
fileLoaderRule.exclude = /\.svg$/
config.module.rules.push({
test: /\.svg$/,
loader: require.resolve('#svgr/webpack'),
})
return config
},
}
nextConfig.i18n = {
locales: ['de-DE', 'en-US', 'ja-JP'],
defaultLocale: 'de-DE',
}
nextConfig.images = {
deviceSizes: [
256, 320, 492, 512, 640, 768, 896, 1024, 1152, 1280, 1408, 1536, 1664, 1792,
1920, 2048, 2176, 2304, 2432, 2560, 2688, 2944,
],
imageSizes: [32, 64, 96, 112, 128, 144, 160, 176, 192, 240],
formats: ['image/avif', 'image/webp'],
domains: [
'upload.wikimedia.org',
'wikimedia.org',
'i.ytimg.com',
],
}
module.exports = (phase) => {
if (process.env.PURGE_CSS === 'true') {
return withCss(withPurgeCss(nextConfig))
}
if (phase === PHASE_DEVELOPMENT_SERVER) {
return nextConfig
}
return nextConfig
}
module.exports = (phase) => {
if (process.env.ANALYZE === 'true') {
return withBundleAnalyzer(nextConfig)
}
if (phase === PHASE_DEVELOPMENT_SERVER) {
return nextConfig
}
return nextConfig
}
next/image element
On an Image in a Card element I set sizes to generate scrset that ships more or less appropriate sizes of the image for different screen sizes.
ContentCard.jsx
import Image from 'next/image'
import Link from 'next/link'
import styles from './ContentCard.module.css'
import dynamic from 'next/dynamic'
import useDimensions from 'react-cool-dimensions';
const ArrowRightIcon = dynamic(() => import('#/assets/icons/arrow-right-icon'), {ssr: false})
function ContentCard({ title, claim, image, action, href }) {
const { observe, width } = useDimensions();
return (
<div className={styles.item}>
<Link passHref href={href}>
<a>
<div>
<div ref={observe} className={styles.image}>
<Image
src={image}
alt={title}
objectFit='cover'
layout='responsive'
placeholder='blur'
width={320}
height={240}
sizes={width !== undefined ? `${Math.round(width)}px` : '100vw'}
/>
</div>
<div className={styles.content}>
<div className={styles.title}>
<h3>{title}</h3>
</div>
<p>{claim}</p>
<div className={styles.actions}>
<span className={styles.icon}>
{action}
<ArrowRightIcon />
</span>
</div>
</div>
</div>
</a>
</Link>
</div>
)
}
export default ContentCard
ContentCar.module.css
.item {
display: inline-block;
width: 100%;
height: 100%;
/* height: 400px; */
overflow: hidden;
transition: 0.5s all;
position: relative;
text-align: left;
}
.item a:link {
color: white;
text-decoration: none;
}
/* visited link */
.item a:visited {
color: white;
}
/* mouse over link */
.item:hover p {
display: flex;
}
.item:hover .content {
transition: 0.5s all;
font-size: larger;
height: 75%;
}
.item:hover .title {
transition: 0.5s all;
font-size: larger;
height: 50%;
}
.item:hover .actions {
transition: 0.5s all;
display: flex;
height: 25%;
}
/* selected link */
.item a:active {
color: white;
}
.content {
position: absolute;
bottom: 0;
background-color: var(--primary-dark);
height: 33.33%;
width: 100%;
padding-left: var(--size-1);
display: flex;
flex-direction: column;
}
.content p {
display: none;
margin-top: 0;
padding: 0 var(--size-1);
}
.title {
text-transform: uppercase;
font-weight: bold;
line-height: var(--size-1);
margin: 0;
position: relative;
background-color: var(--primary-dark);
width: 100%;
padding-left: var(--size-1);
display: flex;
flex-direction: column;
}
.content h3 {
font-size: var(--size-8);
line-height: var(--size-8);
margin-bottom: 0;
padding: 0;
text-transform: uppercase;
font-weight: bold;
/* margin: var(--size-1) 0; */
}
.item p {
font-weight: normal;
font-size: var(--size-7);
line-height: var(--size-7);
margin: 0;
}
.actions {
display: none;
font-size: var(--size-7);
line-height: var(--size-7);
font-weight: bold;
}
.icon {
margin-left: var(--size-1);
display: inline-flex;
justify-content: center;
align-items: center;
}
.icon svg {
width: var(--size-4);
height: var(--size-4);
}
Codepen: https://codepen.io/jitifor864/pen/GRvvpeK?editors=1100
I'm trying to figure out some CSS errors I'm having. At the moment, the problems I'm having are:
The text that is being typed out isn't centered on top of the search bar
If the word gets too long, the bar beneath it begins to expand. How can I keep that bar constant size and not expand if the text gets long
I can't seem to make the blinker a tiny bit wider and stop at the end of the word (it looks like it goes one extra blank character)
Could I get some help on these? I'm sure these are 'small' fixes, just can't figure it out. Thanks!
// values to keep track of the number of letters typed, which quote to use. etc. Don't change these values.
var i = 0,
a = 0,
isBackspacing = false;
// Typerwrite text content. Use a pipe to indicate the start of the second line "|".
var textArray = [
"AskReddit", "AskMen", "Gaming", "FemaleFashionAdvice", "Nosleep", "LetsNotMeet", "Technology", "Funny", "Memes", "Politics", "News"
];
// Speed (in milliseconds) of typing.
var speedForward = 100, //Typing Speed
speedWait = 1000, // Wait between typing and backspacing
speedBackspace = 25; //Backspace Speed
//Run the loop
typeWriter("typewriter", textArray);
function typeWriter(id, ar) {
var element = $("#" + id),
aString = ar[a],
eHeader = element.children("h1"); //Header element
// Determine if animation should be typing or backspacing
if (!isBackspacing) {
// If full string hasn't yet been typed out, continue typing
if (i < aString.length) {
eHeader.text(eHeader.text() + aString.charAt(i));
i++;
setTimeout(function(){ typeWriter(id, ar); }, speedForward);
}
// If full string has been typed, switch to backspace mode.
else if (i == aString.length) {
isBackspacing = true;
setTimeout(function(){ typeWriter(id, ar); }, speedWait);
}
// If backspacing is enabled
} else {
// If either the header, continue backspacing
if (eHeader.text().length > 0) {
// If paragraph still has text, continue erasing, otherwise switch to the header.
if (eHeader.text().length > 0) {
eHeader.addClass("cursor");
eHeader.text(eHeader.text().substring(0, eHeader.text().length - 1));
}
setTimeout(function(){ typeWriter(id, ar); }, speedBackspace);
// If the head has no text, switch to next quote in array and start typing.
} else {
isBackspacing = false;
i = 0;
a = (a + 1) % ar.length; //Moves to next position in array, always looping back to 0
setTimeout(function(){ typeWriter(id, ar); }, 50);
}
}
}
.parent {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
.search-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.typewriter-wrapper {
width: 100%;
text-align: center;
}
.typewriter {
display: inline-block;
overflow: hidden;
border-right: .15em solid orange;
white-space: nowrap;
letter-spacing: .15em;
animation:
typing 2s steps(40, end),
blink-caret .50s step-end infinite;
}
.search-form {
display: flex;
justify-content: center;
width: 100%
}
.search-input {
-webkit-appearance: none;
background-clip: padding-box;
background-color: white;
vertical-align: middle;
border-radius: 0.25rem;
border: 1px solid #e0e0e5;
font-size: 1rem;
line-height: 2;
padding: 0.375rem 1.25rem;
-webkit-transition: border-color 0.2s;
-moz-transition: border-color 0.2s;
transition: border-color 0.2s;
margin-bottom: 0;
flex-grow: 1;
flex-shrink: 0;
flex-basis: auto;
align-self: center;
height: 51px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.search-button {
height: 51px;
margin: 0;
padding: 1rem 1.3rem;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
font-size: 1rem;
display: inline-block;
font-weight: 600;
font-size: 0.8rem;
line-height: 1.15;
letter-spacing: 0.1rem;
background: #F95F5F;
color: #292826;
border: 1px solid transparent;
vertical-align: middle;
text-shadow: none;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
transition: all 0.2s;
}
.cursor::after {
content:'';
display:inline-block;
margin-left:3px;
background-color:white;
animation-name:blink;
animation-duration:0.5s;
animation-iteration-count: infinite;
}
h1.cursor::after {
height:24px;
width:13px;
}
#keyframes blink-caret {
from, to {
border-color: transparent
}
50% {
border-color: orange;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="parent">
<div class="search-container">
<div class="typewriter-container">
<div class="typewriter" id="typewriter">
<h1 class="cursor"> </h1>
</div>
<form class="search-form" method="GET" action="{% url 'ssearch' %}">
<input class="search-input" type="search" name="subreddit">
<button class="search-button" type="submit"> Search </button>
<!-- <i class="fa fa-search"></i> -->
</form>
</div>
</div>
1 - to center the text you should display .typewriter-container as flex with direction column and align-items as center.
.typewriter-container {
/* this are new CSS selector rules you'll need to add */
display: flex;
flex-direction: column;
align-items: center;
}
2 - the browser has a default width for input fields which is setting the initial width of your form. Give the form a fixed width, eg. 330px.
.search-form {
/* Modify this existing rules to set the form width */
display: flex;
justify-content: center;
width: 330px;
}
3 - the blinking border is being spaced by the ::after pseudo element in your h1. Remove the margin of the ::after and set its width to 0 (or whatever spacing you want).
h1.cursor::after {
/* Modify this existing rules to set the margin and width */
height: 24px;
width: 0;
margin: 0;
}
A forked codepen with the changes can be found here https://codepen.io/jla91ab37103f/pen/GRvvqWe
EDIT: Changed the post name, which was incorrectly titled from another post !!
I have been building a sports app in React over the last several months, and I am struggling with a small cosmetic issue with my radio buttons. Immensely frustrating is the fact that despite my attempt at a reproducible example, the bug does not appear in my example below, although fortunately a variant of the issue is occurring. Here are my buttons:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
oneTwoFour: "1 Graph",
quarter: "All"
}
}
handleQuarterChange = (event) => {
this.setState({ quarter: event.target.value });
};
handleOneTwoFourChange = (event) => {
this.setState({ oneTwoFour: event.target.value });
};
render() {
const { oneTwoFour, quarter } = this.state;
const oneTwoFourOptions = ["1 Graph", "2 Graphs", "4 Graphs"];
const oneTwoFourButtons =
<form>
<div className="blg-buttons">
{oneTwoFourOptions.map((d, i) => {
return (
<label key={'onetwofour-' + i}>
<input
type={"radio"}
value={oneTwoFourOptions[i]}
checked={oneTwoFour === oneTwoFourOptions[i]}
onChange={this.handleOneTwoFourChange}
/>
<span>{oneTwoFourOptions[i]}</span>
</label>
)
})}
</div>
</form>;
const quarterOptions = ["All", "OT", "Half 1", "Half 2", "Q1", "Q2", "Q3", "Q4"];
const quarterButtons =
<form>
<div className="blg-buttons">
{quarterOptions.map((d, i) => {
return (
<label key={'quarter-' + i} style={{"width":"50%"}}>
<input
type={"radio"}
value={quarterOptions[i]}
checked={quarter === quarterOptions[i]}
onChange={this.handleQuarterChange}
/>
<span>{quarterOptions[i]}</span>
</label>
)
})}
</div>
</form>;
return(
<div>
<div style={{"width":"25%", "float":"left", "margin":"0 auto", "padding":"5px"}}>
{quarterButtons}
</div>
<div style={{"width":"25%", "float":"left", "margin":"0 auto", "padding":"5px"}}>
{oneTwoFourButtons}
</div>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
.blg-buttons {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.blg-buttons input[type=radio] {
visibility:hidden;
width:0px;
height:0px;
overflow:hidden;
}
.blg-buttons input[type=radio] + span {
cursor: pointer;
display: inline-block;
vertical-align: top;
line-height: 1;
font-size: 1.0vw;
padding: 0.5vw;
border-radius: 0.35vw;
border: 0.15vw solid #333;
width: 90%;
text-align: center;
color: #333;
background: #EEE;
}
.blg-buttons input[type=radio]:not(:checked) + span {
cursor: pointer;
background-color: #EEE;
color: #333;
}
.blg-buttons input[type=radio]:not(:checked) + span:hover{
cursor: pointer;
background: #888;
}
.blg-buttons input[type=radio]:checked + span{
cursor: pointer;
background-color: #333;
color: #EEE;
}
.blg-buttons label {
line-height: 0;
font-size: calc(0.85vw);
margin-bottom: 0.1vw;
width: 90%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<div id='root'>
Come On Work!
</div>
Also, here is a screenshot of an inspection of the buttons in my app (can be found at bigleaguegraphs.com/nba/shotcharts-pro as well), showing the true error that I am having:
The error is in this overhang of the buttons that is not due to padding or margin. I have seemingly gone through every single aspect of the CSS styling my radio buttons, and I have no idea why the element extends a few extra pixels outward to the right.
Amazingly / unfortunately, this is not occurring in the example above, although there is a different issue in the example above where the label element extends a few extra pixels upward (instead of rightward), that I cannot account for.
Any help with removing this extra couple of pixels on the button group would be very much appreciated!
.blg-buttons {
display: flex;
justify-content: center;
/* flex-wrap: wrap; you shouldn't need this */
flex-direction: column;
flex: 1;
}
.blg-buttons label {
display: flex;
line-height: 0;
font-size: 0.85vw;
flex: 1;
align-items: center;
margin-bottom: 5px; /* you don't need that 0.1vw */
font-weight: 700;
}
.blg-buttons input[type=radio]+span {
cursor: pointer;
display: flex;
flex: 1;
justify-content: center;
vertical-align: top;
line-height: 1;
font-size: 1vw;
padding: .5vw;
border-radius: .35vw;
border: .15vw solid #333;
width: 90%;
/* text-align: center; <-- you don't need this with flex */
color: #333;
background: #eee;
}
You should try and use flexbox where possible. I worked this out by playing with your site, so where i saw .nba_scp_cp_rbs i replaced with .blg-buttons (hope that's right). But yeh, avoid using stuff like width: 90%, with flex you rarely have to explicitly define widths, and you can size things based on padding & margins, leading to way less weird sizing bugs like yours :)
picture proof of it working
I'm building a react app using facebook's:
https://github.com/facebookincubator/create-react-app
along w SASS: https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-a-css-preprocessor-sass-less-etc
I'm at a point now where I need to add a dropdown menu to the header. Similar to the header icons on StackOverflow in the top right that open and close on click.
I know this sounds like a dumb question but what is the right way to do this? Do I need to add a UI Framework like a bootstrap for something like this? I have no need for all the bootstrap theming etc...
Thank you - and please be kind to the question given I'm a solo developer and could really use some help building a solid foundation on my app.
Thanks
Yes you can do this easily with just React:
class Hello extends React.Component {
render() {
return <div className="nav">
<Link />
<Link />
<Link />
</div>;
}
}
class Link extends React.Component {
state = {
open: false
}
handleClick = () => {
this.setState({ open: !this.state.open });
}
render () {
const { open } = this.state;
return (
<div className="link">
<span onClick={this.handleClick}>Click Me</span>
<div className={`menu ${open ? 'open' : ''}`}>
<ul>
<li>Test 1</li>
<li>Test 2</li>
<li>Test 3</li>
</ul>
</div>
</div>
)
}
}
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
.nav {
display: flex;
border-bottom: 1px solid gray;
background: white;
padding: 0 10px;
}
.link {
width: 100px;
border-right: 1px solid gray;
padding: 10px;
text-align: center;
position: relative;
cursor: pointer;
}
.menu {
opacity: 0;
position: absolute;
top: 40px; // same as your nav height
left: 0;
background: #ededed;
border: 1px solid gray;
padding: 10px;
text-align: center;
transition: all 1000ms ease;
}
.menu.open {
opacity: 1;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
you can use react-select like this :
var Select = require('react-select');
var options = [
{ value: 'one', label: 'One' },
{ value: 'two', label: 'Two' }
];
function logChange(val) {
console.log("Selected: " + JSON.stringify(val));
}
<Select
name="form-field-name"
value="one"
options={options}
onChange={logChange}
/>
https://github.com/JedWatson/react-select
also this library :
https://www.npmjs.com/package/react-dropdown
Seems like your project is still in his infancy. And that you willing to incorporate a library to your project. So I would definitely recommend you to choose a library right now.
With React you could create your own menu without much effort. But you will also need other components for the rest of your app. And the quality of your menu (and other components) will most likely be greater if you choose a library used by many (rather than your own). For "quality" I mean: UX design, HTML standards, API reusability, number of defects, etc.
If you think your app will be small and won't need an entire UI Framework, you might want to search for an isolated component for menu. Here is a list of navigation components (including the number of github stars of each project):
https://devarchy.com/react/topic/navigation
But in most cases I would choose an entire UI Framework instead: https://devarchy.com/react/topic/ui-framework
And here are some demos of the menu/nav/app-bar of some popular UI Frameworks:
https://ant.design/components/menu/
https://react-bootstrap.github.io/components.html#navs-dropdown
http://www.material-ui.com/#/components/app-bar
Custom dropdown
Dropdown.js
import React, { useState } from "react";
import { mdiMenuDown } from "#mdi/js";
import Icon from "#mdi/react";
export default function DropDown({ placeholder, content }) {
const [active, setactive] = useState(0);
const [value, setvalue] = useState(0);
return (
<div className={active ? "dropdown_wrapper active" : "dropdown_wrapper"}>
<span
onClick={() => {
setactive(active ? 0 : 1);
}}
>
{value ? value : placeholder} <Icon path={mdiMenuDown} />
</span>
<div className="drop_down">
<ul>
{content &&
content.map((item, key) => {
return (
<li
onClick={() => {
setvalue(item);
setactive(0);
}}
key={key}
>
{item}
</li>
);
})}
</ul>
</div>
</div>
);}
dropdown.css
.dropdown_wrapper {
position: relative;
margin-left: 100px;
cursor: pointer;
}
.dropdown_wrapper span {
padding: 12px;
border: 1px solid #a8aaac;
border-radius: 6px;
background-color: #ffffff;
display: flex;
color: #3d3e3f;
font-size: 16px;
letter-spacing: 0;
line-height: 26px;
justify-content: space-between;
text-transform: capitalize;
}
.dropdown_wrapper span svg {
width: 20px;
margin-left: 80px;
fill: #fbb800;
transition: 0.5s ease all;
}
.dropdown_wrapper.active span svg {
transform: rotate(180deg);
transition: 0.5s ease all;
}
.dropdown_wrapper .drop_down {
background-color: #fff;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
position: absolute;
top: 53px;
width: 100%;
z-index: 9;
border-radius: 6px;
border: 1px solid #a8aaac;
height: 0px;
opacity: 0;
overflow: hidden;
transition: 0.5s ease all;
}
.dropdown_wrapper.active .drop_down {
height: fit-content;
opacity: 1;
transition: 0.5s ease all;
}
.dropdown_wrapper .drop_down ul {
list-style: none;
padding-left: 0;
margin: 0;
padding: 10px;
}
.dropdown_wrapper .drop_down ul li {
padding: 10px 0px;
color: #3d3e3f;
font-size: 16px;
letter-spacing: 0;
line-height: 26px;
text-transform: capitalize;
cursor: pointer;
font-weight: 300;
}
.dropdown_wrapper .drop_down ul li:hover {
color: #faab1e;
transition: 0.5s ease all;
}
parent.js
<DropDown placeholder="select a type" content={["breakfast", "lunch", "dinner", "Snacks"]} />